The parser has no asset catalogue, so it cannot know whether an
evidence figure is decorative. Move the empty-alt rejection from
parse-time (a syntax error) to publish validation, where the mock
treats static evidence assets as decorative: false since the static
registry predates the Asset capability. The real rule is that the
server judges alt against Asset.decorative.
Item 1 (real bug): contractOperations.execute only invalidated the cached
CSRF token on UNAUTHENTICATED (401). A CSRF-specific rejection normally
arrives as FORBIDDEN (403) -- the platform classifies any 403 response as
FORBIDDEN unconditionally -- so a token rejected during an ordinary document
save left the stale token cached and every subsequent Studio mutation kept
failing until reload. Extracted invalidateTechLogCsrfOnOutcome() so
production and the composition test call the identical function; it now
invalidates on both UNAUTHENTICATED and FORBIDDEN.
Item 2: the upload transport's uncontracted-status fallback hardcoded status
503, so an uncontracted 401/403 body never reached the gateway's
error.status === 401 || 403 invalidation check. Passes the real
response.status through.
Item 3: safeOperation()'s auth-profile parameter is now typed as a union of
the two valid profile constants instead of a bare string, and
assertExactlyOneTechLogStudioBootstrapOperation() fails composition closed
if getStudioSession stops being the sole caller of the credential-free
bootstrap profile.
Item 4: corrected two stale operation counts in the adapter review doc.
Both new tests for items 1 and 2 were run and shown failing before their
fix, per this task's TDD standard for error-path changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C1 (Critical): getStudioSession was stamped with the same
TECH_LOG_STUDIO_SESSION auth profile as every other Studio operation, and
that profile requires the CSRF header it is getStudioSession's own job to
issue -- an unconditional cycle that recursed without bound in HTTP mode.
Fixed with a credential-free TECH_LOG_STUDIO_BOOTSTRAP auth profile for
getStudioSession alone, a synchronous re-entrancy guard in
createCsrfTokenProvider as defense in depth, and a throwing stub in place of
the prior `let x!: T` assertion. Added a composition-level regression test
that wires the real executor, CSRF provider, and credential-attach function
together and proves getStudioSession dispatches exactly once while its token
reaches both a JSON operation and the multipart upload.
Also: invalidate the cached CSRF token on a 401/403 from the upload path
(I2), a throwing useStudioAssetGateway() accessor so Task 11 cannot silently
compile a null-gateway UI (I3), and the M1-M5 minors from the review (guard
a malformed success body, cover the untested error fallbacks, align aborted
uploads with the JSON path's non-retryable CANCELLED mapping, derive the
credential header name from one source instead of two, and correct the
adapter review doc's operation count).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wires the whole Asset capability into the running application: the
multipart upload transport (the contract runtime can only express JSON
bodies), a single composition-root-owned CSRF provider shared between
the platform's credential collaborator (18 JSON operations) and the
upload transport (1 multipart operation), and Studio/StudioShell
exposure of the Asset gateway alongside the existing document gateway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 3's contract contribution legitimately registered a TECH_LOG_STUDIO_SESSION
auth profile, growing INSTALLED_REST_AUTH_PROFILES from 2 to 3 entries. The
exact-key-set regression test at contract-registry-immutability.test.ts:91
correctly caught the drift; test:unit was left off Task 3's verification list,
so it went unnoticed until Task 5 ran the full suite. Updates the expected key
list rather than weakening the assertion, so it keeps forcing a reviewer to
confirm any future registry change was intended.
Adds TECH_LOG_STUDIO_SOURCE (MOCK | HTTP, default MOCK) to the V2 runtime
config schema so a build can switch createTechLogFeatureInstalledInput
between the mock and HTTP Studio gateways without a rebuild. V1 documents
predate the key and always normalize to MOCK. The HTTP gateway is
constructed with only { operations } per Task 4's actual signature -
no CSRF provider is wired here; that lands with attachCredentials at a
later composition-root task.
Updates every existing Studio test call site to the new required
createTechLogFeatureInstalledInput(context) signature via a shared
tests/helpers/studio-install-context.ts MOCK fixture, so the whole
existing Studio suite keeps exercising the mock adapter unchanged.
Two review Minor findings against the brief itself, both closed:
- tests/mocks/handlers/tech-log-studio.ts silently dropped documented
query filters instead of mirroring the mock gateway it wraps:
listStudioDocuments forwarded only q/limit (dropping kind,
publicationStatus, nextAction, projectId, sort, cursor),
listStudioCatalog forwarded only type (dropping q/cursor/limit), and
listStudioPublications ignored all four of its parameters outright.
Added a shared queryParams() helper and forward every field each
operation's projectRequest actually emits, plus a regression test
that narrows the fixture set by kind through the real HTTP gateway
(confirmed it fails without the fix).
- canonicalInputIdentity (on the idempotency-safety path, reused by
Task 6) had no test. Added three tests against mutationIntent(): a
large Korean payload stays within the byte bound, the same input
retried twice yields an identical identity, and a mid-codepoint
truncation cut leaves no replacement character (confirmed the third
fails without the strip). The known collision limitation of any
bounded-length identity scheme is documented in the test file rather
than solved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds createHttpStudioGateway, the adapter the Studio UI calls in
production. It drives every mutation through mutationIntent()
(defineMutationIntent/defineIdempotencyKey, not
createBrowserMutationIntentFactory, so the caller-supplied idempotency
key is preserved rather than regenerated) and leaves header
construction to the executor/credential collaborator entirely - the
gateway never sees or sets Idempotency-Key or x-csrf-token itself.
Also moves stable-stringify.ts out of adapters/mock/ so the mock and
http adapters share one pure function without production code
depending on the mock directory, and fixes the resulting import in
cursor.ts, mock-studio-gateway.ts, and mock-studio-gateway.test.ts.
Adds MSW handlers (tests/mocks/handlers/tech-log-studio.ts) that wrap
the reference mock-studio-gateway implementation, and a contract test
suite that drives the gateway through a thin fetch-based executor
built from the contract's own projectRequest, proving canonical
path/body/header construction without assembling the full platform
transport.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
release-manifest.test.ts's fixture derived manifest.contractSet.packages
from EXPECTED_CONTRACT_SET_PACKAGES itself, so the equality it checked was
satisfied by construction and CONTRACT_SET_PACKAGE_MISSING became
unreachable from any test in the repo. Adds two independent checks: a
literal (not derived) assertion that EXPECTED_CONTRACT_SET_PACKAGES really
contains @tech-log/studio-contract@2.0.0, and a negative test with a
manifest that omits a package the real expected set requires, asserting
CONTRACT_SET_PACKAGE_MISSING. Confirmed the negative test has teeth by
temporarily disabling the missing-package branch in
verifyContractSet (src/contracts/contract-set.ts) and observing the test
fail before reverting.
Also narrows tech-log-studio-contract-contribution.ts's problemSchema to
match canonical ProblemDetails exactly: title max 200 (was 240) and type
unbounded (was max 512; canonical only constrains it as
format: uri-reference). Both prior values were over-permissive, so no
previously-accepted document is now rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Registers the TECH_LOG_STUDIO_SESSION SAME_ORIGIN_COOKIE auth profile and
declares the tech-log-studio-http-v1 contribution covering all 18 JSON
Studio operations (everything except the multipart uploadStudioAsset),
built from two shared builders (safeOperation/keyedOperation) so every
KEYED command gets IDEMPOTENCY_REPLAY recovery and a zero retry budget,
and every SAFE read gets a plain retry budget, without repeating the
declaration shape 18 times.
Rescopes the canonical package identity from "tech-log-studio-contract"
to "@tech-log/studio-contract" (generator, canonical-source.json,
contract-generation.test.ts) because the platform's contribution
composer requires an npm-scoped packageId; the unscoped form failed
composition. Updates the release-manifest test fixture, which hardcoded
an empty expected contract set, to derive its expected packages from the
real installed set now that TechLog is always installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 3 of the TechLog contract task added two pieces of genuinely new
logic with no assertion on their output: the totals.needsValidation
count in getDashboard(), and the detail() rewrite that avoids a
self-reference when computing WorkingCopyDetail.nextAction. Both would
have passed every existing test if they regressed.
- needsValidation is checked against an independent count derived from
listDocuments()'s per-document nextAction, with sanity bounds so a
filter that always returns 0 or the full count can't pass silently.
- detail()'s nextAction is asserted on two fixtures whose expected
value is justified by the asserted preconditions alongside it
(published-at-current-version -> NONE; INVALID-but-current-validation
-> FIX_VALIDATION). Verified locally that hardcoding nextAction to a
constant in detail() makes the second assertion fail, and that
zeroing needsValidation's count makes the dashboard assertion fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vendors the canonical studio-v1.yaml, generates types via an isolated
`pnpm dlx` toolchain (openapi-typescript needs TypeScript 5's classic
compiler API; this repo pins TypeScript 7.0.2 per VD-01, whose root
export has none), and adds an offline drift gate that checks the
vendored yaml/generated types/canonical-source.json against each
other without touching the sibling design-package repo or the network.
Regenerating from canonical surfaces real, new required fields on
existing schemas (WorkingCopyDetail.nextAction, PreviewDetail/PublicPreview
.dependencyRevision, StudioDashboard.totals.needsValidation,
PublicationSnapshot.contentFormatVersion/rendererContractVersion) and a
new required EvidenceFigureBlock.asset. The mock gateway and fixtures
are updated to satisfy the former; the latter exposes a real authoring-
vs-rendering conflation in the content-format parser (it declared its
output as the server's fully-resolved PublicRenderModel type, which it
has no asset catalog to satisfy). Split that boundary: the parser now
produces an authoring block type omitting the resolved asset, and each
of its three consumers (the mock gateway, the Studio instant preview,
and the static Case demo page) attaches the resolved descriptor from
its own asset source through a shared, pure domain-level resolver.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both headers carried their own literal list of {label, path}. That made the
route contract and the header two sources for the same three facts — which
routes are navigable, what they are called, and in what order — with nothing
keeping them in step: a renamed route or a reordered menu could be right in one
place and stale in the other.
techLogNavigation(layoutGroup) derives the menu from TECH_LOG_ROUTE_REGISTRY,
where navigationOrder is what makes a route navigable. Output is byte-identical
to the previous literal lists, pinned by a new test.
Derived from TechLog's own route contract rather than from the composed
registries: .dependency-cruiser.json freezes an exact allowlist of files that
may read src/features/installed-*, explicitly so that coupling cannot spread,
and a feature header is not on it.
The studio header keeps its active-state rules — "작업본" stays highlighted
across /studio/documents/* except on the new-document screen — because that is
presentation behaviour the contract has no opinion about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The template's demonstration screens exist to explain the template. A product
replaces them with its domain, so deleting them is the expected end state — but
two gates were coupled to them, and the merge accommodated that coupling instead
of fixing it.
product-features.test.ts derives its own scope now: a registry must gate on the
manifest when it imports a module belonging to a manifest-declared feature. The
previous fix put installed-feature-runtimes.tsx in the exempt list, which
silenced the guard for that file permanently. Verified by removing the manifest
reference from installed-feature-adapters.ts and watching the guard fail; a
counter asserts the sweep still watches at least one file.
product-feature-switch.test.tsx exercises the kill switch end to end again. The
mechanism is the ownership lookup plus isFeatureActive, which has nothing to do
with which screens ship, so the ownership map is the fixture: one real
registered route attributed to a real installed feature, with the product's own
router, components and codecs.
That restoration exposed a real gap. The navigation-withdrawal half is not
implemented here: it lives in the template's PrimaryNavigation and this product
does not render the template's AppShell at all — the public header is a
hand-written list of paths. A disabled feature's route is refused by the router
but its link would still be advertised. Harmless only while no feature-owned
route is navigable, which is now asserted so the gap cannot ship silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Integrates the frontend template sync (a0fbafb → 5434760) into the TechLog UI
migration. Merged in this direction so every conflict is resolved and proved in
the worktree; main is only fast-forwarded afterwards and never holds a state
that was not verified here.
15 conflicts. The rule throughout: keep the template's mechanism, keep the
product's content, and never invent a third state neither branch would accept.
The template's product manifest and its runtime feature kill switch are adopted.
The route registry is deliberately not composed from contract.routes: the
reference feature still declares screens this product deleted, and reducing over
them would register paths with no component behind them. ROUTE_FEATURE_OWNER is
narrowed to registered routes for the same reason. The first resolution did
compose from contract.routes and was rejected by product-features.test.ts.
Three files pinned counts and a digest describing the gate contract. Neither
side's numbers describe the merged config/ci/gates.json, so they were recomputed
from it rather than chosen: 27 gates, 82 commands, 94 command references, 107
evidence references, 128 artifacts, shape sha256 5063586d.
README.md and docs/accessibility/manual-checklist.md now enumerate this
product's 27 routes, which the template's own verify:documentation requires.
product-feature-switch.test.tsx was rewritten around the invariant that still
applies here — no registered route without a component — rather than deleted
with the screens it used to exercise.
docs/operations/template-merge-2026-08-17.md records every decision, the gate
results, and the three follow-ups this merge deliberately did not decide.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries eight template commits: the provider sandbox actually running, release
admission to a named environment, the product feature manifest with its runtime
kill switch, architecture and documentation rules that match what is enforced,
the removability fixtures, and the browser, visual and performance evidence.
Product identity is unchanged. `package.json` keeps `tech-log-frontend` and the
catalog keeps the Tech Log naming; the home page was not in the delta. The
visual baselines are this product's own — the template's were excluded from the
transplant and these were regenerated here, where the only difference is the
platform overview's new product-feature section.
What this repository gains operationally: `config/runtime/{local,development,
staging,production}.json` with `FE-GATE-027` refusing an artifact whose runtime
document does not match the environment it is being admitted to, and
`FEATURE_OVERRIDES` for taking an installed feature out of service without a
rebuild.
Verified here: eight gates green, build green, visual 5/5, and 1,858 of 1,859
tests in the suites that do not need a sandbox — the one failure passes in
isolation and is a jsdom lazy-chunk timeout under parallel load. The provider
suites cannot run on this machine at all: `kernel.apparmor_restrict_unprivileged
_userns=1` makes `bwrap --unshare-net` fail, reproducible without any code from
either repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The synced suite contained an assertion that pinned one of two equally
configured deadlines, so it passed alone and failed in a full parallel
run. Fixed upstream and carried here with the template pin moved to
`a0fbafb`.
Three consecutive runs of the 27-file set that reproduced the failure now
pass at 485 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.
The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.
`template.lock.json` now pins the synced revision and tree.
Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>