Compare commits

..
90 Commits
Author SHA1 Message Date
DongHyeonkaandClaude Opus 5 d84b57bb3f fix: let the mock's dependency revision observe the Asset store
`createMockStudioGateway`'s default `dependencyRevision.current()` returned a
literal constant and never consulted `dependencies.assets`, so the staleness
guards in `createStudioPreview` and `publishStudioDocument` could not fire for
an asset-store mutation between validate and preview.

The live case: a document references an evidence key with `alt=""` and the
store holds only a `decorative: true` Asset for it, so validation is correctly
VALID with zero issues. A newer `decorative: false` Asset then wins that key.
Preview succeeds, the figure resolves to `decorative: false, alt: ""`, and
publish snapshots it verbatim -- a meaningful image with no accessible name,
validated clean, with nothing anywhere reporting an error.

The default now folds the Asset store into the revision. Each Asset is reduced
to the fields the mock's own validation and projection read -- identity and
resolution order (`id`, `assetKey`, `updatedAt`), resolvability
(`managementStatus`, `publicPath`), the alt rule (`decorative`, `altText`), and
what the published `ResolvedAsset` carries (`mediaType`, `width`, `height`) --
canonicalized with `stableStringify`, sorted, and folded into a 128-bit FNV-1a
digest. Sorting the canonical strings is what makes it order-independent, which
this mock's reproducibility across the suite depends on.

It is a projection rather than the whole record because the excluded fields cost
sensitivity without buying any. `usageCount` is the clearest: it counts
referencing documents, so on a real backend publishing any document that uses an
Asset would invalidate every other author's in-flight validation, while changing
nothing the validator or renderer reads.

An empty store still reports the bare catalog constant -- that is the world the
seeded fixtures were validated against, and `fixtures.ts` now shares the one
definition rather than retyping the literal.

`findResolvableAsset` is untouched: it is a single-point-in-time predicate and
is correct as it stands. Seeing a change *between* two points is the revision's
job. A caller-supplied `dependencyRevision` still wins outright.

The asset-picker test that reached `failureOf`'s `ContentFormatError` branch did
so only because the revision could not move; it now pins its own revision to
keep reaching the projection, and asserts the problem detail so the two 409
paths cannot be confused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:44:53 +09:00
DongHyeonkaandClaude Opus 5 cf45bcc7dc test: give the Asset Library a visual golden, and a URL that resolves
`/studio/assets` was the one route in the TechLog route contract missing
from `TECH_LOG_CANONICAL_ROUTES` and `TECH_LOG_STUDIO_STATE_PATHS`, so it
had no golden, no responsive-overflow check, and no Axe scan. That gap was
not hypothetical: the responsive overflow just fixed in the Asset search row
shipped in two places and was only caught in the Picker, which has a golden;
the Library's identical copy would have gone unseen.

Verifying the render before capturing anything turned up a second defect.
`studioSpaPathPatterns` in the production serving contract never listed
`/studio/assets`, so a hard navigation or reload of that URL was answered
with the in-shell Studio 404 -- the screen was reachable only by client-side
navigation from another Studio page. A golden taken then would have frozen a
404. With the pattern added, the route serves 200 text/html like its
siblings, and the page renders correctly at both breakpoints:
scrollWidth == clientWidth == viewport at 360 and 1440, `main` and the `h1`
visible, the search label/input/button all inside the 360px viewport, and no
critical or serious Axe violations in chromium.

Three new goldens, no existing golden regenerated:
tech-log-studio-assets-{360,1440} from the canonical route list and
tech-log-studio-asset-library-1440 from the Studio state list, matching how
every other Studio route appears in both. `pnpm test:visual` is 133 passed,
up from 130.

The demo profile seeds no assets, so the golden captures the empty state --
which still covers the header, heading, search row and status region, the
surface the overflow regression lived on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:12:09 +09:00
DongHyeonkaandClaude Opus 5 b2f577ef49 test: stop DOM-element assertions from OOM-killing the worker
`node:assert` builds its `AssertionError` eagerly, running `util.inspect`
over both operands with `depth: 1000`, `getters: true` and
`maxArrayLength: Infinity`. A React-rendered DOM element carries
`__reactFiber$*` / `__reactProps$*` as own enumerable properties, and that
fiber graph re-expands once per traversal path, so inspecting a single
rendered element allocates without bound. Measured on the Asset Library
heading: depth 6 = 1.3MB, 8 = 7.8MB, 10 = 36MB, 12 = 135MB -- at Node's
depth 1000 the worker dies before any `AssertionError` exists.

The damage is not the crash, it is the disguise. Equality assertions only
inspect their operands on failure, so these sites stayed invisible while
green and detonated exactly when the behaviour they guard regressed --
reporting as `worker exited unexpectedly` with a truncated count
(`8 passed (13)`) and no failing test named. Breaking the delete-path focus
restoration in `asset-library.tsx` reproduced it: 29.5GB anon-rss and the
system OOM killer, or a V8 heap abort in 3s under a 512MB cap. The same
regression now fails in 1.15s with `expect(element).toHaveFocus()` naming
both the expected heading and the `<body>` that took focus instead.

`expect` is not affected -- vitest prints and diffs DOM nodes through
pretty-format's DOM plugin, which reads tag/attributes/children and never
touches the fiber -- so every unsafe site converts to a matcher:
`toHaveFocus()` for the three focus comparisons, `not.toBeInTheDocument()`
for the sixteen `assert.equal(queryBy..., null)` absence checks, which are
equally lethal (proved separately: element-vs-null inspects the element).

Three layers so this cannot come back:
- the 20 live sites in asset-library/asset-picker now use matchers;
- `test-assertion-boundary/no-element-operand-equality` fails `pnpm lint`
  when a DOM-element expression reaches `node:assert` equality, resolving
  local bindings and exempting the forms that cannot fail with an element
  in hand (`assert.notEqual(el, null)`, `el.textContent`);
- a 2048MB worker old-space ceiling in `vitest.config.ts` bounds any future
  runaway to a legible `Reached heap limit` abort in seconds instead of an
  OOM-killed machine (heaviest suite peaks near 1.3GB RSS).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:03:33 +09:00
DongHyeonkaandClaude Opus 5 2483c4032f test: regenerate the five editor goldens the Asset search control moved
Stale because of the search UI this task added to the CASE editor's asset
panel, so they belong to this work. Each diff was inspected before
regenerating; every difference is attributable to the added control.

Four render `.../edit` for a CASE (2999 -> 3066px tall, +67px):
  document-edit-1440, case-editor-1440, conflict-editor-1440,
  dirty-leave-dialog-1440 -- the comparator flags one 401x95 region at
  (131,2795), the "Asset 검색" label, input and 검색 button. Above it the
  maximum per-channel delta is 1/255, i.e. visually unchanged.

  conflict-editor was not in my first estimate: document ...115 is a CASE
  too, so it renders the same panel.

document-edit-360 (3627 -> 3694px, +67px) shows more because the layout
is single-column: byte-identical above y=3032, then the added control,
then the status rail below translated down (67px, 68px past the 저장
button -- sub-pixel rounding, same content).

Its width returns to 360 here; the previous run captured it at 382,
which was the overflow fixed in d2574a3, not a golden to bake in.

`corepack pnpm test:visual`: 130 passed. `git diff --stat` for this
commit lists exactly these five PNGs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:40:49 +09:00
DongHyeonkaandClaude Opus 5 d2574a3be7 fix: stop the Asset search row from overflowing the 360px viewport
Caught by the visual suite, not predicted: at 360px the Picker's new
search row laid itself out at 366px inside a 328px column and pushed the
submit button off-screen, taking the document's scrollWidth to 382.

Both defaults involved resolve to a min-content minimum: an implicit grid
track (`auto`) and a flex container (`min-width: auto`). Neither will
shrink below the row's min-content, so the input's `flex: 1; min-width: 0`
never got the chance to give the button room. `minmax(0, 1fr)` on the
track and `min-width: 0` on the row are the same pair
`.studio-editor-layout` already needs one file over.

`.studio-asset-tools` gets the guard too -- the Library's search row has
the identical structure and no visual golden watching it. Verified in
Chromium at 360 and 1440 on both routes: scrollWidth equals the viewport
and no element extends past it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:35:25 +09:00
DongHyeonkaandClaude Opus 5 9950cb9d6b fix: clear the decorative flag when the upload dialog is handed a different file
The twin of the previous commit, with a sharper consequence. `decorative`
does not merely describe the previous image, it *exempts* it:
`validate-working-copy.ts`'s EVIDENCE_ALT_REQUIRED reads
`Asset.decorative`, so a flag inherited from a discarded divider lets a
meaningful diagram publish with no accessible name at all -- the check
passes rather than catching it. Stale alt text ships a wrong description;
stale `decorative` ships none.

Same terminal-state retry path: tick 장식용 for `divider.png`, have it
rejected, pick `sequence.png`, upload -- and `sequence.png` shipped as
decorative with `altText: undefined`.

Resetting it also re-enables the alt input, so the post-selection focus
call no longer has to ask whether it would land on a disabled control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:23:32 +09:00
DongHyeonkaandClaude Opus 5 1f2cba79e9 fix: clear alt text when the upload dialog is handed a different file
The file input's `onChange` reset `state` to `IDLE` and left `altText`
untouched. On success that is harmless -- the dialog unmounts. But
REJECTED, QUARANTINED and TRANSPORT_FAILED all leave it mounted with the
file input re-enabled, and that is precisely the retry path: upload
`db-schema.png` described as "DB 스키마", have it quarantined, pick
`sequence.png`, upload -- and `sequence.png` shipped described as
"DB 스키마", passing EVIDENCE_ALT_REQUIRED and publishing with a caption
about a different image.

Cleared on every file selection rather than only after a failure: alt
text describes one image, and "which file is this describing" has one
honest answer per selection. `submit()`'s existing ALT_REQUIRED refusal
turns the emptied field into a stop rather than a silent omission.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:17:33 +09:00
DongHyeonkaandClaude Opus 5 f69edb633d feat: search the server from the Asset Picker without shrinking preview's catalog
The Picker asked for `{ managementStatus: "READY", limit: 50 }` and
ignored `nextCursor`, so the 51st-oldest READY asset onward could not be
inserted at all. It sits inside the editing flow, where scrolling a long
list is the wrong interaction, so it gets search rather than a "더 보기"
control -- and it still loads a first page, because an empty panel until
you type is hostile to an author reaching for the asset they uploaded a
minute ago.

The trap this creates is the substance of the change. The editor screen's
Asset array feeds two consumers with opposite needs: the Picker's
*displayed* list, which a search must narrow, and Instant Preview's
*resolution catalog*, which a search must never narrow -- its gate
rejects any key no loaded asset backs. Handing search results straight to
the screen's `setAssets` would blank previously-inserted evidence figures
the moment the author typed a query.

They are kept apart by making the screen's callback additive by
construction rather than by convention: `mergeAssetCatalog` (domain,
beside `findResolvableAsset`) can only grow the set, and both writers --
observed pages and fresh uploads -- go through it. The prop is renamed
`onAssetsObserved` so the contract reads as "what the Picker saw", not
"what to show"; the replacing version was one `setAssets` reference away
and looked correct.

A key backed by neither the first page nor the current results is still
unresolvable. That is the known deferred limitation, not this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:16:31 +09:00
DongHyeonkaandClaude Opus 5 a5825c18b5 feat: give the Asset Library server search and cursor paging
`StudioAssetGateway.listAssets` has always accepted `q`/`cursor` and
returned `nextCursor`, and this screen's own heading promises search
("업로드한 Asset을 검색하고..."). The component asked for `{ limit: 50 }`
and dropped `nextCursor`, so past the 51st asset older assets were
unmanageable with nothing on screen admitting anything was left out.

Follows `document-list.tsx`'s established shape: a `searchDraft`/`q`
pair so only a submitted query is an effect dependency (exactly one
request per deliberate search, never a trailing one), and a
`nextCursor`-driven control that appends rather than replaces.

Two things the pattern did not already cover:

- `RequestOptions.signal` is advisory, so aborting is not enough to stop
  an abandoned response from painting over a newer one. An `active` flag
  that flips synchronously on dependency change closes that race.
- "더 보기" unmounts exactly when the last page arrives, which would
  strand focus on `<body>`. Focus moves to the first appended row
  unconditionally, falling back to the page heading -- the same anchor
  the delete path already uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:12:58 +09:00
DongHyeonkaandClaude Opus 5 419d9d006d ci: give test:tech-log its own gate command and junit evidence
The TechLog suite already ran in CI, but only inside `test:coverage`'s combined
`vitest run tests/runtime-schema tests/unit … tests/features/tech-log`
invocation on FE-GATE-005. A TechLog regression therefore surfaced as a
coverage-gate failure, and the only junit that carried it was coverage.xml,
which reports every other suite at the same time.

`test-tech-log` now sits on FE-GATE-007 beside `test-reference-feature`, the
sibling it mirrors — both drive a suite under `tests/features/` — and declares
`artifacts/tests/tech-log.xml`, which `test:tech-log` already wrote, as its own
command-generated junit evidence. The gate log now names
`$ corepack pnpm test:tech-log` as its own step and the failure lands on the
suite that produced it.

Adding a command and an artifact moves the exact-count authority to 84
definitions / 96 references / 130 artifacts (109 evidence references), and
changes the canonical gate shape digest because FE-GATE-007's commandIds and
evidenceArtifactIds are part of it. There is no tooling to regenerate that
digest, so it was recomputed by hand under the standing procedure: a fresh
transcription of `canonicalGateShapeSha256` first reproduced the committed
f3cc9075… from the unedited config/ci/gates.json — proving the transcription,
not just agreeing with whatever the check compares against — and only then
hashed the edited file to 98d19911….

`corepack pnpm ci:gate -- FE-GATE-007` passes end to end; the generated
workflow bytes are unchanged, since the yml dispatches gates rather than
commands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 12:58:41 +09:00
DongHyeonkaandClaude Opus 5 74281b0277 test: probe the provider sandbox instead of failing on it
The 16 sandboxed provider tests in `tests/unit/ci-artifact-contract.test.ts`
fail wherever unprivileged user namespaces are denied. bubblewrap is installed
and answers `--version`, but `bwrap --unshare-net … -- /bin/true` exits 1 with
`bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`, and it fails the
same way with no netns flag at all (`setting up uid map: Permission denied`), so
this is the whole nested-userns capability and not one option. Sixteen
assertion errors on every local run buried whatever else the file had to say.

`scripts/lib/provider-sandbox-probe.ts` now runs a trivial command under the
real isolation options — `runProviderInSandbox` spreads the same
`PROVIDER_SANDBOX_ISOLATION_ARGUMENTS`, and a test fails if an isolation option
is added to the run without the probe having to clear it. Capability is
measured, never inferred from the binary existing or from a version string;
both would pass here.

An unusable sandbox means two different things in two places, so the decision
is explicit. Locally it is an environment fact: the affected tests skip and
carry the bwrap diagnostic as their skip note, visible as `↓ … [reason]`. In CI
it is a regression — a security gate that silently stopped running is exactly
what these tests exist to catch — so the same probe result fails the run
through one guard test that says "the provider sandbox is unavailable" instead
of sixteen assertion errors.

CI is detected with `CI === "true"` via a new `isCiRun`, sharing the predicate
that already gates `ciBuildEnvironmentFailures`. The workflow sets it at the
top-level `env:` block, so it holds in every job; `CI_RUN_ID` and its
`GITEA_`/`GITHUB_` fallbacks are declared only by the release-tier provider
jobs and are absent from the merge gates that run this file, so keying on them
would have left the CI branch permanently dead.

The three `it.each` groups become `it.for` because only `.for` passes the test
context, which is what carries the skip note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 12:55:56 +09:00
DongHyeonkaandClaude Opus 5 1a40522ff8 test: regenerate the remaining 6 goldens stale from Decision authoring
The prior commit (ef17bca) fixed the 7 of 13 test:visual failures that
were a pure dimensional consequence of 79e9aa8's evidence-figure width
rule. The other 6 were left failing pending investigation of their own
diffs rather than being folded into that theory.

Per-file inspection of each diff/expected/actual triple confirms all 6
are stale content from 0355b64 ("feat: add TechLog project decision
authoring"), also an ancestor of main's merge-base -- a "Decision"
document type was added without refreshing the affected goldens:

  tech-log-studio-documents-360        360x2883  (unchanged) -- subtitle
    copy gained ", Decision" before "을 찾고", rewrapping the sentence.
  tech-log-studio-documents-1440       1440x1493 (unchanged) -- same
    subtitle copy change, no wrap at this width.
  tech-log-studio-document-list-1440   1440x1493 (unchanged) -- same
    page/route as documents-1440 (identical diff), same copy change.
  tech-log-studio-document-new-360     360x1000 -> 360x1130 -- a new
    "Decision" radio option/row added to the document-type picker;
    narrow viewport can't absorb the extra row without growing.
  tech-log-studio-document-new-1440    1440x1000 (unchanged) -- same
    new "Decision" radio row; existing bottom whitespace absorbs it at
    this width.
  tech-log-studio-new-document-1440    1440x1000 (unchanged) -- same
    page/route as document-new-1440 (identical diff), same new row.

None of the 6 diffs show anything resembling a rendering defect (no
overlap, no clipped/broken layout) -- each is legitimate new copy or a
legitimate new control from the shipped feature.

test:visual: 124 passed/6 failed -> 130 passed/0 failed. lint and
check:types unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 12:41:25 +09:00
DongHyeonkaandClaude Opus 5 ef17bca445 test: regenerate the 7 goldens stale from the evidence-figure width fix
79e9aa8 ("fix: align TechLog article content widths") narrowed
.evidence-figure/.code-block/.data-table-wrap from
min(61rem, calc(100% + 15rem)) to min(var(--body-copy), 100%), shrinking
every page whose article body contains an evidence-figure diagram. The
goldens were never refreshed after that commit landed, so test:visual
carried 13 failures inherited from main.

Per-failure diff inspection (not just the aggregate count) showed the 13
split into two unrelated causes:

- 7 are a pure dimensional consequence of the width rule -- full-page
  diffs starting at the evidence-figure diagram, page height down by a
  fixed ~93px offset in matched pairs. Regenerated here:
    tech-log-case-1440                              6506 -> 6412
    tech-log-public-fixture-cases-collection-...     6506 -> 6412
    tech-log-studio-document-preview-1440            1904 -> 1811
    tech-log-studio-current-preview-1440             1904 -> 1811
    tech-log-studio-publication-preview-1440         1687 -> 1594
    tech-log-studio-publication-snapshot-1440        1687 -> 1594
    tech-log-studio-immediate-preview-1440           1733 -> 1640

- 6 are unrelated content staleness from a later commit
  (0355b64, "feat: add TechLog project decision authoring") that added a
  "Decision" document type/option without refreshing its goldens. These
  are left failing intentionally -- they are not a dimensional
  consequence of 79e9aa8 and regenerating them here would silently bake
  an unreviewed content change into the baseline:
    tech-log-studio-documents-360/1440,
    tech-log-studio-document-new-360/1440,
    tech-log-studio-document-list-1440, tech-log-studio-new-document-1440

test:visual: 13 failed/117 passed -> 124 passed/6 failed (the 6 above).
lint and check:types unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 12:35:22 +09:00
DongHyeonkaandClaude Opus 5 813f9e16cd test: regenerate only the five goldens this branch actually changed
Attribution was measured, not inferred: the visual suite was run at the
merge-base `9e5fbd1` in a detached checkout (13 failed / 117 passed) and the
`-actual.png` each run produced was compared by SHA-256 against HEAD's. For
13 of the 18 failures the rendered bytes at HEAD and at the merge-base are
identical, so those failures belong to `main`, not here.

Regenerated (all Case-editor surfaces, all grown by the Task 10 Asset panel;
the +293px band was inspected in the new golden and is the "EVIDENCE / 본문에
Asset 삽입" panel):

  tech-log-studio-document-edit-360    360x3313  -> 360x3627
  tech-log-studio-document-edit-1440   1440x2706 -> 1440x2999
  tech-log-studio-case-editor-1440     1440x2706 -> 1440x2999
  tech-log-studio-conflict-editor-1440 1440x2706 -> 1440x2999
  tech-log-studio-dirty-leave-dialog-1440 1440x2706 -> 1440x2999

`--update-snapshots` was restricted to those five tests with `--grep`; a
blanket update would have absorbed the 13 inherited failures and destroyed
the distinction. `test:visual` now reports 13 failed / 117 passed, exactly
the merge-base's set.

Two earlier records are corrected in the spec: the five "pixel-only, not
investigated" failures are all inherited, and `TECH_LOG_STUDIO_DOCUMENT_NEW`
360px was wrongly attributed to the Asset Picker -- it fails at the
merge-base with the identical 28,413 differing pixels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:29:46 +09:00
DongHyeonkaandClaude Opus 5 a889cb5c00 fix: scope the TechLog CSRF invalidation to Studio operations
`contractOperations.execute` is the single executor every installed feature
dispatches through, and it called `invalidateTechLogCsrfOnOutcome` for every
operation. A 403 on an unrelated reference-feature request therefore threw
away a perfectly good TechLog CSRF token, forcing an avoidable
`getStudioSession` round trip on the next Studio operation -- and, when the
session endpoint is itself unhealthy, turning someone else's authorization
failure into a Studio outage.

`invalidateTechLogCsrfOnOutcome` now takes the operation's auth profile and
acts only on the two TechLog Studio profiles. Required, not optional, so the
scoping cannot be dropped again by omission, and the predicate lives in the
feature file: `bootstrap/runtime-adapters.ts` is template-synced and its
change is the one added argument.

The composition test now installs both contributions the way
`installed-contract-contributions.ts` does, and asserts a reference-feature
403 leaves the cached token alone. Reverting the scope check fails exactly
that test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:14:42 +09:00
DongHyeonkaandClaude Opus 5 65f8528ccc test: cross-product Instant Preview's evidence-key pair with the adapter's
`evidence-key-agreement.test.ts` cross-products 144 adversarial combinations
across the mock adapter's gate and descriptor resolver, `validateWorkingCopy`
and the shared pixel resolver -- but omitted `instant-preview.tsx`'s own gate
and descriptor resolver. That pair is byte-parallel to the adapter's (it
lives in `presentation/`, which may not import `adapters/`, so it carries its
own copy of the legacy-key predicate) and it is exactly the pair three earlier
fix rounds regressed.

`instant-preview.tsx` now exports the two expressions it already used, so the
test drives the production code rather than rebuilding it. Added assertions:
gate agreement in both array orders, descriptor equality with the adapter's,
pixel/descriptor agreement, and order independence. The one sanctioned
divergence -- the legacy static key's `assetId`, a fixed literal here versus a
registry-derived one there -- is pinned to that case rather than ignored.

Mutation-checked: breaking the legacy predicate reports 96 of 144 combinations
disagreeing, breaking the legacy descriptor path reports 64 of 144.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:12:06 +09:00
DongHyeonkaandClaude Opus 5 e2a0e695dd fix: normalize uncharacterized read failures in the mock Studio gateway
`idempotent()` was fixed to wrap anything `work()` throws, with a documented
rationale: this port's contract is `StudioGatewayError` only, so nothing else
may cross it. Its `read()` sibling was left unwrapped, so an uncharacterized
internal failure on any of the seven read operations escaped as a raw `Error`
and reached UI code written to catch `StudioGatewayError`.

`read()` now applies the same `failureOf` classification. It has no
idempotency ledger, so only the problem half is used. `boundary()` stays
outside the wrap so an aborted request still surfaces as `AbortError`,
exactly as `idempotent()` arranges it -- asserted by the new test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:09:36 +09:00
DongHyeonkaandClaude Opus 5 e4f9f81a3f docs: record the passthrough output validators as a known limitation
All 18 Studio operations declare `passthrough` (`z.unknown()`) input and
output validators. The choice is deliberate and commented in
`tech-log-studio-contract-contribution.ts`, but it was recorded nowhere a
spec reader looks, and it compounds the cycle's own non-goal: with no running
backend, MSW returns whatever the test author wrote and nothing compares it
to the canonical schemas, so `CONTRACT_VIOLATION` can never fire for a
success-payload shape mismatch. Compile time is currently the only layer that
catches a shape regression.

Recorded in §Task 12 완료 상태 beside "no running-backend verification" -- the
same class of risk -- with the two options for the backend-comparison cycle.
No code change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:08:46 +09:00
DongHyeonkaandClaude Opus 5 172497591f docs: point the spec at canonical-source.json instead of a stale digest
§상태 pinned `sha256:85a65004…` / revision `0ec5582` while the canonical yaml
that actually shipped is `sha256:99f54f56…` / `ce2e748` -- the value
`canonical-source.json` records, the value the contract contribution imports,
and the value the spec's own Task 12 table already quotes. The canonical
source moved during implementation and only one of the two places was updated.

The spec no longer carries the values at all: it names
`contracts/studio/canonical-source.json` as the single record, which is what
the contract contribution reads and what `check:tech-log-contract` verifies,
so the two cannot drift apart again. Task 12 row 3 also records that the drift
gate is now wired into FE-GATE-010 and `test:all`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:08:18 +09:00
DongHyeonkaandClaude Opus 5 6085af51b6 fix: collect alt text and a decorative flag when uploading a Studio Asset
The upload dialog sent `{ file, kind }` only, though `UploadAssetForm` and
`asset-upload-transport.ts` both carry `altText`/`decorative`. Every asset
uploaded through the flagship authoring flow therefore landed as
`altText: null, decorative: false`, and the directive `case-fields.tsx` and
`asset-picker.tsx` build from `asset.decorative ? "" : (asset.altText ?? "")`
could only ever be `alt=""`. The document parsed and previewed correctly and
then failed publish validation with EVIDENCE_ALT_REQUIRED, recoverable only
by hand-editing raw Markdown -- the exact thing the Picker exists to prevent.

The dialog now stages the file instead of uploading on selection, and carries
a decorative checkbox plus an alt-text field (focused as soon as a file is
chosen, submitting on Enter). A decorative asset never demands alt text; a
meaningful one is refused with a stated reason rather than a disabled button.
Insertion is unchanged: both call sites already read the Asset, so they now
insert what it actually carries.

Three new tests drive the whole loop -- upload through the real MOCK
composition, auto-insert, save, validate, preview -- and assert the result is
publishable. The pre-existing loop test no longer needs its hand-edit
workaround.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:07:38 +09:00
DongHyeonkaandClaude Opus 5 f19be639a3 ci: run the TechLog contract drift gate in CI and test:all
`check:tech-log-contract` existed and worked but nothing ran it. No gate in
`config/ci/gates.json` referenced it and `test:all` did not chain it, so a
hand-edit of the vendored canonical yaml or of `generated.ts` passed every
gate the repository actually executes -- the exact regression the digest pin
exists to prevent.

FE-GATE-010 (architecture/contract governance) now owns the command, beside
`check-registries` and `check-ci`, and `test:all` runs it in front of
`test:tech-log`. The canonical authority baseline moves to 83 command
definitions / 95 references and the gate-shape SHA-256 is recomputed with
`canonicalGateShapeSha256`; the recomputation was first verified by
reproducing the previous constant from the previous gates.json.

`tests/features/tech-log/contract-generation.test.ts` now fails if either
wiring is removed again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:03:14 +09:00
DongHyeonka 5c997f3a7e docs: correct the test:visual root-cause attribution in the alignment record
Fix round 1 on Task 12's evidence. The design doc and task-12-report.md
attributed all 18 test:visual failures to Task 9 and described them as
uniformly taller. Independently re-derived from git log/git diff instead of
re-asserting the review's numbers on trust:

- 7 of 18 (both Public tests + 5 Studio preview/snapshot tests) render
  shorter, by 93-94px, caused by 79e9aa8 ("fix: align TechLog article
  content widths") — already on main, an ancestor of this branch's
  merge-base 9e5fbd1. case-body-renderer.tsx/evidence-figure.tsx are
  byte-identical across 9e5fbd1..HEAD; the golden PNGs were last written at
  3a7c5de, before 79e9aa8. Pre-existing at the merge-base, not caused by
  this branch; refresh belongs against 79e9aa8 on main.
- 6 of 18 (Studio editor surfaces) render taller, from this branch's
  sanctioned Asset Picker/upload UI (Task 10) — the only failures this
  branch actually produced.
- Condition-1's citation (check:architecture/check:registries) didn't
  support a Public-render claim; replaced with the actual evidence (empty
  renderer diff) plus the main-inherited visual failure.
- Softened test:coverage's "no new code is under-covered" — risk-coverage
  has zero tech-log entries, so its silence isn't evidence either way;
  stated the global thresholds that are actually cleared instead.

No code changed; no gates re-run.
2026-08-18 08:44:35 +09:00
DongHyeonka 83d47e7185 docs: record TechLog backend alignment completion state
Task 12 of the backend-alignment plan: document the TECH_LOG_STUDIO_SOURCE
switch and the generate/check:tech-log-contract scripts in README.md, and
record the spec's completion status (12/12 completion conditions met, the
two explicitly-out-of-scope items restated, and the gate findings from the
full verification pass) in the design doc's status section. No product code
changed.
2026-08-18 08:25:49 +09:00
DongHyeonkaandClaude Opus 5 3ab04a236d fix: restore focus to a stable anchor, not a removed row, after asset delete
Fix round 1 for the Task 11 asset library review. I1 (Important): a
successful delete removed the row from state, so remove()'s reuse of
closeDetail() queued .focus() on a now-detached button -- a silent
no-op that stranded focus on <body>. Splits closeDetail() (cancel/close,
row still on screen, restores focus to the trigger) from a new
closeDetailAfterRemoval() (post-delete, focuses the page heading, the
one anchor guaranteed to survive any list change) so the two paths
stop sharing a helper that only one of them can safely use.

Also closes four Minors from the same review round:
- route-contract.test.ts's "27-route inventory" test title corrected
  to 28.
- canHardDelete's usageCount clause gets its own isolating assertion
  (every prior case used usageCount: 0, so that clause was never
  independently falsified).
- Dropped a duplicate role="status" announcement on a listAssets
  load failure; the existing role="alert" paragraph is now the sole
  announcement.
- Added success-path tests for archive() and remove() via a new
  recordingGateway() test helper that pins the exact gateway call
  shape (expectedVersion, managementStatus: ARCHIVED, idempotency
  keys), not just the resulting UI text; the delete-success test also
  pins the I1 focus fix so a regression back to the removed trigger
  fails loudly.

See task-11-report.md's "Fix round 1" section for the RED/GREEN
evidence (the pre-fix code reliably crashes the test worker rather
than failing the assertion cleanly -- explained there) and the
correction to this task's original claim about matching
publication-list.tsx's focus pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 07:53:17 +09:00
DongHyeonkaandClaude Opus 5 b11aa94f1c feat: add the TechLog Studio asset library route and screen
Adds TECH_LOG_STUDIO_ASSETS (/studio/assets) as a route reachable but
excluded from primary Studio navigation (navigationLabel/navigationOrder
null), plus the AssetLibrary screen that lists assets, shows usage, and
lets an operator archive or hard-delete one. canHardDelete() is a pure
gate mirroring the server's ASSET_IN_USE rule so the screen never offers
an action the server would refuse.

Adding a 28th route also required updating the route-scoped CI
accessibility-evidence gate (FE-GATE-009 in config/ci/gates.json, plus
its authority-baseline counts and shape digest in
scripts/contracts/ci-gates.ts) and the Vite route-to-chunk map that
scripts/generate-build-manifest.ts depends on, or test:unit and the
production build both fail. See task-11-report.md for the full
breakdown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 07:26:05 +09:00
DongHyeonkaandClaude Opus 5 073fda87eb fix: collapse the evidence-key gate and resolver into one decision
Three fix rounds each rebuilt the gate as a separate expression that merely
agreed with the resolver on the inputs that round's tests used. Different
expressions cannot agree in general, so the defect class stayed open while
each reported instance closed.

`findResolvableAsset(assets, key)` is now the single place that decides which
Asset an evidence key resolves to. Every gate is
`Boolean(findResolvableAsset(...)) || legacyKey(key)` via one shared
composition, and every resolver returns what it returns:

- validate-working-copy: the key gate and the decorative lookup (a last-wins
  Map against the resolver's first-wins find, so alt could be judged against a
  different Asset than the one rendered)
- adapters/mock/project-public-render-model: gate and resolver
- instant-preview: gate and descriptor resolver
- createAssetCatalogResolver: the pixels, a fourth expression nobody had
  listed -- one Asset's caption could sit over another Asset's image

Duplicate assetKeys are a contract violation but reachable through a paged
list, so the choice is total and order-independent: newest updatedAt wins,
tie-broken by id.

InstantPreview's gate is no longer looser than the others. The un-loaded-asset
case it was loosened for blanks either way; all the looseness bought was
catalog-only keys rendering an empty gap with no message while validation said
EVIDENCE_UNSUPPORTED. The test that pinned that divergence now asserts the
consistent behaviour, and the false comment claiming a fix that did not exist
is gone.

idempotent() now maps a deterministic content failure to VALIDATION_STALE/409
instead of offering a retry that fails identically, and no longer caches
uncharacterized internal failures -- reporting one as retryable while freezing
it in the ledger meant the retry could never re-run.

Adds tests/features/tech-log/evidence-key-agreement.test.ts: 144 adversarial
(asset list, key) combinations asserting the agreement itself rather than
examples. It reported 53 disagreements against the previous code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 06:55:06 +09:00
DongHyeonkaandClaude Opus 5 783e9b2cf1 fix: rebuild gate 1 from the resolver's own predicate, not a catalog lookup
Fix round 3 (review of 7ff9728):

Round 2's gate 1 (supportsEvidenceKeyIn(evidenceCatalogEntriesFromAssets(assets)))
still routed through evidenceCatalogEntryFor, matching on id || label ||
publicPath -- gate 2's question, asked over fewer rows, not the resolver's
actual success condition (assetKey === key && managementStatus === "READY"
&& Boolean(publicPath)). Two Asset shapes made the two predicates disagree:
a READY asset with publicPath null/"", and a READY asset whose publicPath
happens to satisfy the legacy /media/${someOtherKey}.svg convention for a
key that isn't its own assetKey. Both passed gate 1 while the resolver
could not produce real pixels, reproducing the validateDocument-VALID /
createPreview-throws-raw-Error disagreement a second time.

Rebuilt gate 1 in validate-working-copy.ts and
adapters/mock/project-public-render-model.ts directly from a new domain
function, supportsEvidenceKeyFromReadyAssets, that mirrors the resolver's
exact condition -- never through evidenceCatalogEntryFor again. Gate 2
keeps reading the merged catalog. Made the mock resolver total (returns a
placeholder instead of throwing, matching instant-preview.tsx's resolver),
removing a comment that asserted an invariant the code did not hold.
Wrapped mock-studio-gateway.ts's idempotent() so any non-StudioGatewayError
that reaches its catch is normalized before crossing the port -- closing
the class generally, not just this instance.

Reverted instant-preview.tsx's own gate 1 to the merged catalog (unlike
the mock adapters, its resolver is provably total, so a loose gate there
only ever degrades to a placeholder) -- round 2's narrowing there was a
separate regression: a CASE referencing a document-catalog-backed key
outside the editor's currently-loaded Asset list blanked the entire
preview instead of degrading one figure.

Pinned both slip-through shapes failing on both mock paths with the
thrown/rejected error's type asserted (StudioGatewayError, never a raw
Error), pinned idempotent()'s new wrapping via a validate/preview race,
pinned EVIDENCE_NOT_FOUND as reachable through validateWorkingCopy
directly, and pinned Instant Preview's graceful degradation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 06:24:46 +09:00
DongHyeonkaandClaude Opus 5 7ff9728a5c fix: un-collapse evidence gate 1 from gate 2 to close a raw-Error escape
Fix round 2 (review of 9864958):

Round 1's I2 fix made gate 1 (supportsEvidenceKeyIn) read the same
merged catalog gate 2 already checks, so gate 1 stopped asking "does a
real Asset (or the legacy key) back this key" and started asking the
identical question gate 2 asks ("is there any EVIDENCE catalog row for
it"). A real EVIDENCE catalog row that exists for something other than
media (fixtures.ts's row for a QUESTION resolution target) then passed
gate 1 with nothing backing it as evidence. validateDocument reported
VALID; createPreview's resolver -- which only ever knew the legacy key
and real Assets -- threw a raw, unwrapped Error, violating the port's
StudioGatewayError-only contract. I2's disagreement reproduced in the
opposite direction.

Rebuilt gate 1 in all three callers (validate-working-copy.ts,
adapters/mock/project-public-render-model.ts, instant-preview.tsx) as
"legacy key OR an Asset-derived catalog entry only" -- never the
merged document catalog -- while gate 2 keeps reading the merged
catalog as before. Also stopped emitting the real Asset UUID as the
synthesized CatalogEntry's id (prefixed instead), closing a path where
pasting an Asset's real id -- never something the Picker itself
produces -- would have resolved as an evidence key.

Restored content-format.test.ts's domain tests to exercise the same
gate-1 composition production callers now use instead of a bare
hand-injected predicate, and added coverage for: a real non-asset
EVIDENCE row still being rejected, an Asset-backed key being accepted
with no document-catalog row at all, the fixture-UUID case failing
both mock paths with no raw Error crossing the gateway port, and the
EVIDENCE_ALT_REQUIRED decorative-Asset pairing that had no test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 05:54:21 +09:00
DongHyeonkaandClaude Opus 5 98649585e6 fix: generate fresh upload idempotency keys and reconcile mock preview/validation on evidence keys
Fix round 1 (review of 54d9bf9):

I1: AssetUploadDialog reused one idempotency key across every upload
attempt in a session, generated once when the dialog opened. Every
terminal state re-enables the file input, so retrying with a different
file after a failure sent two distinct payloads under the same key.
The key is now generated fresh inside submit() on each call, matching
every other mutation call site in the repo, and dropped from the
dialog's public props entirely (it was never in the task's own
"Produces" interface).

I2: the mock's validateDocument gate only recognized the one legacy
hardcoded evidence key, completely disconnected from the Asset system
Instant Preview now consults -- so a directive the Picker or upload
dialog inserted always previewed live and then failed validation with
EVIDENCE_UNSUPPORTED for every other key. Extracted the Asset-to-
CatalogEntry mapping (evidenceCatalogEntriesFromAssets) and the
domain's one EVIDENCE-catalog matching rule (evidenceCatalogEntryFor)
into shared domain modules that both the preview path
(instant-preview.tsx) and the validation path (validate-working-copy.ts,
the mock's own createPreview) now call. Reconciled the underlying MOCK
studioSource gap that caused this: built a mock asset gateway
(mock-studio-asset-gateway.ts) sharing one in-memory Asset store with
the mock document gateway, wired per composition-root instance in
create-tech-log-feature-input.ts, so an Asset the editor actually
loaded is visible to validation too, while a key backed by nothing
still fails both paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 05:31:01 +09:00
DongHyeonkaandClaude Opus 5 54d9bf9120 feat: insert evidence directives from the TechLog asset picker
Adds an Asset Picker and upload dialog to the CASE editor so authors can
insert `:::evidence` directives that reference backend assets, and opens
projectWorkingCopy's two evidence gates so Instant Preview accepts a key
backed by a freshly loaded READY asset instead of only the one hardcoded
legacy key. The editor screen now owns the loaded Asset list so the
Picker, the upload dialog, and Instant Preview all read the same array,
and a freshly uploaded asset appears in the preview without a refetch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 04:32:56 +09:00
DongHyeonkaandClaude Opus 5 e2c1d076f4 fix: overlay legacy evidence labels instead of shadowing the whole descriptor
Review finding I1: the static-key-first resolution order protected the
entire legacy fixture (image and labels), when only the hand-authored labels
ever needed protecting -- ResolvedAsset carries no label fields, so nothing
else could recover them, but a real descriptor's publicPath/width/height were
never actually at risk of mismatching. Narrowed resolveWith so a resolved
descriptor's data fields always win; the legacy registry only overlays
triggerLabel/dialogLabel for keys it recognizes, and only supplies the full
descriptor when nothing else resolves the key at all. A future backend asset
colliding with the legacy key now degrades to a wrong caption, never a wrong
image.

Review finding I2: renamed and rewrote a test whose title claimed a
READY-vs-QUARANTINED same-key guarantee its body never constructed. It now
puts both a QUARANTINED and a READY entry under one assetKey and asserts the
READY one wins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 04:03:43 +09:00
DongHyeonkaandClaude Opus 5 9d91001a31 feat: resolve evidence figures from backend asset descriptors
Public Preview and Publication Snapshot now resolve evidence figures from the
ResolvedAsset descriptor the server already attaches to each EVIDENCE_FIGURE
block, instead of a local literal that only knew one hardcoded key and threw
on anything else. Instant Preview gains an `assets` prop (defaults to `[]`,
unwired until the Asset Picker task) and stops throwing when a key has no
catalog match yet.

Builds on Task 1's existing seam (resolveCaseEvidenceAssets /
ResolveEvidenceAsset) via a new asset-resolvers.ts rather than a parallel
path. Kept out of adapters/ (presentation may not import it) by carrying a
small local copy of the one legacy fixture entry, checked before any
descriptor match so the pre-existing key keeps rendering byte-identically
across surfaces during the migration window. Non-READY assets never resolve;
unresolvable keys return a placeholder instead of throwing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:54:30 +09:00
DongHyeonka 0c071aaabc fix: judge evidence alt text with asset metadata, not syntax
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.
2026-08-18 03:33:19 +09:00
DongHyeonkaandClaude Opus 5 35cc5c868a fix: invalidate the TechLog CSRF token on 403 and harden the bootstrap profile wiring
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>
2026-08-18 03:25:29 +09:00
DongHyeonkaandClaude Opus 5 2cab4974b7 fix: break the TechLog CSRF bootstrap cycle and close the review's fix-round-1 items
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>
2026-08-18 03:07:02 +09:00
DongHyeonkaandClaude Opus 5 c9c832c365 feat: add the TechLog asset multipart upload transport
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>
2026-08-18 02:39:51 +09:00
DongHyeonka d9c2d8bc5e feat: add the TechLog Studio asset gateway port and JSON adapter 2026-08-18 02:16:51 +09:00
DongHyeonka 7724a8720c fix: account for TECH_LOG_STUDIO_SESSION in the auth profile registry pin
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.
2026-08-18 02:05:29 +09:00
DongHyeonka 3b641906b8 feat: select the TechLog Studio adapter from runtime configuration
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.
2026-08-18 01:57:15 +09:00
DongHyeonkaandClaude Opus 5 7424ed4594 fix: forward every documented Studio query filter and pin canonicalInputIdentity
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>
2026-08-18 01:43:51 +09:00
DongHyeonkaandClaude Opus 5 1d01a522de feat: implement the TechLog Studio HTTP gateway
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>
2026-08-18 01:30:24 +09:00
DongHyeonkaandClaude Opus 5 9313018ef5 fix: restore contract-set regression coverage and mirror canonical problem bounds
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>
2026-08-18 01:21:14 +09:00
DongHyeonkaandClaude Opus 5 66c047cec8 feat: register the TechLog Studio contract contribution
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>
2026-08-18 01:10:04 +09:00
DongHyeonkaandClaude Opus 5 a6fc536d8a docs: scope the TechLog contract packageId to satisfy PACKAGE_ID
The platform models contract contributions as published npm packages and
rejects an unscoped packageId at composition time. Use
@tech-log/studio-contract, the name a real publish would carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:05:16 +09:00
DongHyeonka 6c40c291d9 test: cover Studio error mapping's remaining outcome branches and CSRF rejection recovery 2026-08-18 00:51:50 +09:00
DongHyeonka 7381be1477 feat: add TechLog Studio error mapping and CSRF token provider 2026-08-18 00:44:37 +09:00
DongHyeonkaandClaude Opus 5 a0e0be6522 test: cover dashboard needsValidation and detail() nextAction regression risk
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>
2026-08-18 00:38:03 +09:00
DongHyeonkaandClaude Opus 5 639e1a49c9 build: generate the TechLog Studio contract from canonical source
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>
2026-08-18 00:27:59 +09:00
DongHyeonkaandClaude Opus 5 ac555a85e8 docs: generate the TechLog contract outside the repo toolchain
openapi-typescript needs the TypeScript 5 classic compiler API; this repo
pins typescript@7.0.2 (VD-01), whose root export has no compiler API.
Run the generator in an isolated pnpm dlx environment instead, so the
lockfile and peer contract are untouched.

Also make check:tech-log-contract work without the canonical repo or the
generator — it read an absolute path to a sibling repo that CI never has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:03:29 +09:00
DongHyeonkaandClaude Opus 5 9f0599a389 docs: correct the alignment plan against platform transport constraints
Pre-flight scan found five defects that would have failed at composition:

- authProfileId TECH_LOG_STUDIO_SESSION was never registered
- deleteStudioAsset declared responseByteLimit 0 (platform requires >= 1)
- gateways passed CSRF/idempotency through operation input, but contract
  projection has no header channel; both must use the platform seams
- the browser mutation intent factory generates its own key, discarding the
  caller-supplied one the port contract depends on
- instant preview was never wired to the loaded asset list

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:52:47 +09:00
DongHyeonkaandClaude Opus 5 78766accdf docs: plan the TechLog backend alignment implementation
12개 Task로 분해한다. 계약 생성·digest 고정(1), 오류/CSRF(2), 계약 기여(3),
StudioGateway HTTP(4), 런타임 스위치(5), Asset 포트(6), multipart 전송과
배선(7), alt 규칙 이동(8), evidence resolver(9), Picker(10), Library(11),
전체 게이트(12).

자체 검토에서 세 결함을 고쳤다.
- Asset gateway가 feature input에 배선되지 않아 UI가 도달할 수 없었다.
- Backend assetKey를 해석할 resolver가 없어 삽입한 directive가 즉시
  미리보기를 깨뜨렸다. 렌더러의 기존 주입점을 쓰는 Task를 추가했다.
- dialog/library 단계에 코드가 없었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:44:25 +09:00
DongHyeonkaandClaude Opus 5 12329ec9c0 docs: define TechLog backend alignment
Studio 계약을 tech-log-design-package의 canonical studio-v1.yaml 단일
출처로 정합시키고, Asset capability를 편집 흐름에 연결하는 설계를 확정한다.

- 계약 drift는 EXTERNAL_PACKAGE provenance의 digest 고정으로 막는다.
- Studio 전송은 플랫폼 계약 런타임을 통과한다. multipart 업로드 1개만
  전용 seam으로 분리한다 — 런타임이 JSON 본문만 표현할 수 있기 때문이다.
- CSRF는 전송 관심사로 어댑터 내부에 둔다. Studio 인증 UI는 추가하지 않는다.
- Public 조회의 HTTP 전환은 별도 사이클로 분리한다. 동기 포트를 async로
  바꾸는 작업이 19개 파일 31개 호출 지점과 이식 parity 기준선을 흔든다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:24:29 +09:00
DongHyeonkaandClaude Opus 5 9e5fbd1384 refactor: derive TechLog navigation from the route contract
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>
2026-08-17 20:34:36 +09:00
DongHyeonkaandClaude Opus 5 93ce86eef4 test: decouple the feature-switch gates from the template's demo screens
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>
2026-08-17 19:08:42 +09:00
DongHyeonkaandClaude Opus 5 cd1ef5cda2 chore: ignore in-repository git worktrees
.worktrees/ showed as untracked in main's status, so a worktree checkout could
be committed into the repository by an ordinary 'git add -A'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:27 +09:00
DongHyeonkaandClaude Opus 5 7d1ccccbcd Merge branch 'main' into feature/techlog-ui-migration
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>
2026-08-17 18:38:08 +09:00
DongHyeonka 0355b644a0 feat: add TechLog project decision authoring 2026-08-17 17:23:42 +09:00
DongHyeonka 79e9aa8328 fix: align TechLog article content widths 2026-08-17 16:49:20 +09:00
DongHyeonka 72669949bc docs: define TechLog content width alignment 2026-08-17 16:34:45 +09:00
DongHyeonka 47cbf4ddca docs: record TechLog migration evidence 2026-08-16 05:07:58 +09:00
DongHyeonka 3a7c5deca0 fix: complete TechLog migration evidence 2026-08-16 04:55:52 +09:00
DongHyeonka 6c2780b7a7 test: prove TechLog UI migration parity 2026-08-16 02:48:26 +09:00
DongHyeonka c5c8b9423c feat: complete TechLog Studio publication flow 2026-08-16 00:35:11 +09:00
DongHyeonka 9c6906fc6f feat: port TechLog Studio validation workflow 2026-08-15 23:59:08 +09:00
DongHyeonka 5933265975 feat: port TechLog Studio editors 2026-08-15 23:41:34 +09:00
DongHyeonka 887f5e6eb1 feat: port TechLog Studio shell and indexes 2026-08-15 23:29:32 +09:00
DongHyeonka 2b6fa42620 feat: complete TechLog public screens 2026-08-15 23:19:04 +09:00
DongHyeonka 4283e40bb2 feat: port TechLog document screens 2026-08-15 23:06:57 +09:00
DongHyeonka 512aa4a1e9 fix: synchronize TechLog focus and not-found runtime 2026-08-15 22:52:42 +09:00
DongHyeonka ef1d5cc548 feat: port TechLog discovery screens 2026-08-15 22:29:57 +09:00
DongHyeonka c9164c1a03 test: assert TechLog search focus style 2026-08-15 21:54:09 +09:00
DongHyeonka 3bc74e0195 test: strengthen TechLog shell contracts 2026-08-15 21:42:49 +09:00
DongHyeonkaandClaude Opus 5 bdee07a93b chore: sync the frontend template from a0fbafb to 5434760
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>
2026-08-15 21:34:19 +09:00
DongHyeonka 627df884dd feat: port TechLog shells and styles 2026-08-15 21:20:47 +09:00
DongHyeonka 01316763e3 fix: inject grouped route codecs 2026-08-15 20:51:22 +09:00
DongHyeonka adb8613cb9 feat: add grouped TechLog route contracts 2026-08-15 20:30:32 +09:00
DongHyeonka 27dda3e0e3 feat: compose TechLog static and mock adapters 2026-08-15 19:36:46 +09:00
DongHyeonka 708680b28e fix: reject TechLog protocol-relative links 2026-08-15 19:10:05 +09:00
DongHyeonka 8342fb14dc fix: reject unsafe TechLog network paths 2026-08-15 19:00:10 +09:00
DongHyeonka 16753f53af feat: port TechLog content format and renderer 2026-08-15 18:44:09 +09:00
DongHyeonka 82f94423e5 test: enforce exact TechLog contract keys 2026-08-15 18:16:21 +09:00
DongHyeonka 01ed1e9300 feat: add TechLog feature contracts 2026-08-15 18:07:12 +09:00
DongHyeonka 5479101c8c fix: exercise production evidence asset lookup 2026-08-15 17:55:37 +09:00
DongHyeonka 034b702e8e chore: establish TechLog migration baseline 2026-08-15 17:49:40 +09:00
DongHyeonka 05e3d50ba0 chore: prepare isolated migration worktree 2026-08-15 17:35:21 +09:00
DongHyeonka 954ca8a1fd docs: plan TechLog UI migration 2026-08-15 17:09:58 +09:00
DongHyeonka 325a2a0843 docs: define GitFlow delivery for UI migration 2026-08-15 16:46:36 +09:00
DongHyeonka c2d03165b3 docs: define TechLog UI migration design 2026-08-15 16:43:45 +09:00
DongHyeonkaandClaude Opus 5 93db3c184b chore: carry the RPC-02 deadline-race test fix and re-pin the template
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>
2026-08-15 12:33:43 +09:00
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
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>
2026-08-15 12:04:58 +09:00
679 changed files with 91125 additions and 5548 deletions
+35
View File
@@ -156,6 +156,41 @@
"path": "^(src/(presentation|bootstrap)|react|react-dom|@tanstack)"
}
},
{
"name": "contracts-do-not-know-application",
"comment": "§4. `src/contracts` is the lower of the two packages: application reads contracts, never the other way round. Before this rule the shared Result carrier and the compatibility predicate lived in application and were imported back down by contracts, so neither package owned the shared vocabulary and the coupling was invisible to every gate.",
"severity": "error",
"from": {
"path": "^src/contracts"
},
"to": {
"path": "^src/(application|features)"
}
},
{
"name": "generic-presentation-does-not-compose-the-product",
"comment": "§4 / §9. Which features are installed is a product decision that belongs to bootstrap. Generic presentation reads the installed registries directly today; the paths below are the exact set that does so, frozen so the coupling cannot spread while the assembly is lifted into bootstrap.",
"severity": "error",
"from": {
"path": "^src/presentation/",
"pathNot": "^src/presentation/(layouts/app-shell\\.tsx|pages/(not-found-page|home-page)\\.tsx|routes/(route-contract|route-codecs|app-router|navigation-policy)\\.(ts|tsx)|i18n/catalog\\.ts|examples/platform-overview-page\\.tsx)$"
},
"to": {
"path": "^src/features/installed-"
}
},
{
"name": "adapters-do-not-know-other-concrete-adapters",
"comment": "docs/architecture/layers.md §4: a concrete adapter never depends on another concrete adapter. Only the adapter kernel is shared — `src/adapters/platform` (clock, abort primitive, capacity guard) and the browser-data result helpers. `query-cache` still reads two collaborator types from `cross-context-invalidation`; that edge is named here rather than left silent, and closes when those types are lifted to a port.",
"severity": "error",
"from": {
"path": "^src/adapters/([^/]+)/"
},
"to": {
"path": "^src/adapters/([^/]+)/",
"pathNot": "^src/adapters/($1/|platform/|browser-file-storage/result\\.ts$|cross-context-invalidation/index\\.ts$)"
}
},
{
"name": "no-circular-dependencies",
"severity": "error",
+25
View File
@@ -0,0 +1,25 @@
# Build-time inputs (§6.1). These are compiled into the bundle by Vite, so
# everything here is public by definition. Never put a secret in this file or in
# any `.env*` file: a frontend has no confidential storage, and a value that
# reaches the browser has been published.
#
# Runtime configuration — API endpoints, auth mode, telemetry, capability
# switches — is NOT here. It lives in `config/runtime/<profile>.json` and is
# materialized into `dist/config.json` at build time, so it can be changed
# without rebuilding. See docs/architecture/layers.md.
#
# Copy to `.env.local` (git-ignored) to override locally.
# Identifies the build in release manifests and the runtime document.
# CI supplies the real value; a developer build falls back to "local-build".
VITE_BUILD_ID=local-build
# Source revision the bundle was produced from.
VITE_COMMIT_SHA=local
# Sub-path the app is served under. Must start and end with "/".
# Feeds the router, the Service Worker scope and Vite's asset base together.
VITE_ROUTER_BASE_PATH=/
# Where the browser fetches the runtime document from at boot.
VITE_RUNTIME_CONFIG_URL=/config.json
+4
View File
@@ -126,6 +126,9 @@ jobs:
outputs:
dist_sha256: ${{ steps.candidate.outputs.dist_sha256 }}
archive_sha256: ${{ steps.candidate.outputs.archive_sha256 }}
env:
APP_PROFILE: "${{ vars.APP_PROFILE }}"
RELEASE_TARGET: "${{ vars.RELEASE_TARGET }}"
steps:
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
@@ -187,6 +190,7 @@ jobs:
scripts/lib/secret-scan.ts \
scripts/lib/supply-chain.ts \
scripts/lib/validated-json-artifact.ts \
scripts/lib/vite-route-chunks.ts \
src/contracts/release-artifacts.ts \
src/features/installed-contract-contributions.ts \
src/features/installed-feature-contracts.ts \
+11
View File
@@ -6,6 +6,7 @@ dist/
playwright-report/
test-results/
coverage/
.worktrees/
!tests/fixtures/coverage/
!tests/fixtures/coverage/below-threshold.json
artifacts/**/*.json
@@ -18,3 +19,13 @@ artifacts/storybook/
artifacts/tests/storybook/
artifacts/tests/visual/
!artifacts/**/.gitkeep
# Local environment overrides. `.env.example` is the tracked template; every
# other `.env*` file is a developer's own machine and never enters the repo.
.env
.env.*
!.env.example
# Git worktrees created inside the repository. A worktree is a checkout, not
# source: committing one would nest a second working copy inside this one.
.worktrees/
+15
View File
@@ -10,6 +10,11 @@ import { ApplicationProvider } from "../src/presentation/providers/application-p
import { SessionProvider } from "../src/presentation/providers/session-provider.tsx";
import { ThemeProvider } from "../src/presentation/providers/theme-provider.tsx";
import "../src/presentation/styles/theme.css";
import { resolveProductFeatures } from "../src/contracts/product-features.ts";
import {
COMPILED_PRODUCT_FEATURE_IDS,
INSTALLED_PRODUCT_FEATURE_IDS,
} from "../src/features/installed-product-manifest.ts";
const preferences = new Map<string, unknown>();
const application = createApplication({
@@ -45,6 +50,16 @@ const application = createApplication({
routeChunks: {},
}),
},
// Storybook renders components, not a product: every declared feature is
// shown as active so a story is never blank because of a deployment switch.
productFeatures: {
getSnapshot: () =>
resolveProductFeatures(
COMPILED_PRODUCT_FEATURE_IDS,
INSTALLED_PRODUCT_FEATURE_IDS,
),
isActive: () => true,
},
runtimeCapabilities: {
getSnapshot: () =>
Object.freeze(
@@ -0,0 +1,30 @@
# Task 10 report
## Mapping
- Source Studio provider/runtime shell and header → application-input-created, provider-scoped gateway; React Router navigation; native Public link; persisted `pageshow` generation reset.
- Source dashboard → exact workspace heading, totals, workflow sections, row labels, links, loading and error copy.
- Source document list → exact search/filter/list/empty surfaces plus cursor pagination, retry, and abort of obsolete requests.
- Source new-document form → exact type cards/copy, session gateway creation, announcement, and editor redirect.
- Source Studio not-found → in-shell 404 surface; Studio routes remain public with no auth UI.
## TDD evidence
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx` failed both suites at missing Studio presentation imports (exit 1).
- GREEN: the same command passed 2 files / 8 tests.
- Focused regression: both Studio suites plus `tests/features/tech-log/runtime-composition.test.ts` passed 3 files / 10 tests.
## Files
- Added the 12 Task 10 Studio provider/runtime/shell/component/page files under `src/features/tech-log/presentation/studio/`.
- Added `studio-shell-smoke.test.tsx` and `studio-screens-smoke.test.tsx`.
## SHA
- Base: `2b6fa42620136c3edb1506f907ce79c2251d1316`.
- Implementation: the commit containing this report, titled `feat: port TechLog Studio shell and indexes` (final SHA recorded in the Task 10 handoff).
## Deferred
- Task 11 editor screens and Task 12 dirty-leave/save/validation dialogs remain intentionally deferred.
- Broad architecture, type, lint, build, security, and visual gates remain deferred to Task 14 by user direction.
@@ -0,0 +1,33 @@
# Task 11 report
## Mapping
- Source common, Case, Reference, and Question fields → exact target labels, controls, order, loaded values, conditional resolution fields, and CSS classes.
- Source ordered text/rule/option/relation editors → presentation-owned add, remove, reorder, local IDs, limits, and accessibility names.
- Source document editor/status rail → source tabs, keyboard focus, working-copy status, dirty indicator, version/kind rail, and deferred workflow controls.
- Source instant preview → Content Format v1 `projectWorkingCopy` plus the shared `PublicRecordRenderer`; no gateway preview mutation or parser/renderer duplication.
- Source edit page → registered route input adaptation for the document ID.
- Task 10 provider seam → smallest generic provider-owned editor session (`saved`, `draft`, `status`) retained across editor tabs.
## TDD evidence
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-editor-smoke.test.tsx` failed at the exact missing `document-editor-screen.tsx` import before test collection.
- First GREEN: the same focused command passed 1 file / 5 tests.
- Focused regression: editor smoke plus `content-format.test.ts` and `public-render.test.tsx` passed 3 files / 41 tests.
- Scope check: `git diff --check` passed.
## Files
- Added the 12 Task 11 component/page files under `src/features/tech-log/presentation/studio/`.
- Extended `studio-provider.tsx` and `use-studio.ts` only with presentation-owned editor session state.
- Added `tests/features/tech-log/studio-editor-smoke.test.tsx`.
## SHA
- Base: `887f5e6eb1a5ccdf4feab0b27fae1c5823233190`.
- Implementation: the commit containing this report, titled `feat: port TechLog Studio editors` (final SHA recorded in the Task 11 handoff).
## Deferred
- Save/conflict resolution, guarded navigation, validation, server preview, publish, and unpublish workflows remain deferred to Tasks 12/13. The source Save control is present but disabled until Task 12 supplies the workflow.
- Broad app/test types, lint, build, architecture, security, visual, and full-suite gates remain deferred to integrated Task 14 by user direction.
@@ -0,0 +1,33 @@
# Task 12 report
## Mapping
- Source editor save workflow → pending/success announcements, fresh per-command idempotency keys, retry after request failure, and revision-conflict state without replacing the local draft.
- Source guarded Studio links/provider/dialog → all internal Studio anchors use the guarded `<a>` DOM; dirty navigation offers stay, discard, and save-then-navigate with modal focus/trigger restoration and native `beforeunload` protection.
- Source validation report/screen → saved-version validation gates, current/stale freshness copy, error-before-warning issue order, exact JSON-pointer editor anchors, retry/not-found surfaces, and abortable route reads.
- Source Public Preview screen → missing/current/stale/expired states, exact next-action labels, idempotent preview creation, retry/not-found surfaces, and the shared typed `PublicRecordRenderer`.
- Route pages → existing route-input codecs supply the validation/preview document ID; no publish/history/snapshot behavior was pulled forward.
- Task 10/11 seam → provider gained only dirty-navigation/time state, editor gained the save callback, and existing Studio links were switched to the newly available source guarded-link component.
## TDD evidence
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx` exited 1 before collection at the intentionally missing `guarded-studio-link.tsx` and `public-preview-screen.tsx` imports (2 failed files, 0 tests).
- GREEN: the two workflow suites plus `tests/features/tech-log/mock-studio-gateway.test.ts` passed 3 files / 22 tests.
- Focused seam regression: those three files plus the existing Studio shell, screen, and editor suites passed 6 files / 35 tests.
- Scope check: `git diff --check` passed.
## Files
- Added guarded link, unsaved dialog, beforeunload hook, validation report/screen, Public Preview screen, and validation/preview route pages under `src/features/tech-log/presentation/studio/`.
- Extended the Task 10 provider/context and Task 11 editor/status rail; updated existing Studio internal link consumers to use the source guard.
- Added `studio-save-navigation.test.tsx` and `studio-validation-preview.test.tsx`.
## SHA
- Base: `59332659752ee17c095471d06e7f0fc8b00c89b4`.
- Implementation: the commit containing this report, titled `feat: port TechLog Studio validation workflow` (final SHA recorded in the Task 12 handoff).
## Deferred
- Publish, republish, unpublish, publication history, warning acknowledgement, and immutable publication snapshots remain deferred to Task 13.
- Broad browser-security, app/test types, lint, build, architecture, visual, and full-suite gates remain deferred to integrated Task 14 by user direction.
@@ -0,0 +1,47 @@
# Task 13 report: publication flow and atomic install
## Status
`DONE_WITH_CONCERNS`
Base SHA: `9c6906fc6f76115346360d050dd8c211fee1b5a9`
Delivery commit: the commit containing this report, with subject `feat: complete TechLog Studio publication flow`.
## Publication mapping
- Added the source-faithful publish screen, warning acknowledgements, publication history/filter, unpublish dialog, immutable event snapshot preview, and their three route pages.
- Publish blocks invalid or stale validation, requires every warning acknowledgement, creates a fresh idempotency key per command/retry, preserves gateway command ordering, and exposes pending/error/retry states.
- Unpublish preserves the reason/confirmation contract. Historical preview reads the event-owned immutable snapshot and renders it through the shared `PublicRecordRenderer`; a missing/unknown event stays inside Studio.
- Added `tests/features/tech-log/studio-publication-flow.test.tsx` first. The initial red was the exact missing `publication-list.tsx` module/screen; the implemented suite is green at 7 tests.
## Atomic install and removals
- Installed exactly the 27 governed TechLog route definitions, codecs, runtime imports, module identities, message catalogs, schemas, and release-manifest chunk IDs. `PublicShell` and `StudioShell` are the grouped layout elements.
- Added governed Vite chunk naming for the 27 route module identities and changed the performance probe to `TECH_LOG_HOME`.
- Kept the reference contract/adapters and API/schema/invalidation/platform fixture tests, while removing its presentation runtime/pages and page-level tests.
- Removed the four sample presentation pages, starter home/not-found pages, their page-level component/E2E screens, and all four `/examples/*` E2E specs authorized by the brief.
- Added `tests/e2e/tech-log-studio-workflow.spec.ts`; it was deliberately not run and is deferred to Task 14.
- Updated the removal fixture to retain TechLog after reference removal and to exclude tests whose only contract is the removed reference runtime or the canonical (non-reduced) CI authority.
- Split `DocumentEditorController` into a type-only module to remove the editor/status-rail cycle exposed by the installed route graph.
## Verification evidence
- Publication + validation-preview + mock gateway: PASS, 3 files / 23 tests.
- Router + runtime application + retained reference contract: PASS, 3 files / 16 tests.
- Final route contract + navigation policy: PASS, 2 files / 10 tests.
- Registry structure: PASS, 11 registries.
- Release manifest inventory: PASS, exactly 27 derived chunk IDs.
- `git diff --check`: PASS.
- `test:sample-removal` was run once. Its isolated home smoke passed 9/9 and registry/CI reduced-contract checks passed, but its internally broad type/architecture/unit/coverage/build loop failed. Task-owned findings were fixed afterward: ES-target-incompatible `toSorted`, stale `APP_HOME`, direct adapter import, editor/status-rail cycle, reference-dependent fixture residue, canonical-CI-only tests in a reduced fixture, and missing governed build chunk names. Per fast-mode direction, that several-minute broad loop was not rerun; Task 14 must confirm the fixes through its integrated gates.
## Files
- Publication/UI/runtime: `src/features/tech-log/presentation/**`, including the five publication components, three pages, route runtime, and controller boundary.
- Contracts/install: `src/contracts/{routes,route-runtime-contract}.ts`, `src/features/installed-feature-*.{ts,tsx}`, TechLog route contract, platform codecs/runtime, router, layout reference, registry governance, Vite config, release manifest, performance/removal scripts.
- Tests: new publication flow and Studio workflow specs; updated route/router/runtime/reference/navigation expectations; authorized sample/reference presentation test deletions.
## Task 14 deferred concerns
- Run the complete integrated review/gates, including the sample-removal loop with the post-fix code, production build/manifest verification, types, lint, architecture, security, and Playwright workflow.
- Confirm the governed Vite chunk names in the generated production manifest and assess any unrelated environment/timing failures from the broad isolated fixture.
@@ -0,0 +1,310 @@
# Task 14 report: integrated TechLog parity and release verification
## Status
`DONE_WITH_CONCERNS`. The production serving correction, 130-case
source-to-target comparison, full recursive product-tree evidence, direct HTTP
contract, target visuals, focused browser suites, and static gates pass. The
automated Chromium accessibility suite passes 29/29, but the 27 signed human
keyboard/focus/screen-reader records remain `PENDING`; `FE-GATE-009` is not
claimed as passing. The other concern is the repository's pre-existing
restricted-runner `test:all` baseline: 19 provider-environment cases remain
red. The isolation and exact counts below prove that no TechLog test is among
those failures.
The work remains on `feature/techlog-ui-migration`. It was not merged, pushed,
finished with GitFlow, or deleted. The immutable code candidate is
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`
(`fix: complete TechLog migration evidence`). This report and the durable
parity JSON are deliberately recorded afterward in
`docs: record TechLog migration evidence`, so the evidence can name the exact
candidate it verifies.
## Evidence files
Added or replaced product evidence:
- `tests/visual/tech-log.visual.spec.ts` and 129 target-only PNG snapshots. The
suite has 130 cases because canonical and state coverage for the 1440-pixel
`/studio/publications` screen deliberately share the same reviewed image.
- `scripts/lib/tech-log-production-server.ts`, the generated self-contained
`dist/server.mjs` build artifact, its serving contract/generator, and 39-case
direct HTTP regression coverage.
- `tests/e2e/tech-log-public-discovery.spec.ts`,
`tests/e2e/tech-log-accessibility.spec.ts`, and
`tests/e2e/tech-log-responsive.spec.ts`.
- `tests/support/browser/tech-log-fixtures.ts`, the checked Node 24 parity
runner, and durable
`docs/operations/evidence/tech-log-source-parity.json` evidence.
- Focused regressions in `tests/unit/vite-route-chunks.test.ts`,
`tests/unit/design-system-source.test.ts`, the bounded-body reader tests, the
router component suite, and TechLog feature suites.
- Governed registry, dependency, release, CI, and operations evidence in
`config/contracts`, `config/security`, `config/ci`, the generated Gitea
workflow, `README.md`, and
`docs/operations/techlog-ui-migration-baseline.md`.
Removed starter-only evidence:
- `tests/e2e/compact-smoke.spec.ts`,
`tests/e2e/design-system-interactions.spec.ts`, `tests/e2e/i18n.spec.ts`, and
`tests/e2e/theme.spec.ts`.
- `tests/visual/platform.visual.spec.ts` and all five platform visual PNGs.
The retained `app-shell`, registry-wide accessibility, and responsive suites
were rewritten around TechLog. No stale starter browser or visual snapshot is
left referenced.
## Source-to-target visual method and result
The supplied source at `/home/donghyeon/workspace/techlog-studio-frontend` was
never written. It was copied to `/tmp/techlog-source-parity.I0CBK7`; build and
Vinext runtime caches were created only in that temporary copy. The source
production server on `4375` and target `dist/server.mjs` production artifact on
`4174` were opened by one Playwright Chromium instance with two fresh contexts
and the following identical controls:
- device scale factor 1, light color scheme, `ko-KR`, `Asia/Seoul`, reduced
motion, service workers blocked, 1000-pixel viewport height, and full-page
screenshots;
- fixed clock `2026-08-14T01:00:00.000Z`, deterministic in-memory data,
`document.fonts.ready`, matching Pretendard/IBM Plex Mono font-face state,
and zero-duration animation, transition, and caret styles;
- no masks and no tolerance: exact RGBA pixel comparison, normalized recursive
product-subtree tags, ordered child nodes, complete classes, attributes,
text and ARIA relationships, layout diagnostics, response metadata, boot
lifecycle, and console/page/request failure collection.
The only attributes normalized by name are diagnostics-confirmed framework
outputs: React Router `data-discover`; Next Image `data-nimg`, `decoding`, and
`srcset`; and Next SSR `selected` for a controlled select. Generated React IDs
and CSS-module hashes are normalized by value; there is no broad attribute
omission.
The final external comparison command was:
```bash
TZ=Asia/Seoul corepack pnpm verify:tech-log-source-parity
```
Result: **130/130 passed**, 0 failed, `totalDifferentPixels=0`, every recursive
DOM/class/attribute/text/ARIA tree equal, all HTTP metadata equal, and 0
unexplained source/target errors. Source
screenshots were temporary comparison inputs; none was copied into target
snapshots. The no-update target visual run also passed 130/130 with
`maxDiffPixels=0` and `maxDiffPixelRatio=0`.
The durable evidence identifies source-tree digest
`6724c2f898eefc62d2fc0ee695bccc3ae61a69c5153ed43c69f2cf99ee45bca5`,
candidate `3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, build-manifest digest
`3579650faa482f566553c00d8b4a05a05b4f7a1ab93b83e48676289bbcf02984`,
Vite-manifest digest
`cfd583ed7b6c27dce7f47389447608636b6b9df3e28df00c6416674da2c7c46d`,
case-inventory digest
`5689bcdb5d5637205cdeb92b7c57f72d99f0b039818b8f90afdde526bbafe0ac`,
and evidence-payload digest
`f046047beded19ce468be607dcf99c6b74d4e07323457ec7145c56d2f67d2c79`.
The comparison found and corrected actual integration defects rather than
accepting drift: the TechLog Tailwind bootstrap is loaded exactly once in the
same cascade order as source; the starter theme import is removed; CSS-module
class mapping, shell navigation/focus, publication labels and states, router
404 handling, and generated Vite route-chunk lookup now follow source. All five
source/target CSS pairs pass `cmp -s`; their hashes are recorded in the
operations baseline.
Source production returns missing dynamic slugs and an unmatched Public path as
HTTP 404, `text/plain;charset=UTF-8`, with the exact nine-byte body `Not Found`.
The target production boundary now returns that exact shell-free response;
known Public paths and all known Studio paths remain SPA-served, while an
unknown Studio path preserves the source's HTML Studio shell with HTTP 404.
This is an observed source-production contract and satisfies the planned
prohibition on a generic runtime error; it is not a redesign.
## Route, viewport, and state inventory
The 27 canonical contract routes were each compared at 360 and 1440 pixels:
- Public: `/`, `/explore`, `/explore/:kind`, `/cases/:slug`,
`/references/:slug`, `/questions/:slug`, `/topics/:slug`, `/projects`,
`/projects/:slug`, the `records`, `decisions`, and `activity` project views,
`/releases`, `/releases/:version`, `/profile`, `/search`, and `*`.
- Studio: `/studio`, `/studio/documents`, `/studio/documents/new`, the `edit`,
`validation`, `preview`, and `publish` document views,
`/studio/publications`, publication-event preview, and `/studio/*`.
All known Public fixtures were exercised: two cases, two references, two open
questions, three topics, both projects and all three nested views, and release
`0.1.0`. Ten unknown Public dynamic shapes were also compared at both widths.
The 130-case matrix is 54 canonical-route captures, 18 additional known Public
fixture captures, 12 home breakpoints (1180, 1179, 1050, 1024, 980, 900, 820,
768, 767, 420, 390, and 375), 19 Studio states, 20 unknown-Public captures, and
7 interactions. Studio states cover the
dashboard, list/new, Case/Reference/Question/conflict editors, valid/invalid
validation, current/missing/expired previews, ready/blocked publish,
publications/snapshot, missing document/publication, and unknown Studio route.
Interactions cover Public search, Studio mobile menu, immediate preview,
dirty-leave dialog, newly created current preview, unpublish confirmation, and
warning acknowledgement through publish-ready state.
## Browser, responsive, and accessibility outcomes
- Required four-spec Chromium command: **48/48 passed**. It covers Public
discovery, the full Studio workflow, responsive behavior, and accessibility.
- Responsive plus accessibility focused command: **26/26 passed**.
- Direct production HTTP contract: **39/39 passed**, including exact raw Public
404s, the in-shell Studio 404, known SPA routes, and boot documents.
- Exact target visual command: **130/130 passed** in 2.5 minutes with no masks
and zero pixel tolerance.
- Automated Chromium `@a11y`: **29/29 passed**. The 27 human review records are
intentionally pending, so `corepack pnpm review:a11y-manual` exits 1 and
lists missing status, candidate release ID, reviewer/signature/attestation,
reviewed time, M1-M7, and screen-reader evidence for every route.
- Keyboard/focus checks cover Public search dismissal/restoration, Studio mobile
navigation, dirty-leave and unpublish dialogs, labels, heading/landmark order,
and focus-visible behavior. Axe reports no violations in the required route
and state inventory. Overflow assertions pass at the compact and transition
widths, and no unexpected console, page, or request error remains.
Commands:
```bash
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
corepack pnpm exec playwright test tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
corepack pnpm exec playwright test tests/e2e/tech-log-http-contract.spec.ts --project=chromium
corepack pnpm exec playwright test tests/e2e/accessibility.spec.ts --project=chromium --grep @a11y
corepack pnpm exec playwright test tests/visual/tech-log.visual.spec.ts --config=playwright.visual.config.ts --project=chromium
corepack pnpm test:visual
```
## Registry and supply-chain governance
The initial no-baseline registry artifact reported 23 migration-owned breaking
IDs. Each now has owner `tech-log-frontend`, a TechLog contract-version reason,
atomic route/runtime/manifest installation, same-release compatibility, and
rollback to `05e3d50ba01f01c27f257d2e9040c2bc413ea053`:
- Contract: `$contract:{allowedValues,breakingFields,fieldTypes,requiredFields}`.
- Removed route rows: `APP_HOME`, `EXAMPLES_AUTH`, `EXAMPLES_PLATFORM`,
`EXAMPLES_STATES`, `EXAMPLES_UI`, `REFERENCE_RESOURCE_DETAIL`,
`REFERENCE_RESOURCE_FORM`, `REFERENCE_RESOURCE_LIST`, and
`REFERENCE_RESOURCE_STATUS`.
- Runtime removals: the same nine route IDs.
- Runtime change: `NOT_FOUND:moduleId:field-changed`.
The governed update used exactly:
```bash
REGISTRY_BASELINE_OWNER=tech-log-frontend REGISTRY_BASELINE_REASON="Install approved TechLog Public and Studio route contract" node scripts/update-registry-baseline.ts artifacts/quality/registries.json
corepack pnpm check:registries
```
Final result: 11 registries pass, compatibility `none`, no unacknowledged
change. Approved snapshot digest:
`428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b`.
Six direct dependency additions have evidence owner `tech-log-frontend`,
reviewer `frontend-platform-security`, product-specific reason, and atomic
rollback: `@fontsource/ibm-plex-mono@5.3.0`, `pretendard@1.3.9`,
`remark-directive@4.0.0`, `remark-gfm@4.0.1`, `remark-parse@11.0.0`, and
`unified@11.0.5`. The dependency policy recognizes the font packages' OFL-1.1
license. Supply-chain generation covered 641 packages. The pre-existing
dependency baseline was not promoted; the denied promotion was unnecessary for
the regular verification path, which passes with the committed evidence.
## Sample removal and fresh verification
The final staged-candidate command passed:
```bash
corepack pnpm test:sample-removal
```
Result: **PASS (13 checks, no fixture IDs)**. Its internal evidence included
types; reduced architecture (382 modules/1,170 dependencies, 12 graph checks,
9 forbidden fixtures); 11 registry checks; runtime schema 3 files/40 tests;
unit 118/1,250; component 18/124; integration 8/74; recipes 2/17; coverage at
77.40% statements, 73.26% branches, 83.88% functions, and 80.02% lines; risk
coverage 381/381 with 76 thresholds; source evidence 202 files/129 baselines;
artifact/CI checks; router smoke 9/9; and production build.
Fresh completion commands and results:
| Command | Result |
| --- | --- |
| `corepack pnpm exec vitest run tests/features/tech-log` | 21 files, 170 tests passed |
| required four-spec Chromium command above | 48 tests passed |
| `corepack pnpm exec playwright test tests/e2e/tech-log-http-contract.spec.ts --project=chromium` | 39 tests passed |
| Chromium `@a11y` command above | 29 tests passed |
| `corepack pnpm test:visual` | 130 tests passed |
| `corepack pnpm check:types` | app/node/test/recipes/web-worker/service-worker passed |
| `corepack pnpm lint` | passed with 0 warnings |
| `corepack pnpm check:architecture` | 391 modules, 1,212 dependencies, 12 graph checks, 9 forbidden fixtures passed |
| `corepack pnpm check:design-system` | 48 tokens and vendor boundaries passed |
| `corepack pnpm check:i18n` | 194 keys across 4 locales passed |
| `corepack pnpm check:registries` | 11 registries passed; compatibility `none` |
| `corepack pnpm check:browser-security` | injection rejected; Public source maps absent |
| `corepack pnpm build` | 2,351 modules transformed; build and manifest completed |
| `git diff --check` | passed |
The fresh staged-candidate `corepack pnpm test:all` passed runtime schema 3/40,
then its unit phase passed 122 files/1,773 tests and failed 19 tests in only
`ci-artifact-contract`. The failures are the documented provider/cgroup,
RLIMIT/EMFILE, restrictive-umask, `/tmp`, timing, and identity environment
cases; no TechLog test failed. A pre-staging run had also exposed 39
release-inventory `APP_HOME` failures because the new serving files were not
yet visible to `git ls-files`; staging the complete candidate corrected that
test precondition, and all 39 disappeared. Its one aggregate guardian timeout
passed 1/1 in isolation and 21/21 in the fresh staged aggregate.
The earlier exact baseline-isolation command:
```bash
corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts tests/unit/ci-workflow-generation.test.ts tests/unit/http-scenario-evidence.test.ts
```
ran 3 files/529 tests: 510 passed; all 407 CI-workflow and all 14 HTTP-scenario
tests passed, leaving the same 19 environment-only CI-artifact cases. Direct
runs of the aggregate's remaining phases passed: component 18/124, integration
11/82 under the required child-process scope, reference feature 4/13, and
recipes 2/17. This environment-only baseline is also recorded in the
operations baseline.
## Fixes, branch audit, and handoff
Root-cause-driven fixes added regressions for generated Vite manifest chunk
resolution, source-compatible raw 404 responses, palette-source detection, and
abort rejection. Presentation integration corrections preserve source DOM,
ARIA, copy, assets, CSS, workflow state, and focus behavior; no design was
introduced. The branch-wide audit of
`05e3d50ba01f01c27f257d2e9040c2bc413ea053..HEAD` found no migrated
presentation import of adapters, Next.js, Vinext, or Cloudflare. The 27 route
chunks are present in the release manifest and derive from actual Vite output,
not hard-coded generated filenames.
The immutable code candidate
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a` contains the production 404
boundary, parity runner, tests, snapshots, and 27 pending human-review records.
Only after that commit existed was the target rebuilt cleanly and the final
130-case parity, visual, HTTP, accessibility, and static verification rerun.
This report and its JSON are committed separately as
`docs: record TechLog migration evidence`; later human accessibility evidence
must cite the candidate SHA, not the evidence-only commit.
The independent `final-review.md` remains the immutable review input with its
historical `CHANGES_REQUESTED` verdict. This candidate addresses its production
404 issue with a real build artifact and 39 direct HTTP tests; expands the
source matrix from 112 to 130 and makes recursive tree equality part of pass;
and replaces the missing `tsx` invocation with a checked Node runner and
durable provenance. Its accessibility inventory issue is structurally fixed
and automated Chromium coverage is green, but the reviewer-dependent 27 human
records deliberately remain pending. No review verdict was rewritten or
self-approved.
Manual completion requires a human to check out candidate
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, review every route according to
`docs/accessibility/manual-checklist.md`, fill each record's exact candidate
Release ID, reviewer, signature, attestation, reviewed time, M1-M7, and screen
reader result, commit that evidence separately, then rerun
`corepack pnpm review:a11y-manual`. Until then `FE-GATE-009` remains pending.
+102 -26
View File
@@ -4,9 +4,9 @@ Initialized from `clean-architecture-frontend-template` revision
`4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is
recorded in `template.lock.json`.
A React/Vite reference implementation where architecture boundaries,
integration behavior, release coherence, accessibility, performance, and
operations are executable contracts rather than conventions.
A React/Vite TechLog application where architecture boundaries, integration
behavior, release coherence, accessibility, performance, and operations are
executable contracts rather than conventions.
## Start locally
@@ -28,26 +28,84 @@ corepack pnpm dev
Runtime-public settings live in `public/config.json` and are validated before
the product tree mounts. Client secrets are forbidden.
## Included starter experience
## TechLog experience
The default build mounts a domain-neutral application shell with a header,
responsive sidebar, route focus management, session integration status, and a
persistent `system` / `light` / `dark` theme selector.
The default build mounts the source-faithful TechLog Public and Studio
experience. Public routes provide discovery, search, documents, topics,
projects, releases, and profile content. `/studio` provides the session-scoped
mock authoring workflow: create, edit, validate, preview, publish, unpublish,
and immutable publication history.
| Route | Purpose |
| --- | --- |
| `/` | implementation readiness and starter links |
| `/examples/ui` | buttons, fields, cards, alerts, badges, modal, and tokens |
| `/examples/states` | loading, refresh, empty, error, auth, forbidden, and not-found states |
| `/examples/auth` | reactive external-auth integration seam |
| `/examples/reference-resources` | removable, session-required reference feature |
The 27 canonical route definitions are divided into `PUBLIC` and `STUDIO`
nested layouts. Studio authentication remains deliberately deferred; its mock
gateway state lasts for one Studio shell session and resets on a full document
load. The exact route, dependency, stylesheet, asset, and parity inventory is
recorded in
[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md).
`AUTH_MODE=demo` is credential-free and accepted only in local/development
environments. Deployments use `AUTH_MODE=external` and provide the opaque auth
owner described in
[`docs/architecture/starter-experience.md`](docs/architecture/starter-experience.md).
The client route policy is user experience only; server authorization remains
authoritative.
`corepack pnpm build` emits a self-contained `dist/server.mjs` production
boundary. `corepack pnpm preview --host 127.0.0.1 --port 4174` serves known
Public and Studio routes as SPA documents, preserves the in-shell Studio 404,
and returns source-exact raw `404 text/plain` responses for missing Public
content. With the read-only source temp-copy server on `4375`, run:
```bash
TECH_LOG_SOURCE_URL=http://127.0.0.1:4375 \
TECH_LOG_TARGET_URL=http://127.0.0.1:4174 \
corepack pnpm verify:tech-log-source-parity
```
### Studio backend source
`TECH_LOG_STUDIO_SOURCE` (`MOCK` | `HTTP`) selects which `StudioGateway`
adapter the composition root wires up. It defaults to `MOCK` — the
session-scoped in-memory Studio described above — so the existing Studio
workflow and its test suites are unaffected unless the switch is deliberately
turned on. Setting it to `HTTP` wires the HTTP `StudioGateway` instead, which
calls the canonical `@tech-log/studio-contract` operations against
`API_BASE_URL`. With no backend reachable at that URL, Studio still boots and
its shell renders; the specific panels that need the backend show an inline
"failed to load" state rather than a blank screen or an unhandled exception.
The switch is a field on the versioned runtime config document
(`RuntimeConfigV2`), not a build-time flag:
- `config/runtime/{local,development,staging,production}.json` are the
deployment profiles `corepack pnpm build` (via
`scripts/generate-runtime-config.ts`) materializes into `dist/config.json`
for a real build.
- `corepack pnpm dev` does not run that step. Plain `vite` serves
`public/config.json` (and `public/release-manifest.json`) verbatim as dev
fixtures — editing `config/runtime/local.json` alone has no effect on
`pnpm dev`. To exercise `HTTP` mode under `pnpm dev`, set
`TECH_LOG_STUDIO_SOURCE` in `public/config.json` directly. Switching to
`HTTP` also requires `public/release-manifest.json`'s `contractSet` to
declare the `@tech-log/studio-contract` package the build compiled in
(`.generated/frontend-runtime/contract-set.ts` after a build), or boot fails
closed earlier, at contract-set verification (`CONTRACT_SET_PACKAGE_MISSING`)
— itself a graceful, non-blank error screen, just not the one this switch is
usually used to exercise.
### TechLog contract generation
The Studio HTTP contract is vendored from a canonical OpenAPI source, not
hand-written:
- `corepack pnpm generate:tech-log-contract` regenerates
`src/features/tech-log/contracts/studio/studio-api.openapi.yaml`,
`generated.ts`, and `canonical-source.json` from the canonical
`tech-log-design-package` repository (path from `TECH_LOG_DESIGN_PACKAGE`,
default `/home/donghyeon/workspace/tech-log-design-package`). It needs that
repository checked out locally and network access, because type generation
runs in an isolated `pnpm dlx` sandbox (this repo pins TypeScript 7, which
has no classic compiler API for `openapi-typescript` to use). Run it after
the canonical contract changes, then commit the regenerated files.
- `corepack pnpm check:tech-log-contract` is the drift gate: it hashes the
vendored yaml against the recorded digest and confirms every recorded
`operationId` is present in both the yaml and the generated types. It needs
neither the canonical repository nor the network, so it runs in CI and in
this sandbox. Run it any time to confirm the vendored contract has not
drifted from what was last generated.
## Architecture
@@ -61,11 +119,13 @@ contracts own cross-cutting registries
```
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
`docs/architecture/starter-experience.md`. The removable vertical slice is
under `src/features/reference-feature`; its domain, application input, HTTP
adapter, contracts, route runtime, and presentation are installed through the
feature contribution files in `src/features`. The generic starter routes
continue to typecheck, test, and build after that contribution is removed.
`docs/architecture/starter-experience.md`. TechLog is one feature boundary
under `src/features/tech-log`. Its immutable Public catalog and session-scoped
Studio mock gateway are injected through the application feature input;
Public and Studio presentation code shares the typed content renderer without
importing concrete adapters. The retained reference feature remains a
non-product platform contract fixture and can be removed without changing the
TechLog route set.
### Platform capability review
@@ -117,6 +177,9 @@ corepack pnpm verify:release
corepack pnpm check:registries
corepack pnpm drill:runbooks
corepack pnpm check:ci
corepack pnpm exec vitest run tests/features/tech-log
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
corepack pnpm test:visual
```
`check:types`는 source, Node scripts/config와 tests를 분리된 TypeScript
@@ -144,7 +207,20 @@ corepack pnpm exec playwright install --with-deps chromium firefox webkit
Two gates intentionally need external evidence:
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
for all six registered routes.
for all 27 registered routes:
`NOT_FOUND`, `TECH_LOG_CASE`, `TECH_LOG_EXPLORE`, `TECH_LOG_EXPLORE_KIND`,
`TECH_LOG_HOME`, `TECH_LOG_PROFILE`, `TECH_LOG_PROJECT`,
`TECH_LOG_PROJECTS`, `TECH_LOG_PROJECT_ACTIVITY`,
`TECH_LOG_PROJECT_DECISIONS`, `TECH_LOG_PROJECT_RECORDS`,
`TECH_LOG_QUESTION`, `TECH_LOG_REFERENCE`, `TECH_LOG_RELEASE`,
`TECH_LOG_RELEASES`, `TECH_LOG_SEARCH`, `TECH_LOG_STUDIO_DOCUMENTS`,
`TECH_LOG_STUDIO_DOCUMENT_EDIT`, `TECH_LOG_STUDIO_DOCUMENT_NEW`,
`TECH_LOG_STUDIO_DOCUMENT_PREVIEW`, `TECH_LOG_STUDIO_DOCUMENT_PUBLISH`,
`TECH_LOG_STUDIO_DOCUMENT_VALIDATION`, `TECH_LOG_STUDIO_HOME`,
`TECH_LOG_STUDIO_NOT_FOUND`, `TECH_LOG_STUDIO_PUBLICATIONS`,
`TECH_LOG_STUDIO_PUBLICATION_PREVIEW`, `TECH_LOG_TOPIC`.
`verify:documentation` derives that list from the route registry and fails if
this paragraph falls behind it.
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
eligible-sample threshold and 28 days of production data exist.
-18
View File
@@ -1,18 +0,0 @@
# APP_HOME accessibility review
Status: pending-manual-review
Route ID: APP_HOME
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Automated axe, keyboard-focus, and reduced-motion evidence is available; human review pending.
@@ -1,18 +0,0 @@
# EXAMPLES_AUTH accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_AUTH
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review session state announcements and unavailable external-integration behavior.
@@ -1,18 +0,0 @@
# EXAMPLES_PLATFORM accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_PLATFORM
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review the horizontally scrollable registry tables for keyboard reachability of the scroll container, table caption and header association announced per row, capability status badges carrying their meaning in text rather than colour alone, and the release identity region announcing its update through aria-live without interrupting a reader mid row.
@@ -1,18 +0,0 @@
# EXAMPLES_STATES accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_STATES
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review loading, refresh, empty, error, authentication, forbidden, and not-found announcements.
+3 -3
View File
@@ -10,9 +10,9 @@ Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,18 +0,0 @@
# REFERENCE_RESOURCE_STATUS accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_STATUS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
@@ -1,7 +1,7 @@
# REFERENCE_RESOURCE_FORM accessibility review
# TECH_LOG_CASE accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_FORM
Route ID: TECH_LOG_CASE
Release ID:
Reviewer:
Reviewed at:
@@ -15,4 +15,4 @@ M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,7 +1,7 @@
# EXAMPLES_UI accessibility review
# TECH_LOG_EXPLORE accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_UI
Route ID: TECH_LOG_EXPLORE
Release ID:
Reviewer:
Reviewed at:
@@ -15,4 +15,4 @@ M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review form primitives, Menu/Tabs keyboard behavior, Toast announcements, Tooltip supplemental copy, text-field error association and modal focus containment/restoration.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_EXPLORE_KIND accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_EXPLORE_KIND
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,7 +1,7 @@
# REFERENCE_RESOURCE_LIST accessibility review
# TECH_LOG_HOME accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_LIST
Route ID: TECH_LOG_HOME
Release ID:
Reviewer:
Reviewed at:
@@ -10,9 +10,9 @@ Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,7 +1,7 @@
# REFERENCE_RESOURCE_DETAIL accessibility review
# TECH_LOG_PROFILE accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_DETAIL
Route ID: TECH_LOG_PROFILE
Release ID:
Reviewer:
Reviewed at:
@@ -10,9 +10,9 @@ Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECTS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECTS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT_ACTIVITY accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT_ACTIVITY
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT_DECISIONS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT_DECISIONS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT_RECORDS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT_RECORDS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_QUESTION accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_QUESTION
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_REFERENCE accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_REFERENCE
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_RELEASE accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_RELEASE
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_RELEASES accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_RELEASES
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_SEARCH accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_SEARCH
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_ASSETS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_ASSETS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENTS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENTS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_EDIT accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_EDIT
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_NEW accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_NEW
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_PREVIEW accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_PREVIEW
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_PUBLISH accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_PUBLISH
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_VALIDATION accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_VALIDATION
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_HOME accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_HOME
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_NOT_FOUND accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_NOT_FOUND
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_PUBLICATIONS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_PUBLICATIONS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_PUBLICATION_PREVIEW accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_PUBLICATION_PREVIEW
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_TOPIC accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_TOPIC
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
+250 -42
View File
@@ -174,6 +174,11 @@
"script": "test:reference-feature",
"expect": "pass"
},
{
"id": "test-tech-log",
"script": "test:tech-log",
"expect": "pass"
},
{
"id": "test-recipes",
"script": "test:recipes",
@@ -323,6 +328,11 @@
"expectedExitCode": 1,
"expectedDiagnosticId": "duplicates routeId=DUPLICATE"
},
{
"id": "check-tech-log-contract",
"script": "check:tech-log-contract",
"expect": "pass"
},
{
"id": "build",
"script": "build",
@@ -472,6 +482,11 @@
"id": "check-ci",
"script": "check:ci",
"expect": "pass"
},
{
"id": "check-release-admission",
"script": "check:release-admission",
"expect": "pass"
}
],
"artifactSchemas": [
@@ -732,6 +747,12 @@
"id": "sarif-secret-scan",
"kind": "sarif",
"maxBytes": 67108864
},
{
"id": "json-deployment-admission",
"kind": "json",
"maxBytes": 67108864,
"executableSchemaId": "deployment-admission"
}
],
"artifacts": [
@@ -885,6 +906,15 @@
"test-reference-feature"
]
},
{
"id": "artifact-artifacts-tests-tech-log-xml",
"path": "artifacts/tests/tech-log.xml",
"schemaId": "junit",
"production": "command-generated",
"producerCommandIds": [
"test-tech-log"
]
},
{
"id": "artifact-artifacts-tests-optional-recipes-xml",
"path": "artifacts/tests/optional-recipes.xml",
@@ -1006,58 +1036,172 @@
]
},
{
"id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md",
"path": "artifacts/tests/a11y-manual/APP_HOME.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_HOME.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
"path": "artifacts/tests/a11y-manual/EXAMPLES_UI.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
"path": "artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
"path": "artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_CASE.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
"path": "artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-ASSETS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_ASSETS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-report-json",
@@ -1602,6 +1746,21 @@
"producerCommandIds": [
"check-ci"
]
},
{
"id": "artifact-artifacts-release-deployment-admission-json",
"path": "artifacts/release/deployment-admission.json",
"schemaId": "json-deployment-admission",
"production": "command-generated",
"producerCommandIds": [
"check-release-admission"
]
},
{
"id": "artifact-artifacts-quality-gates-FE-GATE-027-txt",
"path": "artifacts/quality/gates/FE-GATE-027.txt",
"schemaId": "text",
"production": "runner-generated"
}
],
"gates": [
@@ -1707,6 +1866,7 @@
"test-integration",
"test-http-scenario-evidence",
"test-reference-feature",
"test-tech-log",
"test-recipes"
],
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-007-txt",
@@ -1716,6 +1876,7 @@
"artifact-artifacts-quality-http-scenario-evidence-json",
"artifact-artifacts-quality-http-scenario-evidence-fixture-json",
"artifact-artifacts-tests-reference-feature-xml",
"artifact-artifacts-tests-tech-log-xml",
"artifact-artifacts-tests-optional-recipes-xml"
],
"retentionClassId": "merge-cycle"
@@ -1757,11 +1918,33 @@
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
"evidenceArtifactIds": [
"artifact-artifacts-tests-a11y-json",
"artifact-artifacts-tests-a11y-manual-APP-HOME-md",
"artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
"artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
"artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
"artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-ASSETS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
"artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
"artifact-artifacts-tests-a11y-manual-report-json"
],
@@ -1788,6 +1971,7 @@
"check-registries-baseline-fixture",
"check-registries-fixture",
"check-routes-fixture",
"check-tech-log-contract",
"check-ci"
],
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
@@ -2049,6 +2233,18 @@
"artifact-artifacts-performance-lab-json"
],
"retentionClassId": "release-coherence"
},
{
"id": "FE-GATE-027",
"name": "release-admission",
"commandIds": [
"check-release-admission"
],
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-027-txt",
"evidenceArtifactIds": [
"artifact-artifacts-release-deployment-admission-json"
],
"retentionClassId": "release-coherence"
}
],
"stages": [
@@ -2083,7 +2279,8 @@
"FE-GATE-014",
"FE-GATE-015",
"FE-GATE-019",
"FE-GATE-026"
"FE-GATE-026",
"FE-GATE-027"
]
},
{
@@ -2236,10 +2433,20 @@
"condition": "release",
"timeoutMinutes": 45,
"gateIds": [
"FE-GATE-015"
"FE-GATE-015",
"FE-GATE-027"
],
"browserGateIds": [],
"environment": [],
"environment": [
{
"name": "APP_PROFILE",
"value": "${{ vars.APP_PROFILE }}"
},
{
"name": "RELEASE_TARGET",
"value": "${{ vars.RELEASE_TARGET }}"
}
],
"steps": [
{
"kind": "checkout"
@@ -2301,6 +2508,7 @@
"scripts/lib/secret-scan.ts",
"scripts/lib/supply-chain.ts",
"scripts/lib/validated-json-artifact.ts",
"scripts/lib/vite-route-chunks.ts",
"src/contracts/release-artifacts.ts",
"src/features/installed-contract-contributions.ts",
"src/features/installed-feature-contracts.ts",
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8",
"owner": "frontend-platform",
"reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
"approvedAt": "2026-08-01T15:15:45.537Z"
"snapshotDigest": "428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b",
"owner": "tech-log-frontend",
"reason": "Install approved TechLog Public and Studio route contract",
"approvedAt": "2026-08-15T16:32:32.042Z"
}
+633 -153
View File
@@ -5,11 +5,12 @@
"registryId": "FE-REG-ROUTE",
"owner": "feature-frontend-routing-release-recovery-runtime",
"source": "src/features/installed-feature-contracts.ts",
"rowCount": 10,
"rowCount": 27,
"contract": {
"requiredFields": [
"routeId",
"path",
"layoutGroup",
"paramsSchema",
"searchSchema",
"access",
@@ -23,6 +24,7 @@
"fieldTypes": {
"routeId": "string",
"path": "string",
"layoutGroup": "string",
"paramsSchema": "string|null",
"searchSchema": "string|null",
"access": "string",
@@ -43,14 +45,29 @@
"public",
"session-required"
],
"layoutGroup": [
"PUBLIC",
"STUDIO"
],
"paramsSchema": [
null,
"NotFoundSplat",
"ReferenceResourceParams"
"ReferenceResourceParams",
"TechLogExploreKindParams",
"TechLogSlugParams",
"TechLogVersionParams",
"TechLogDocumentIdParams",
"TechLogPublicationEventIdParams",
"TechLogStudioSplat"
],
"searchSchema": [
null,
"ReferenceResourceListQuery"
"ReferenceResourceListQuery",
"TechLogHomeSearch",
"TechLogExploreSearch",
"TechLogExploreKindSearch",
"TechLogSearchQuery",
"TechLogCaseStateSearch"
],
"loadingSurface": [
"app-shell",
@@ -88,6 +105,7 @@
"breakingFields": [
"routeId",
"path",
"layoutGroup",
"paramsSchema",
"searchSchema",
"access",
@@ -95,75 +113,11 @@
]
},
"rows": {
"APP_HOME": {
"access": "public",
"chunkId": "route-home",
"errorSurface": "route-boundary",
"loadingSurface": "app-shell",
"navigationLabel": "시작",
"navigationOrder": 10,
"paramsSchema": null,
"path": "/",
"routeId": "APP_HOME",
"searchSchema": null,
"title": "시작"
},
"EXAMPLES_AUTH": {
"access": "public",
"chunkId": "route-examples-auth",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "인증 연동",
"navigationOrder": 40,
"paramsSchema": null,
"path": "/examples/auth",
"routeId": "EXAMPLES_AUTH",
"searchSchema": null,
"title": "인증 연동"
},
"EXAMPLES_PLATFORM": {
"access": "public",
"chunkId": "route-examples-platform",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "플랫폼 구성",
"navigationOrder": 15,
"paramsSchema": null,
"path": "/examples/platform",
"routeId": "EXAMPLES_PLATFORM",
"searchSchema": null,
"title": "플랫폼 구성"
},
"EXAMPLES_STATES": {
"access": "public",
"chunkId": "route-examples-states",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "화면 상태",
"navigationOrder": 30,
"paramsSchema": null,
"path": "/examples/states",
"routeId": "EXAMPLES_STATES",
"searchSchema": null,
"title": "화면 상태"
},
"EXAMPLES_UI": {
"access": "public",
"chunkId": "route-examples-ui",
"errorSurface": "route-boundary",
"loadingSurface": "example-page",
"navigationLabel": "UI 구성요소",
"navigationOrder": 20,
"paramsSchema": null,
"path": "/examples/ui",
"routeId": "EXAMPLES_UI",
"searchSchema": null,
"title": "UI 구성요소"
},
"NOT_FOUND": {
"access": "public",
"chunkId": "route-not-found",
"errorSurface": "not-found",
"layoutGroup": "PUBLIC",
"loadingSurface": "none",
"navigationLabel": null,
"navigationOrder": null,
@@ -171,59 +125,371 @@
"path": "*",
"routeId": "NOT_FOUND",
"searchSchema": null,
"title": "페이지를 찾을 수 없"
"title": "페이지를 찾을 수 없습니다."
},
"REFERENCE_RESOURCE_DETAIL": {
"access": "session-required",
"chunkId": "route-reference-resource-detail",
"TECH_LOG_CASE": {
"access": "public",
"chunkId": "route-tech-log-case",
"errorSurface": "feature-boundary",
"loadingSurface": "reference-resource-detail",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "ReferenceResourceParams",
"path": "/examples/reference-resources/:resourceId",
"routeId": "REFERENCE_RESOURCE_DETAIL",
"searchSchema": null,
"title": "Reference detail"
"paramsSchema": "TechLogSlugParams",
"path": "/cases/:slug",
"routeId": "TECH_LOG_CASE",
"searchSchema": "TechLogCaseStateSearch",
"title": "Case"
},
"REFERENCE_RESOURCE_FORM": {
"access": "session-required",
"chunkId": "route-reference-resource-form",
"TECH_LOG_EXPLORE": {
"access": "public",
"chunkId": "route-tech-log-explore",
"errorSurface": "feature-boundary",
"loadingSurface": "reference-resource-form",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": "탐색",
"navigationOrder": 10,
"paramsSchema": null,
"path": "/explore",
"routeId": "TECH_LOG_EXPLORE",
"searchSchema": "TechLogExploreSearch",
"title": "탐색"
},
"TECH_LOG_EXPLORE_KIND": {
"access": "public",
"chunkId": "route-tech-log-explore-kind",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogExploreKindParams",
"path": "/explore/:kind",
"routeId": "TECH_LOG_EXPLORE_KIND",
"searchSchema": "TechLogExploreKindSearch",
"title": "유형별 탐색"
},
"TECH_LOG_HOME": {
"access": "public",
"chunkId": "route-tech-log-home",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": null,
"path": "/examples/reference-resources/new",
"routeId": "REFERENCE_RESOURCE_FORM",
"searchSchema": null,
"title": "Reference form"
"path": "/",
"routeId": "TECH_LOG_HOME",
"searchSchema": "TechLogHomeSearch",
"title": "TechLog"
},
"REFERENCE_RESOURCE_LIST": {
"access": "session-required",
"chunkId": "route-reference-resources",
"TECH_LOG_PROFILE": {
"access": "public",
"chunkId": "route-tech-log-profile",
"errorSurface": "feature-boundary",
"loadingSurface": "reference-resource-list",
"navigationLabel": "Reference feature",
"navigationOrder": 50,
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": "프로필",
"navigationOrder": 40,
"paramsSchema": null,
"path": "/examples/reference-resources",
"routeId": "REFERENCE_RESOURCE_LIST",
"searchSchema": "ReferenceResourceListQuery",
"title": "Reference feature"
"path": "/profile",
"routeId": "TECH_LOG_PROFILE",
"searchSchema": null,
"title": "프로필"
},
"REFERENCE_RESOURCE_STATUS": {
"access": "session-required",
"chunkId": "route-reference-resource-status",
"TECH_LOG_PROJECT": {
"access": "public",
"chunkId": "route-tech-log-project",
"errorSurface": "feature-boundary",
"loadingSurface": "reference-resource-status",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogSlugParams",
"path": "/projects/:slug",
"routeId": "TECH_LOG_PROJECT",
"searchSchema": null,
"title": "프로젝트"
},
"TECH_LOG_PROJECT_ACTIVITY": {
"access": "public",
"chunkId": "route-tech-log-project-activity",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogSlugParams",
"path": "/projects/:slug/activity",
"routeId": "TECH_LOG_PROJECT_ACTIVITY",
"searchSchema": null,
"title": "프로젝트 활동"
},
"TECH_LOG_PROJECT_DECISIONS": {
"access": "public",
"chunkId": "route-tech-log-project-decisions",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogSlugParams",
"path": "/projects/:slug/decisions",
"routeId": "TECH_LOG_PROJECT_DECISIONS",
"searchSchema": null,
"title": "프로젝트 결정"
},
"TECH_LOG_PROJECT_RECORDS": {
"access": "public",
"chunkId": "route-tech-log-project-records",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogSlugParams",
"path": "/projects/:slug/records",
"routeId": "TECH_LOG_PROJECT_RECORDS",
"searchSchema": null,
"title": "프로젝트 기록"
},
"TECH_LOG_PROJECTS": {
"access": "public",
"chunkId": "route-tech-log-projects",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": "프로젝트",
"navigationOrder": 20,
"paramsSchema": null,
"path": "/projects",
"routeId": "TECH_LOG_PROJECTS",
"searchSchema": null,
"title": "프로젝트"
},
"TECH_LOG_QUESTION": {
"access": "public",
"chunkId": "route-tech-log-question",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogSlugParams",
"path": "/questions/:slug",
"routeId": "TECH_LOG_QUESTION",
"searchSchema": null,
"title": "Open Question"
},
"TECH_LOG_REFERENCE": {
"access": "public",
"chunkId": "route-tech-log-reference",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogSlugParams",
"path": "/references/:slug",
"routeId": "TECH_LOG_REFERENCE",
"searchSchema": null,
"title": "Reference"
},
"TECH_LOG_RELEASE": {
"access": "public",
"chunkId": "route-tech-log-release",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogVersionParams",
"path": "/releases/:version",
"routeId": "TECH_LOG_RELEASE",
"searchSchema": null,
"title": "변경 기록"
},
"TECH_LOG_RELEASES": {
"access": "public",
"chunkId": "route-tech-log-releases",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": "변경 기록",
"navigationOrder": 30,
"paramsSchema": null,
"path": "/releases",
"routeId": "TECH_LOG_RELEASES",
"searchSchema": null,
"title": "변경 기록"
},
"TECH_LOG_SEARCH": {
"access": "public",
"chunkId": "route-tech-log-search",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": null,
"path": "/examples/reference-resources/status",
"routeId": "REFERENCE_RESOURCE_STATUS",
"path": "/search",
"routeId": "TECH_LOG_SEARCH",
"searchSchema": "TechLogSearchQuery",
"title": "검색"
},
"TECH_LOG_STUDIO_DOCUMENT_EDIT": {
"access": "public",
"chunkId": "route-tech-log-studio-document-edit",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogDocumentIdParams",
"path": "/studio/documents/:id/edit",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT",
"searchSchema": null,
"title": "Reference status"
"title": "문서 편집"
},
"TECH_LOG_STUDIO_DOCUMENT_NEW": {
"access": "public",
"chunkId": "route-tech-log-studio-document-new",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": "새 문서",
"navigationOrder": 30,
"paramsSchema": null,
"path": "/studio/documents/new",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW",
"searchSchema": null,
"title": "새 문서"
},
"TECH_LOG_STUDIO_DOCUMENT_PREVIEW": {
"access": "public",
"chunkId": "route-tech-log-studio-document-preview",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogDocumentIdParams",
"path": "/studio/documents/:id/preview",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW",
"searchSchema": null,
"title": "Public Preview"
},
"TECH_LOG_STUDIO_DOCUMENT_PUBLISH": {
"access": "public",
"chunkId": "route-tech-log-studio-document-publish",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogDocumentIdParams",
"path": "/studio/documents/:id/publish",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH",
"searchSchema": null,
"title": "게시"
},
"TECH_LOG_STUDIO_DOCUMENT_VALIDATION": {
"access": "public",
"chunkId": "route-tech-log-studio-document-validation",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogDocumentIdParams",
"path": "/studio/documents/:id/validation",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION",
"searchSchema": null,
"title": "문서 검증"
},
"TECH_LOG_STUDIO_DOCUMENTS": {
"access": "public",
"chunkId": "route-tech-log-studio-documents",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": "작업본",
"navigationOrder": 10,
"paramsSchema": null,
"path": "/studio/documents",
"routeId": "TECH_LOG_STUDIO_DOCUMENTS",
"searchSchema": null,
"title": "작업본"
},
"TECH_LOG_STUDIO_HOME": {
"access": "public",
"chunkId": "route-tech-log-studio-home",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": null,
"path": "/studio",
"routeId": "TECH_LOG_STUDIO_HOME",
"searchSchema": null,
"title": "TechLog Studio"
},
"TECH_LOG_STUDIO_NOT_FOUND": {
"access": "public",
"chunkId": "route-tech-log-studio-not-found",
"errorSurface": "not-found",
"layoutGroup": "STUDIO",
"loadingSurface": "none",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogStudioSplat",
"path": "/studio/*",
"routeId": "TECH_LOG_STUDIO_NOT_FOUND",
"searchSchema": null,
"title": "Studio 화면을 찾을 수 없습니다"
},
"TECH_LOG_STUDIO_PUBLICATION_PREVIEW": {
"access": "public",
"chunkId": "route-tech-log-studio-publication-preview",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogPublicationEventIdParams",
"path": "/studio/publications/:publicationEventId/preview",
"routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW",
"searchSchema": null,
"title": "게시 Snapshot"
},
"TECH_LOG_STUDIO_PUBLICATIONS": {
"access": "public",
"chunkId": "route-tech-log-studio-publications",
"errorSurface": "feature-boundary",
"layoutGroup": "STUDIO",
"loadingSurface": "app-shell",
"navigationLabel": "게시 기록",
"navigationOrder": 20,
"paramsSchema": null,
"path": "/studio/publications",
"routeId": "TECH_LOG_STUDIO_PUBLICATIONS",
"searchSchema": null,
"title": "게시 기록"
},
"TECH_LOG_TOPIC": {
"access": "public",
"chunkId": "route-tech-log-topic",
"errorSurface": "feature-boundary",
"layoutGroup": "PUBLIC",
"loadingSurface": "app-shell",
"navigationLabel": null,
"navigationOrder": null,
"paramsSchema": "TechLogSlugParams",
"path": "/topics/:slug",
"routeId": "TECH_LOG_TOPIC",
"searchSchema": null,
"title": "Topic"
}
}
},
@@ -231,7 +497,7 @@
"registryId": "FE-REG-ROUTE-RUNTIME",
"owner": "feature-frontend-routing-release-recovery-runtime",
"source": "src/features/installed-feature-contracts.ts",
"rowCount": 10,
"rowCount": 27,
"contract": {
"requiredFields": [
"routeId",
@@ -276,64 +542,166 @@
]
},
"rows": {
"APP_HOME": {
"moduleId": "home-page",
"paramsCodec": "none",
"routeId": "APP_HOME",
"searchCodec": "none"
},
"EXAMPLES_AUTH": {
"moduleId": "auth-example-page",
"paramsCodec": "none",
"routeId": "EXAMPLES_AUTH",
"searchCodec": "none"
},
"EXAMPLES_PLATFORM": {
"moduleId": "platform-overview-page",
"paramsCodec": "none",
"routeId": "EXAMPLES_PLATFORM",
"searchCodec": "none"
},
"EXAMPLES_STATES": {
"moduleId": "state-gallery-page",
"paramsCodec": "none",
"routeId": "EXAMPLES_STATES",
"searchCodec": "none"
},
"EXAMPLES_UI": {
"moduleId": "ui-gallery-page",
"paramsCodec": "none",
"routeId": "EXAMPLES_UI",
"searchCodec": "none"
},
"NOT_FOUND": {
"moduleId": "not-found-page",
"moduleId": "route-not-found",
"paramsCodec": "NotFoundSplat",
"routeId": "NOT_FOUND",
"searchCodec": "none"
},
"REFERENCE_RESOURCE_DETAIL": {
"moduleId": "reference-resource-detail-page",
"paramsCodec": "ReferenceResourceParams",
"routeId": "REFERENCE_RESOURCE_DETAIL",
"TECH_LOG_CASE": {
"moduleId": "route-tech-log-case",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_CASE",
"searchCodec": "TechLogCaseStateSearch"
},
"TECH_LOG_EXPLORE": {
"moduleId": "route-tech-log-explore",
"paramsCodec": "none",
"routeId": "TECH_LOG_EXPLORE",
"searchCodec": "TechLogExploreSearch"
},
"TECH_LOG_EXPLORE_KIND": {
"moduleId": "route-tech-log-explore-kind",
"paramsCodec": "TechLogExploreKindParams",
"routeId": "TECH_LOG_EXPLORE_KIND",
"searchCodec": "TechLogExploreKindSearch"
},
"TECH_LOG_HOME": {
"moduleId": "route-tech-log-home",
"paramsCodec": "none",
"routeId": "TECH_LOG_HOME",
"searchCodec": "TechLogHomeSearch"
},
"TECH_LOG_PROFILE": {
"moduleId": "route-tech-log-profile",
"paramsCodec": "none",
"routeId": "TECH_LOG_PROFILE",
"searchCodec": "none"
},
"REFERENCE_RESOURCE_FORM": {
"moduleId": "reference-resource-form-page",
"paramsCodec": "none",
"routeId": "REFERENCE_RESOURCE_FORM",
"TECH_LOG_PROJECT": {
"moduleId": "route-tech-log-project",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_PROJECT",
"searchCodec": "none"
},
"REFERENCE_RESOURCE_LIST": {
"moduleId": "reference-resource-page",
"paramsCodec": "none",
"routeId": "REFERENCE_RESOURCE_LIST",
"searchCodec": "ReferenceResourceListQuery"
"TECH_LOG_PROJECT_ACTIVITY": {
"moduleId": "route-tech-log-project-activity",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_PROJECT_ACTIVITY",
"searchCodec": "none"
},
"REFERENCE_RESOURCE_STATUS": {
"moduleId": "reference-resource-status-page",
"TECH_LOG_PROJECT_DECISIONS": {
"moduleId": "route-tech-log-project-decisions",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_PROJECT_DECISIONS",
"searchCodec": "none"
},
"TECH_LOG_PROJECT_RECORDS": {
"moduleId": "route-tech-log-project-records",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_PROJECT_RECORDS",
"searchCodec": "none"
},
"TECH_LOG_PROJECTS": {
"moduleId": "route-tech-log-projects",
"paramsCodec": "none",
"routeId": "REFERENCE_RESOURCE_STATUS",
"routeId": "TECH_LOG_PROJECTS",
"searchCodec": "none"
},
"TECH_LOG_QUESTION": {
"moduleId": "route-tech-log-question",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_QUESTION",
"searchCodec": "none"
},
"TECH_LOG_REFERENCE": {
"moduleId": "route-tech-log-reference",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_REFERENCE",
"searchCodec": "none"
},
"TECH_LOG_RELEASE": {
"moduleId": "route-tech-log-release",
"paramsCodec": "TechLogVersionParams",
"routeId": "TECH_LOG_RELEASE",
"searchCodec": "none"
},
"TECH_LOG_RELEASES": {
"moduleId": "route-tech-log-releases",
"paramsCodec": "none",
"routeId": "TECH_LOG_RELEASES",
"searchCodec": "none"
},
"TECH_LOG_SEARCH": {
"moduleId": "route-tech-log-search",
"paramsCodec": "none",
"routeId": "TECH_LOG_SEARCH",
"searchCodec": "TechLogSearchQuery"
},
"TECH_LOG_STUDIO_DOCUMENT_EDIT": {
"moduleId": "route-tech-log-studio-document-edit",
"paramsCodec": "TechLogDocumentIdParams",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_DOCUMENT_NEW": {
"moduleId": "route-tech-log-studio-document-new",
"paramsCodec": "none",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_DOCUMENT_PREVIEW": {
"moduleId": "route-tech-log-studio-document-preview",
"paramsCodec": "TechLogDocumentIdParams",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_DOCUMENT_PUBLISH": {
"moduleId": "route-tech-log-studio-document-publish",
"paramsCodec": "TechLogDocumentIdParams",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_DOCUMENT_VALIDATION": {
"moduleId": "route-tech-log-studio-document-validation",
"paramsCodec": "TechLogDocumentIdParams",
"routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_DOCUMENTS": {
"moduleId": "route-tech-log-studio-documents",
"paramsCodec": "none",
"routeId": "TECH_LOG_STUDIO_DOCUMENTS",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_HOME": {
"moduleId": "route-tech-log-studio-home",
"paramsCodec": "none",
"routeId": "TECH_LOG_STUDIO_HOME",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_NOT_FOUND": {
"moduleId": "route-tech-log-studio-not-found",
"paramsCodec": "TechLogStudioSplat",
"routeId": "TECH_LOG_STUDIO_NOT_FOUND",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_PUBLICATION_PREVIEW": {
"moduleId": "route-tech-log-studio-publication-preview",
"paramsCodec": "TechLogPublicationEventIdParams",
"routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW",
"searchCodec": "none"
},
"TECH_LOG_STUDIO_PUBLICATIONS": {
"moduleId": "route-tech-log-studio-publications",
"paramsCodec": "none",
"routeId": "TECH_LOG_STUDIO_PUBLICATIONS",
"searchCodec": "none"
},
"TECH_LOG_TOPIC": {
"moduleId": "route-tech-log-topic",
"paramsCodec": "TechLogSlugParams",
"routeId": "TECH_LOG_TOPIC",
"searchCodec": "none"
}
}
@@ -530,7 +898,7 @@
"registryId": "FE-REG-SCHEMA",
"owner": "feature-frontend-contract-schema-registry",
"source": "src/features/installed-feature-contracts.ts",
"rowCount": 8,
"rowCount": 19,
"contract": {
"requiredFields": [
"schemaId",
@@ -633,6 +1001,105 @@
"schemaId": "ReferenceResourcePayload",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
},
"TechLogCaseStateSearch": {
"boundary": "route-search",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogCaseStateSearch",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
},
"TechLogDocumentIdParams": {
"boundary": "route-params",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogDocumentIdParams",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"TechLogExploreKindParams": {
"boundary": "route-params",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogExploreKindParams",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"TechLogExploreKindSearch": {
"boundary": "route-search",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogExploreKindSearch",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
},
"TechLogExploreSearch": {
"boundary": "route-search",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogExploreSearch",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
},
"TechLogHomeSearch": {
"boundary": "route-search",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogHomeSearch",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
},
"TechLogPublicationEventIdParams": {
"boundary": "route-params",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogPublicationEventIdParams",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"TechLogSearchQuery": {
"boundary": "route-search",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogSearchQuery",
"schemaVersion": 1,
"unknownFieldPolicy": "STRIP_UNKNOWN"
},
"TechLogSlugParams": {
"boundary": "route-params",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogSlugParams",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"TechLogStudioSplat": {
"boundary": "route-params",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogStudioSplat",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
},
"TechLogVersionParams": {
"boundary": "route-params",
"direction": "REQUEST",
"owner": "feature-tech-log",
"runtime": "zod",
"schemaId": "TechLogVersionParams",
"schemaVersion": 1,
"unknownFieldPolicy": "REJECT_UNKNOWN"
}
}
},
@@ -766,7 +1233,7 @@
"registryId": "FE-REG-STORAGE",
"owner": "feature-frontend-storage-registry-contract",
"source": "src/contracts/storage-keys.ts",
"rowCount": 4,
"rowCount": 5,
"contract": {
"requiredFields": [
"logicalName",
@@ -847,6 +1314,19 @@
"ttl": null,
"valueCodec": "none"
},
"CACHE_INVALIDATION_PULSE": {
"backend": "localStorage",
"classification": "opaque-cache",
"logicalName": "CACHE_INVALIDATION_PULSE",
"migration": "discard",
"name": "pulse",
"physicalKey": "ca-frontend:cache-invalidation:v1:pulse",
"quotaFallback": "no-persist",
"schemaVersion": 1,
"scope": "cache-invalidation",
"ttl": null,
"valueCodec": "opaque-string-v1"
},
"CHUNK_RELOAD_GUARD": {
"backend": "sessionStorage",
"classification": "opaque-cache",
@@ -1,6 +1,190 @@
{
"schemaVersion": 1,
"changes": [
{
"changeId": "FE-REG-ROUTE:$contract:allowedValues:contract-field-changed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:$contract:breakingFields:contract-field-changed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:$contract:fieldTypes:contract-field-changed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:$contract:requiredFields:contract-field-changed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:APP_HOME:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:EXAMPLES_AUTH:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:EXAMPLES_PLATFORM:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:EXAMPLES_STATES:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:EXAMPLES_UI:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_DETAIL:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_FORM:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_LIST:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_STATUS:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:APP_HOME:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_AUTH:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_PLATFORM:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_STATES:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_UI:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:NOT_FOUND:moduleId:field-changed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_DETAIL:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_FORM:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_LIST:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_STATUS:*:removed",
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
"owner": "tech-log-frontend"
},
{
"changeId": "FE-REG-ENV:API_CONTRACT_VERSION:*:removed",
"versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.",
+25 -3
View File
@@ -15,6 +15,7 @@
"requiredFields": [
"routeId",
"path",
"layoutGroup",
"paramsSchema",
"searchSchema",
"access",
@@ -28,6 +29,7 @@
"fieldTypes": {
"routeId": "string",
"path": "string",
"layoutGroup": "string",
"paramsSchema": "string|null",
"searchSchema": "string|null",
"access": "string",
@@ -41,8 +43,27 @@
"uniqueFields": ["routeId", "path", "chunkId"],
"allowedValues": {
"access": ["public", "session-required"],
"paramsSchema": [null, "NotFoundSplat", "ReferenceResourceParams"],
"searchSchema": [null, "ReferenceResourceListQuery"],
"layoutGroup": ["PUBLIC", "STUDIO"],
"paramsSchema": [
null,
"NotFoundSplat",
"ReferenceResourceParams",
"TechLogExploreKindParams",
"TechLogSlugParams",
"TechLogVersionParams",
"TechLogDocumentIdParams",
"TechLogPublicationEventIdParams",
"TechLogStudioSplat"
],
"searchSchema": [
null,
"ReferenceResourceListQuery",
"TechLogHomeSearch",
"TechLogExploreSearch",
"TechLogExploreKindSearch",
"TechLogSearchQuery",
"TechLogCaseStateSearch"
],
"loadingSurface": [
"app-shell",
"example-page",
@@ -84,6 +105,7 @@
"breakingFields": [
"routeId",
"path",
"layoutGroup",
"paramsSchema",
"searchSchema",
"access",
@@ -234,7 +256,7 @@
"consumerIdentityField": "schemaId",
"consumerDirectories": [
"src/presentation/routes",
"src/features/reference-feature/presentation",
"src/features/tech-log/presentation",
"src/features/reference-feature/contracts"
],
"breakingFields": ["schemaId", "boundary", "runtime"]
@@ -195,7 +195,7 @@
"lifecycleMethods": ["release-file-ref", "release-or-dispose-preview-leases", "cancel-via-AbortSignal", "reconcile-or-explicitly-abort-upload", "close-checkpoint-store", "dispose-capability-and-image-runtime"],
"owner": "project-owner-required",
"securityPrivacy": ["Treat file name, extension, MIME and lastModified as untrusted metadata.", "Resolve only exact composition-issued file and image policy object identities; callers cannot raise byte, candidate, pixel, quality, format, lifetime or origin ceilings.", "Use opaque file references and verification receipts bound to an inspected immutable file snapshot and the exact registered profile; reject replay through another profile even when an inspection rule ID matches.", "Treat presigned URLs as bearer capabilities; bind exact method, resource or upload part, offset, length, media type, checksum, origin, path, query, headers and expiry in an in-memory identity vault.", "Use credentials omit, redirect error, no-referrer and no-store for direct data-plane fetch; never persist or observe URL, query, signed header, capability, file name or raw backend message, and never emit digest, raw ETag or receipt values to diagnostics or telemetry.", "A strict account-partitioned upload checkpoint may persist only the protocol-defined SHA-256 file fingerprint, per-part checksum and bounded opaque non-authorizing part receipt token required for server reconciliation; no bearer token or raw signed capability is allowed.", "Persist only strict non-authorizing upload checkpoints and reconcile them with server-authoritative status and re-hashed local parts before completion.", "Require a synchronous server-issued browser-managed download capability whose receipt exactly equals the caller's branded capability receipt and whose resource, media type, safe extension, maximum bytes, optional digest and expiry all match before handoff.", "Expose File, OPFS, Cache and transfer byte streams only as chunk-level closed Results; stop after the first failure, cancel native readers and never throw a raw native exception across the port.", "Accept Image CDN assets only through immutable allowlisted or signature-verified descriptors and registered preset identities; reject active formats, arbitrary transforms, pixel/decode-budget overflow and unsafe cache policy.", "Upload completion remains QUARANTINED until backend scan and promotion; client capability checks are not an authorization boundary.", "Active content preview requires isolation or download-only treatment."],
"bundleBudgetGzipBytes": 52000,
"bundleBudgetGzipBytes": 54600,
"fallback": "Accessible native file input, same-origin authorized server upload/download and a single bounded server-selected image rendition; generated artifacts above the buffer budget move to server-side generation.",
"removal": ["Stop new capability and upload-session issuance, then cancel active reads and transfers.", "Reconcile or explicitly abort active multipart sessions and let backend TTL cleanup remove ambiguous orphans.", "Remove non-secret checkpoints according to account and retention policy.", "Release file references, revoke preview object-URL leases and dispose file, capability and image runtimes.", "Remove transfer/image feature facades and composition, then prove browser-transfer sources are absent from the production module inventory."],
"serverStatePolicy": "query-cache-metadata-only"
+20
View File
@@ -0,0 +1,20 @@
{
"APP_ENV": "development",
"API_BASE_URL": "https://api.dev.example.com/",
"REQUEST_TIMEOUT_MS": 15000,
"MAX_RETRY_ATTEMPTS": 2,
"TELEMETRY_ENABLED": false,
"AUTH_MODE": "external",
"CONFIG_SCHEMA_VERSION": "2.0",
"RELEASE_MANIFEST_URL": "/release-manifest.json",
"CAPABILITY_OVERRIDES": {
"REALTIME": "DEFAULT",
"WEB_WORKER": "DEFAULT",
"SERVICE_WORKER": "DEFAULT",
"OFFLINE_COMMANDS": "DEFAULT"
},
"TECH_LOG_STUDIO_SOURCE": "MOCK",
"FEATURE_OVERRIDES": {
"reference-feature": "DEFAULT"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"APP_ENV": "local",
"API_BASE_URL": "http://localhost:8080/",
"REQUEST_TIMEOUT_MS": 10000,
"MAX_RETRY_ATTEMPTS": 2,
"TELEMETRY_ENABLED": false,
"AUTH_MODE": "demo",
"CONFIG_SCHEMA_VERSION": "2.0",
"RELEASE_MANIFEST_URL": "/release-manifest.json",
"CAPABILITY_OVERRIDES": {
"REALTIME": "DEFAULT",
"WEB_WORKER": "DEFAULT",
"SERVICE_WORKER": "DEFAULT",
"OFFLINE_COMMANDS": "DEFAULT"
},
"TECH_LOG_STUDIO_SOURCE": "MOCK",
"FEATURE_OVERRIDES": {
"reference-feature": "DEFAULT"
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"APP_ENV": "production",
"API_BASE_URL": "https://api.example.com/",
"REQUEST_TIMEOUT_MS": 10000,
"MAX_RETRY_ATTEMPTS": 2,
"TELEMETRY_ENABLED": true,
"TELEMETRY_ENDPOINT": "https://telemetry.example.com/v1/events",
"AUTH_MODE": "external",
"CONFIG_SCHEMA_VERSION": "2.0",
"RELEASE_MANIFEST_URL": "/release-manifest.json",
"CAPABILITY_OVERRIDES": {
"REALTIME": "DEFAULT",
"WEB_WORKER": "DEFAULT",
"SERVICE_WORKER": "DEFAULT",
"OFFLINE_COMMANDS": "DEFAULT"
},
"TECH_LOG_STUDIO_SOURCE": "HTTP",
"FEATURE_OVERRIDES": {
"reference-feature": "DEFAULT"
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"APP_ENV": "staging",
"API_BASE_URL": "https://api.staging.example.com/",
"REQUEST_TIMEOUT_MS": 10000,
"MAX_RETRY_ATTEMPTS": 2,
"TELEMETRY_ENABLED": true,
"TELEMETRY_ENDPOINT": "https://telemetry.staging.example.com/v1/events",
"AUTH_MODE": "external",
"CONFIG_SCHEMA_VERSION": "2.0",
"RELEASE_MANIFEST_URL": "/release-manifest.json",
"CAPABILITY_OVERRIDES": {
"REALTIME": "DEFAULT",
"WEB_WORKER": "DEFAULT",
"SERVICE_WORKER": "DEFAULT",
"OFFLINE_COMMANDS": "DEFAULT"
},
"TECH_LOG_STUDIO_SOURCE": "HTTP",
"FEATURE_OVERRIDES": {
"reference-feature": "DEFAULT"
}
}
@@ -1,4 +1,47 @@
{
"schemaVersion": 1,
"changes": []
"changes": [
{
"changeId": "add:@fontsource/ibm-plex-mono@5.3.0",
"owner": "tech-log-frontend",
"reviewer": "frontend-platform-security",
"reason": "Preserve the source application's IBM Plex Mono typography and bundled font assets without a runtime font request.",
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
},
{
"changeId": "add:pretendard@1.3.9",
"owner": "tech-log-frontend",
"reviewer": "frontend-platform-security",
"reason": "Preserve the source application's Pretendard Variable typography using its pinned bundled font asset.",
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
},
{
"changeId": "add:remark-directive@4.0.0",
"owner": "tech-log-frontend",
"reviewer": "frontend-platform-security",
"reason": "Parse the source TechLog document directive syntax through the migrated deterministic content pipeline.",
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
},
{
"changeId": "add:remark-gfm@4.0.1",
"owner": "tech-log-frontend",
"reviewer": "frontend-platform-security",
"reason": "Preserve source TechLog GitHub-flavored Markdown tables, task lists, and autolink parsing.",
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
},
{
"changeId": "add:remark-parse@11.0.0",
"owner": "tech-log-frontend",
"reviewer": "frontend-platform-security",
"reason": "Parse the source TechLog Markdown records into the migrated typed public-render model.",
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
},
{
"changeId": "add:unified@11.0.5",
"owner": "tech-log-frontend",
"reviewer": "frontend-platform-security",
"reason": "Compose the source-equivalent Markdown and directive parsing stages without framework coupling.",
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
}
]
}
+2 -1
View File
@@ -12,7 +12,8 @@
"ISC",
"MIT",
"MIT-0",
"MPL-2.0"
"MPL-2.0",
"OFL-1.1"
],
"deniedLicensePatterns": [
"(^|\\s)AGPL",
+26 -10
View File
@@ -1,12 +1,27 @@
# Manual accessibility review checklist
Automated axe checks do not establish WCAG conformance. A human reviewer must
review all six route records in `artifacts/tests/a11y-manual/` against one
release candidate and sign them. The required scope is derived from the route
registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`,
`REFERENCE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the
gate rejects blank identity/timestamp/signature fields, pending verdicts,
mismatched release IDs, or missing routes.
review all 27 route records in `artifacts/tests/a11y-manual/` against one
release candidate and sign them. The required TechLog Public and Studio scope is
derived from the installed route registry, so the gate rejects stale, missing,
or additional route records as well as blank identity/timestamp/signature
fields, pending verdicts, and mismatched release IDs. The scope is:
`NOT_FOUND`, `TECH_LOG_CASE`, `TECH_LOG_EXPLORE`, `TECH_LOG_EXPLORE_KIND`,
`TECH_LOG_HOME`, `TECH_LOG_PROFILE`, `TECH_LOG_PROJECT`,
`TECH_LOG_PROJECTS`, `TECH_LOG_PROJECT_ACTIVITY`,
`TECH_LOG_PROJECT_DECISIONS`, `TECH_LOG_PROJECT_RECORDS`,
`TECH_LOG_QUESTION`, `TECH_LOG_REFERENCE`, `TECH_LOG_RELEASE`,
`TECH_LOG_RELEASES`, `TECH_LOG_SEARCH`, `TECH_LOG_STUDIO_DOCUMENTS`,
`TECH_LOG_STUDIO_DOCUMENT_EDIT`, `TECH_LOG_STUDIO_DOCUMENT_NEW`,
`TECH_LOG_STUDIO_DOCUMENT_PREVIEW`, `TECH_LOG_STUDIO_DOCUMENT_PUBLISH`,
`TECH_LOG_STUDIO_DOCUMENT_VALIDATION`, `TECH_LOG_STUDIO_HOME`,
`TECH_LOG_STUDIO_NOT_FOUND`, `TECH_LOG_STUDIO_PUBLICATIONS`,
`TECH_LOG_STUDIO_PUBLICATION_PREVIEW`, `TECH_LOG_TOPIC`.
This list is not maintained by hand: `verify:documentation` compares it against
the installed route registry and fails when a registered route is absent. The
template carried the same rule for its own example screens; the route set is
this product's, the rule is the template's.
Allowed item verdicts:
@@ -17,7 +32,7 @@ Required record:
```text
Status: reviewed
Route ID: APP_HOME
Route ID: <exact installed route ID>
Release ID: <immutable release ID>
Reviewer: <human reviewer identity>
Reviewed at: <RFC 3339 timestamp>
@@ -45,7 +60,8 @@ The reviewer must verify:
- M7: non-essential motion is suppressed with reduced-motion preference
- Screen reader: headings, live regions, errors, and actions are announced once
`EXAMPLES_UI` requires real M4 modal and M5 field-error review; those items must
not be marked not-applicable on that route. Passing automated evidence means
Routes with dialogs or form errors require real M4 modal-focus or M5
error-association review; those items must not be marked not-applicable when the
reviewed route exposes the relevant behavior. Passing automated evidence means
only that tested pages had no critical or serious axe findings under the
recorded Chromium, Firefox, and WebKit runs.
recorded browser runs.
@@ -690,11 +690,20 @@ default physical layout은 구현과 동일하게 다음과 같다.
```text
/ca-frontend-opfs-v1/
authorities/<authorityToken>/<namespaceToken>/<partitionToken>/
objects/<object-id-prefix>/<opaque-object-id>/<generation>/manifest.json
objects/<object-id-prefix>/<opaque-object-id>/<generation>/manifest.json # physical v1 (read-only)
objects/<object-id-prefix>/<opaque-object-id>/g<generation>-<token>/manifest.json # physical v2 (new writes)
chunks/sha256/<digest-prefix>/<digest>.bin
staging/<transaction-id>/receipt.json
```
physical v2는 STO-01 수정의 일부다. logical `generation`은 설계상 transaction 간에
재사용되므로, 늦게 도착한 T1 보상이 같은 logical generation을 쓰는 T2의 디렉터리를
지울 수 있었다. v2는 transaction-unique `physicalGenerationId` fencing token을
경로, staging receipt, prepared object에 함께 기록해 보상이 자기 transaction의
디렉터리만 삭제하도록 만든다. expand 단계에서는 v1 경로/receipt/prepared object를
계속 읽고 새 write만 v2로 쓴다. rollback window가 끝나기 전에 v1 physical
generation을 일괄 삭제하지 않는다.
구조화 metadata, query, revision, refcount와 operation journal은 IndexedDB가
소유한다. OPFS에는 immutable chunk와 bounded runtime-schema-validated manifest만
둔다. readable `scope.namespace`는 경로에 쓰지 않는다.
@@ -736,6 +745,23 @@ COMMITTED <- 사용자에게 보이는 유일한 commit point
CLEANED -> journal 제거
```
보상(compensation)은 saga의 반쪽이며 다음 규칙을 따른다.
- journal row와 budget reservation은 physical cleanup effect가
`CLEANED` 또는 `ALREADY_CLEAN`으로 확인된 뒤에만 해제한다. timeout, crash,
malformed response, `EFFECT_UNKNOWN`은 성공이 아니며 `PREPARING`/`FILES_READY`를
그대로 남기고 `OBJECT_RECONCILE`로 반환한다.
- coordinator가 `abortPreparedPut()` 하나만 소유한다. worker client는 prepare 실패
시 별도의 fire-and-forget abort를 발행하지 않는다. 중복 보상은 아직 남아 있어야
할 journal row를 조기에 지우는 경로였다.
- 보상은 caller signal을 상속하지 않는다. composition이 소유한 bounded
`compensationSignal`을 사용하므로 이미 abort된 caller가 cleanup RPC 자체를
시작조차 못 하게 만들 수 없다.
- abort/cleanup은 origin mutation Web Lock을 physical 삭제와 staging 제거가 끝날
때까지 계속 보유한다. lease를 먼저 release하지 않는다. 단, staging이 아직 없는
transaction은 삭제할 것이 없으므로 lock을 기다리지 않고 `ALREADY_CLEAN`을
반환한다. 이는 자기 자신이 취소하는 BEGIN과의 deadlock을 막는다.
- `PREPARING` crash: partial staging을 검증 후 resume하거나 purge한다.
- `FILES_READY` crash: expected generation과 digest가 맞으면 idempotent logical
commit, 아니면 quarantine한다.
@@ -37,12 +37,41 @@ vendor 결정 전에는 안전한 기본값이 아니다.
abort마다 `http.request.completed` diagnostics를 정확히 한 번 남긴다.
`api.request.failed` telemetry는 retry가 끝난 terminal non-abort failure에만
정확히 한 번 발행한다.
5-1. V2 client와 V3 contract executor는 각각 자신의 logical execution에 대해
이 규칙을 만족한다. V3에서는 execution site가 `HttpExecutionObservation`
typed record 하나만 만들고, composition root의
`createHttpObservationProjector`가 유일한 projection authority다. observation은
arbitrary context map이 아니며 projector는 `route_id`, `operation_id`,
`operation`, `outcome`, `error_kind`, `http_status_group`,
`attempt_count_bucket`, `duration_bucket`만 사용한다. raw attempt count,
duration, status, URL, intent, key, input identity와 내부 `terminalReason`
sink로 나가지 않는다. effect certainty가 운영상 필요해지면 `effect_certainty`
key와 닫힌 value policy를 contract·fixture·이 ADR에 동시에 추가한 뒤에만
전달한다.
5-2. caller cancellation과 scope fence는 API failure가 아니다. diagnostics는 한
번 남기고 `api.request.failed`는 발행하지 않는다.
5-3. `routeId`는 installed operation-executor 경계의 필수 입력이다. feature
gateway가 소유한 low-cardinality route identity를 URL에서 재구성하지 않는다.
6. `app.boot.failed`, `ui.render.failed`, `release.mismatch.detected`,
`telemetry.delivery.dropped`를 production path에 연결한다. cache와 storage
실패는 diagnostics로 기록하되 raw key/value를 기록하지 않는다.
7. queue full, invalid event/context, serialization과 sink failure는 제한된
reason bucket으로 집계한다. drop observer의 failure는 다시 telemetry를
발행하지 않는 nonrecursive 경계다.
7-1. telemetry adapter lifecycle은 `ACTIVE | DISPOSED` 둘뿐이다. `dispose()`
한 번만 전이하고 `pagehide` listener 제거, queue 비우기, scheduled callback
generation 무효화, in-flight sink `AbortController` abort를 모두 수행한다.
dispose 뒤 `emit()`은 no-op이고 새 flush는 스케줄되지 않으며, abort를 무시한
sink가 늦게 settle해도 post-dispose delivery state를 갱신하거나 재스케줄하지
못한다. 종료 중 drop telemetry를 재귀적으로 발행하지 않는다.
7-2. `flush()`는 active delivery promise를 join한다. 이미 진행 중인 flush가
있으면 같은 promise를 반환하므로 `await flush()`는 실제 settle을 뜻한다.
7-3. runtime `infrastructure.dispose()`는 diagnostics/state dependency를 파괴하기
전에 `telemetry.dispose()`를 먼저 호출한다.
7-4. queue/entry capacity는 construction-time 계약이다. `Number.isSafeInteger`
아니거나 1 미만이거나 문서화된 ceiling(각각 `MAX_TELEMETRY_QUEUE`,
`MAX_DIAGNOSTIC_ENTRIES` = 10,000)을 넘으면 `TypeError`로 거절한다. NaN/Infinity가
조용히 eviction을 비활성화하는 경로를 남기지 않는다.
8. diagnostics와 telemetry failure는 제품 흐름, HTTP 결과, route transition,
storage/cache fallback과 React error surface를 바꾸지 않는다.
9. mount 전 bootstrap failure는 안전한 build/config/error kind만 별도 evidence로
@@ -79,6 +108,11 @@ route/application/HTTP/cache/storage/bootstrap
queue full, sink/observer failure와 pre-mount boot evidence를 검증한다.
- HTTP integration은 success, retry recovery, terminal failure와 abort의 producer
횟수, route/operation/correlation context와 요청 값 비노출을 검증한다.
- `tests/integration/http-execution-v3-observability.test.ts`는 V3 terminal
outcome이 실제로 closed allowlist를 통과하는지, terminal non-abort failure가
`api.request.failed`를 정확히 한 번 발행하는지, cancellation/scope fence가
발행하지 않는지, feature route ID가 executor 경계까지 보존되는지, sink 예외가
HTTP 결과를 바꾸지 못하는지를 검증한다.
- cache/storage/release/application/runtime test는 각 production wiring과
diagnostics failure isolation을 검증한다.
@@ -262,6 +262,55 @@ auth-required operation은 session state가 `authenticated`가 아니면 fetch
`integration-failed`, `unauthenticated`와 credential attach rejection을 anonymous
request로 downgrade하지 않는다.
#### 5-0. Logical effect certainty는 단조 증가한다
`PhysicalAttemptState`는 현재 attempt만 설명한다. logical execution 전체에는
별도의 monotonic accumulator를 두고 `joinMutationEffectCertainty`로 join한다.
join 순서는 보수적이다.
```text
NOT_STARTED < NOT_APPLIED < MAYBE_APPLIED < APPLIED_CONFIRMED
```
`fetch()` dispatch 시점에 command는 즉시 `MAYBE_APPLIED`를 기록한다. 이후 retry
loop entry, pre-dispatch final invariant, scope fence, cancellation, timeout
return은 모두 accumulator를 읽는다. 아직 보내지 않은 새 retry가 있다는 이유로
전체 logical operation을 `NOT_STARTED`로 되돌리지 않는다. query operation은
`NOT_APPLICABLE`로 남고 이 lattice를 쓰지 않는다.
#### 5-1. Installed auth profile registry (V3 집행)
`installRestAuthProfileRegistry()`가 composition 시점에 profile을 한 번 설치하고
`INSTALLED_REST_AUTH_PROFILES`가 유일한 authority다. contract composition
(`assertExecutionPolicy`)은 등록되지 않은 `authProfileId`를 거절하므로 executor는
runtime에 profile을 발명하지 않는다. profile은 다음을 exact하게 소유한다.
- Fetch `credentials` (credential collaborator가 바꿀 수 없다)
- `allowedCredentialHeaders`: 이 operation이 허용하는 정확한 proof header 집합
- `requiredCredentialHeaders`: dispatch 전에 반드시 관찰되어야 하는 집합
`CredentialPatchOutcome.READY`는 proof header만 담는다. `credentials` field는
제거되었다. credential owner가 transport-owned header(`accept`, `content-type`,
`idempotency-key`)나 forbidden header를 넣거나, profile이 허용하지 않는 header를
넣거나, required header를 빠뜨리면 `AUTH_INTEGRATION_FAILURE`이고 fetch 0회이며
command effect는 `NOT_STARTED`다. `idempotency-key`는 contract-owned이므로 더
구체적인 `UNEXPECTED_IDEMPOTENCY_KEY` request violation으로 남는다.
`UNAUTHENTICATED`는 user/session state이지 integration failure가 아니다.
transport-owned header는 credential header 뒤에 기록되어 key ordering으로도
shadow될 수 없고, final invariant가 `init.credentials`와 profile을 다시 대조하며
allowed/required credential header 집합을 독립적으로 재검증한다.
`AUTH_MODE=demo`는 profile을 약화시키지 않는다. `createDemoSessionAdapter`
고정된 비밀 아닌 `DEMO_AUTHORIZATION_MARKER` proof header를 제공하여 strict
`REFERENCE_EXTERNAL_BEARER`를 그대로 만족시킨다. 진짜 anonymous backend는 별도
anonymous contract/profile을 composition에서 선택해야 한다.
credential collaborator는 `AuthOperationContext { signal, deadlineAtMonotonicMs }`
받는다. cooperative owner는 스스로 중단하고, non-cooperative owner도 executor가
같은 lifetime signal과 race하므로 operation 수명을 넘기지 못하며 late completion은
관찰되지 않는다.
### 6. Cookie auth, CSRF와 CORS
same-origin BFF cookie session을 기본 권장한다.
@@ -11,6 +11,26 @@
첫 제품 stream/Web Push를 선택할 때, 또는 backend replay/hosting/provider
protocol이 바뀔 때
## 스트림 lifecycle은 freshness와 직교한다 (R-02, R-03)
`RealtimeStreamLifecycle = OPEN | DRAINING | CLOSED`는 freshness
(`UNKNOWN/CURRENT/STALE/RESYNCING`)와 별개다.
- effect/recovery deadline에 도달하면 commit capability를 즉시 영구 무효화하고
abort한다. caller에는 bounded `IDLE_TIMEOUT`(non-retryable, operation
`APPLY`/`RECOVER`)을 반환하되 **실제 task는 버리지 않고 retain**한다.
- retain된 task가 하나라도 있으면 stream은 `DRAINING`이고 새 event/recovery
admission을 거절한다. 실제 settlement가 일어나야 `STALE`로 돌아가
authoritative recovery를 요구하거나, close 요청이면 `CLOSED`가 된다.
- `close()``Promise<RealtimeResult<void>>`다. 모든 retain task가 실제로
settle해야 success이고, drain bound를 넘기면 `IDLE_TIMEOUT/CLOSE`를 반환하며
stream은 계속 `DRAINING`이다. teardown success가 곧 quiescence다.
- LIVE↔POLL overflow fail-close는 active/probe/quiescing/transition lease를
모두 abort한 뒤 **retired writer set**으로 옮기고 나서 reference를 지운다.
`close()`는 current와 retired를 dedupe해 함께 기다리므로, 버려진
non-cooperative writer가 아직 실행 중인데 close가 성공을 보고할 수 없다.
## 배경
현재 optional recipe catalog는 realtime capability에
+22
View File
@@ -17,11 +17,33 @@ The following edges are forbidden:
- domain to application, presentation, adapters, bootstrap, React, or browser globals
- application to presentation, concrete adapters, bootstrap, React, or browser globals
- `contracts` to application or features: contracts is the lower package and
owns the shared vocabulary both of them read
- presentation to concrete adapters, raw DTO schemas, or storage implementations
- generic presentation to the installed-feature registries: which features exist
is a product decision owned by `bootstrap`
- an adapter to presentation, bootstrap internals, or another concrete adapter
- feature domain/application to its presentation or outbound adapter, and
feature presentation to its outbound adapter
## The adapter kernel
"Another concrete adapter" excludes the adapter kernel, which is shared on
purpose and is the only adapter code an adapter may reach across a group for:
- `src/adapters/platform/**` — the system clock, the shared abort primitive and
the bounded-capacity guard
- `src/adapters/browser-file-storage/result.ts` — the browser-data result and
failure constructors
Each rule above is enforced by `check:architecture`, including the kernel
carve-out, so this table and the executable rules cannot drift apart. Two edges
are still open and are named explicitly in `.dependency-cruiser.json` rather
than left silent: the generic presentation modules that read the installed
registries today, and the two collaborator types `query-cache` reads from
`cross-context-invalidation`. Both lists are frozen — a new edge of either kind
fails the gate.
`bootstrap` contains composition only. Business rules and page-specific
orchestration belong to domain/application.
@@ -19,6 +19,37 @@
- 운영 절차:
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
## Installed binding snapshot과 stream cleanup bound (R-01, R-04, R-05, R-06)
- `installBrowserRpcContractBindings()`가 registry를 **parse → validate →
install** 순서로 처리한다. own data descriptor만 읽어 exact key set으로
null-prototype frozen snapshot을 만들고, 그 snapshot을 검증한 뒤 설치한다.
getter/accessor, extra key, symbol key, malformed descriptor, revoked proxy는
composition-time `TypeError`이며 getter는 호출조차 되지 않는다. runtime과
transport call은 이후 snapshot만 읽으므로 validation 이후 registry mutation이
replay policy·deadline·byte ceiling·transport selection을 바꿀 수 없다.
- server stream 종료는 transport iterator에 lifecycle authority를 위임하지
않는다. commit/admission generation은 즉시 fence하고 listener는 바로 해제하며,
`iterator.return()`은 cleanup **요청**으로서 bound 안에서만 기다린다. 끝나지
않은 cleanup은 관찰만 유지되고(unhandled rejection 없음) application generator는
bound 안에 종료된다. cleanup rejection은 이미 선택된 application failure를
덮지 않는다.
- WebSocket text frame은 allocation 전에 admission한다. UTF-16 code unit 길이가
이미 cap을 넘으면 encoder를 만들지 않고 거절하고, 나머지는 early exit하는
code-point 누적으로 센다. valid surrogate pair는 4 bytes, lone surrogate는
`TextEncoder`와 동일하게 replacement 3 bytes다.
- clock/fence collaborator 예외는 Result 경계를 벗어나지 않는다. clock 실패는
`SERVER_FAILURE/RPC_RUNTIME_DEPENDENCY_FAILED`, capture 실패는
`SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE`, `isCurrent` 실패는
fail-closed로 canonicalize하며 listener/timer는 단일 exit path에서 정확히 한 번
해제한다.
Browser RPC는 여전히 `AVAILABLE_NOT_COMPOSED`다. 선택된 Connect/gRPC-Web
transport는 enqueue-time `maxBufferedBytes`, raw/decompressed ceiling,
cancel/closed receipt, terminal framing, target browser와 load behavior를
별도로 증명해야 조립할 수 있다 (R-07).
## 1. 먼저 축을 분리한다
네 이름은 같은 종류의 대안이 아니다.
@@ -34,6 +34,16 @@ capability가 설치됐거나 production-ready라는 뜻이 아니다.
[Frontend ports, adapters, and boundaries](./frontend-ports-adapters-and-boundaries.md)
를 따른다.
## Bounded task lease와 DRAINING (R-02, R-03)
non-cooperative effect/recovery authority 하나가 stream tail 전체를 영구
wedge하지 못하도록, common coordinator는 각 task를 deadline으로 감싼다. deadline
초과 시 commit capability는 즉시 취소되지만 task 자체는 `retainedTasks`에 남아
stream을 `DRAINING`으로 유지한다. `close()`는 이 retain 집합이 실제로 settle해야
성공을 반환한다. handoff coordinator도 같은 원칙으로 fail-close된 writer를
`retiredWriters`에 보존한다.
## 0. 현재 상태와 목표 delta
이 문서에서 설계 승인, reference source 존재, production 조합과 target browser의
+3 -2
View File
@@ -5,7 +5,7 @@
"standard": "rules/diagram-standards.md v2",
"evidenceReport": {
"repoPath": "docs/architecture/review-evidence.md",
"canonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
"upstreamCanonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
"canonicalSha256": "b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29"
},
"reviews": {
@@ -25,5 +25,6 @@
"thresholdSatisfied": true,
"scope": "immutable static assets and mutable /config.json delivery"
}
}
},
"note": "`repoPath` is this repository's copy and must resolve. `upstreamCanonicalPath` and every `reviews[*].sourcePath` name the reviewing workspace, not this tree; they are provenance labels and are deliberately not resolvable here. `canonicalSha256` is what binds the two, and the gate checks it appears in `repoPath`."
}
@@ -0,0 +1,188 @@
{
"schemaVersion": 1,
"review": "third-review-2026-08-14",
"note": "GOV-03. The machine-readable disposition of every finding the third re-review raised. `check:remediation-ledger` joins this file against the prose ledger and refuses a blanket closure claim while any row is not FIXED, so a summary sentence can never outrun the evidence.",
"dispositions": [
{
"id": "NS-01",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "A credential owner's answer is decoded once, inside the auth boundary, through own data descriptors.",
"evidence": ["tests/integration/http-execution-v3-live-authority.test.ts"]
},
{
"id": "NS-02",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "Contract composition snapshots first and validates the snapshot, so the installed row is the row that was checked.",
"evidence": ["tests/unit/contract-registry-immutability.test.ts"]
},
{
"id": "NS-03",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "The `responseBody: NONE` probe owns its reader: the operation lifetime reaches it, and the lock is released.",
"evidence": ["tests/integration/http-execution-v3-live-authority.test.ts"]
},
{
"id": "NS-04",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "A journal transaction that cannot be completed is maintenance debt, not a settled write.",
"evidence": ["tests/unit/opfs-byte-store.test.ts"]
},
{
"id": "NS-05",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "A bootstrap failure answers with the request kind it belongs to, so the real cause survives the gateway.",
"evidence": ["tests/unit/opfs-worker-runtime.test.ts"]
},
{
"id": "NS-06",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "A reply is decoded before its pending row is released, and an uncorrelatable reply fails the channel closed.",
"evidence": ["tests/unit/opfs-worker-runtime.test.ts"]
},
{
"id": "NS-07",
"previous": "NEW",
"disposition": "FIXED",
"summary": "Cursor caps and collaborators are captured at construction, so a later mutation cannot widen a validated cap.",
"evidence": ["tests/unit/cursor-pagination-runtime.test.ts"]
},
{
"id": "NS-08",
"previous": "NEW",
"disposition": "FIXED",
"summary": "One terminal owner covers the whole public-cache staging body, so nothing writes after the abort.",
"evidence": ["tests/unit/public-response-cache.test.ts"]
},
{
"id": "RPC-01",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "Only a fulfilled, contract-shaped `waitClosed()` receipt prunes an active stream registration.",
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
},
{
"id": "RPC-02",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "Iterator cleanup and the lease decoder read foreign state inside their own boundaries.",
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
},
{
"id": "RPC-03",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "Every registry is snapshotted before any validation runs, and rows with hidden fields are refused.",
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
},
{
"id": "RPC-04",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "A transport result is an exact union: required own keys, no inherited extras, plain prototype.",
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
},
{
"id": "RT-01",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "A tracked task is registered before the authority is invoked, closing the reentrant-close window.",
"evidence": ["tests/unit/realtime/stream-coordinator.test.ts"]
},
{
"id": "RT-02",
"previous": "NEW",
"disposition": "FIXED",
"summary": "A scheduler that cannot install a deadline fails closed inside the realtime result contract.",
"evidence": ["tests/unit/realtime/stream-coordinator.test.ts"]
},
{
"id": "TR-01",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "The vault snapshots a registration and everything nested in it before validating or storing it.",
"evidence": ["tests/unit/presigned-transfer.test.ts"]
},
{
"id": "TR-02",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "A source lease that arrives after the delivery ended is closed exactly once by a compensator.",
"evidence": ["tests/unit/presigned-transfer.test.ts"]
},
{
"id": "TR-03",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "The shared abort primitive settles once by observation order, and all four consumers use it with bound timer snapshots.",
"markers": ["X-AUDIT-01", "X-AUDIT-02"],
"evidence": [
"tests/unit/abortable-operation.test.ts",
"tests/unit/image-cdn-runtime.test.ts",
"tests/unit/resumable-upload-fetch-transport.test.ts"
]
},
{
"id": "TR-04",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "Teardown proves quiescence of the raw provider registry, not only of the wrappers that bound it.",
"evidence": ["tests/unit/resumable-upload-runtime.test.ts"]
},
{
"id": "TR-05",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "The control-plane decoder validates an owned snapshot, so a stateful answer cannot swap a checked value.",
"evidence": ["tests/unit/resumable-upload-http-control-plane.test.ts"]
},
{
"id": "SW-01",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "The activation marker read is bounded in bytes, cancels what it refuses and releases its reader lock.",
"evidence": ["tests/unit/service-worker-runtime.test.ts"]
},
{
"id": "SW-02",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "The generator and the runtime decoder share one canonical asset-path predicate, and the generator self-validates.",
"evidence": ["tests/unit/service-worker-web-push-remediation.test.ts"]
},
{
"id": "WP-01",
"previous": "PARTIAL",
"disposition": "FIXED",
"summary": "One observation authority per click; certainty is monotone and the late-effect tail is owned by `waitUntil`.",
"evidence": ["tests/unit/web-push-worker-runtime.test.ts"]
},
{
"id": "GOV-03",
"previous": "OPEN",
"disposition": "FIXED",
"summary": "This file plus `check:remediation-ledger` bind each row to a disposition and a test path, and block a blanket closure claim while any row is open.",
"evidence": ["scripts/check-remediation-ledger.ts"]
},
{
"id": "GOV-04",
"previous": "OPEN",
"disposition": "FIXED",
"summary": "The inventory gate requires the exact named consumer set to resolve its import to the shared primitive and prints the set.",
"evidence": ["scripts/check-adapter-inventory.ts"]
},
{
"id": "GOV-05",
"previous": "OPEN",
"disposition": "FIXED",
"summary": "Duplicate abort mechanics were consolidated onto the shared primitive and the file-transfer budget was reset to cover the remaining correctness code.",
"markers": [],
"evidence": ["config/recipes/frontend-capability-recipes.json"]
}
]
}
@@ -0,0 +1,567 @@
# Adapter Remediation Ledger
> Source of truth for the execution state of every confirmed finding in
> [`docs/reviews/adapters/`](../reviews/adapters/README.md).
>
> Plan: [2026-08-13 adapter remediation](../superpowers/plans/2026-08-13-adapter-remediation.md).
> Baseline revision: `develop` / `4dc033cf33a5b6173bbf960d5eb464a406dc4c92`.
## Containment state (plan Task 1, Step 1)
Scan performed on the baseline revision:
```bash
rg -n "createBrowserOpfsRuntime|createBrowserFileRuntime|createPublicResponseCache|createBrowserRpcRuntime|createWebPush|createServiceWorker|createResumableUpload|createImageCdn" src recipes tests
rg -n "AVAILABLE_NOT_COMPOSED|DESIGNED_NOT_IMPLEMENTED|NOT_SELECTED" docs/architecture src/bootstrap
```
Result:
| Capability | Template default composition | Containment action |
| --- | --- | --- |
| OPFS byte store (`createBrowserOpfsRuntime`) | Not composed. Only `recipes/frontend-capabilities/*` reference the `"OPFS"` backend literal in contract/fake code. | None required. No V1 writer is admitted, so no kill switch is invented. |
| Browser files runtime | Not composed in `src/bootstrap/**`. | None. |
| Public response cache | Not composed; only `tests/unit/public-response-cache.test.ts` constructs it. | None. |
| Browser RPC runtime | Not composed; `AVAILABLE_NOT_COMPOSED`. | None. |
| Web Push | Not composed; `NOT_SELECTED` / `AVAILABLE_NOT_COMPOSED`. | None. |
| Resumable upload / image CDN | Not composed; test-only construction. | None. |
| Service Worker runtime host | Composed conditionally through `src/bootstrap/optional-runtime-host.ts``createServiceWorkerRuntimeHost`, gated by `ResolvedRuntimeCapabilities`. | Stays as-is. Task 14 fixes truthfulness without changing selection. |
| Realtime | `src/bootstrap/optional-runtime-host.ts:91` keeps realtime `NOT_SELECTED` with `realtime: null`. | None. |
No product-specific composition root outside the template default exists in this repository, so
there is no OPFS V1 write admission to close.
## Baseline gates (plan Task 1, Step 3)
Captured on the baseline revision before any source change.
| Command | Exit code | Result |
| --- | ---: | --- |
| `corepack pnpm check:types` | 0 | app, node, test, recipes, web-worker, service-worker projects all pass. |
| `corepack pnpm lint` | 0 | `--max-warnings=0` clean. |
| `corepack pnpm check:architecture` | 0 | 286 modules, 854 dependencies, all imports resolved; 12 graph fixtures PASS; TS-only policy PASS; allowed PASS / 9 forbidden rejected. |
| `corepack pnpm test:unit` | 1 | 110 passed / 1 failed test files; 1496 passed / 19 failed tests (1515 total), 150.92s. |
### `test:unit` failure attribution
The single failing file is `tests/unit/ci-artifact-contract.test.ts`. All 19 failures come from
child-process, cgroup, and filesystem-permission behavior of the sandboxed execution
environment, not from adapter code. Verbatim causes recorded from the run log:
```
Error: ENOENT: no such file or directory, open '/proc/1325422/task/1325422/children'
Error: provider output did not reach expected content: /tmp/ci-provider-upload-J9hxXD/provider-evidence/untrusted/vulnerability-report.json
Error: provider scope survived completion: ca-provider-vulnerability-1326812-f4869853ef09ea2c2c95cd01.scope
Error: EACCES: permission denied, open '/tmp/ci-captured-archive-OWTs7M/candidate.tar.gz'
Error: Test timed out in 10000ms.
AssertionError: expected 5714 to be less than 5000
AssertionError: expected [] to deeply equal ArrayContaining{…}
```
This matches the environment note already recorded in the review index. It is **not** converted
to an adapter failure and it is **not** treated as green. Every adapter task below must keep the
adapter-focused suites green and must not increase this file's failure count.
## Finding ledger
`Activation` records whether the finding is reachable on the current template execution path.
Rollout state starts at `NOT_STARTED`; documented-unimplemented items start at
`PROMOTION_BLOCKED` and are never labelled `DEFECT`.
### Network and state (`docs/reviews/adapters/01-network-and-state.md`)
| ID | Activation | Red test command | Fix commit/PR | Rollout state | Rollback trigger | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| N-01 | Live V3 path | `corepack pnpm exec vitest run tests/integration/http-execution-v3-observability.test.ts` | `fix: restore V3 HTTP observability` | `FIXED_NOT_RELEASED` | diagnostics/telemetry producer gate regression | Red 5/5 failed → green 5/5; `check:diagnostics` PASS (8 diagnostics, 5 telemetry producers); `check:types` PASS; `check:architecture` PASS |
| N-02 | Live V3 path | `corepack pnpm exec vitest run tests/integration/http-execution-v3-auth-profile.test.ts` | `fix: enforce installed HTTP auth profiles` | `FIXED_NOT_RELEASED` | authenticated request 4xx spike after profile enforcement | Red suite failed to load (`installRestAuthProfileRegistry` absent) → green 7/7; `check:types` PASS; `check:architecture` PASS; `lint` PASS; unit+integration+features 1560 passed with only the pre-existing environmental `ci-artifact-contract` failures |
| N-03 | Live V3 path | `corepack pnpm exec vitest run tests/unit/http-execution-v3.test.ts -t "retry-time fence"` | `fix: preserve command effect certainty across retries` | `FIXED_NOT_RELEASED` | command effect verdict regression | Red reproduced `SCOPE_FENCED` with `NOT_STARTED` after one dispatched attempt → green `MAYBE_APPLIED`; lattice table 9/9; `check:types` PASS; `lint` PASS |
| N-04 | Live composition teardown | `corepack pnpm exec vitest run tests/unit/telemetry.test.ts tests/unit/runtime-adapters.test.ts` | `fix: terminate telemetry work on disposal` | `FIXED_NOT_RELEASED` | telemetry delivery loss after teardown change | Red 10 failed (5 lifecycle + 5 capacity) → green 34/34; `check:diagnostics` PASS; `check:types` PASS; `check:architecture` PASS; `lint` PASS |
| N-05 | Rollout blocker (sidecar not composed) | `corepack pnpm exec vitest run tests/unit/conditional-validator-store.test.ts` | `fix: harden bounded state sidecars` | `FIXED_NOT_RELEASED` | persisted validator key incompatibility | Red collision case (two valid bindings sharing one delimiter-joined key) → green; key is now a bounded validated tuple encoded with `JSON.stringify` |
| N-06 | Legacy V2 rollback seam | `corepack pnpm exec vitest run tests/integration/http-client.test.ts` | `fix: harden the legacy HTTP rollback path` | `FIXED_NOT_RELEASED` | legacy keyed command rejection spike | Red 4 invalid-key cases → green; rejection happens before credentials and fetch (0 credential calls, 0 fetches) |
| N-07 | Legacy V2 rollback seam | `corepack pnpm exec vitest run tests/integration/http-client.test.ts tests/integration/auth-recovery.test.ts` | `fix: harden the legacy HTTP rollback path` | `FIXED_NOT_RELEASED` | legacy credential timeout regression | Red never-settling owner → green; credential wait races the existing attempt controller so no extra timer is added; ownership maps to REQUEST_TIMEOUT / REQUEST_ABORTED / AUTH_INTEGRATION_FAILURE with zero fetches |
| N-08 | Legacy V2 rollback seam | `corepack pnpm exec vitest run tests/unit/bounded-json-compatibility.test.ts` | `fix: harden the legacy HTTP rollback path` | `FIXED_NOT_RELEASED` | legacy JSON failure-code drift | Green 7/7 including throwing cancel/releaseLock; `readBoundedJson` now delegates to `bounded-body-reader` with the legacy codes preserved |
| N-09 | Live cross-context host | `corepack pnpm exec vitest run tests/unit/cross-tab-invalidation.test.ts` | `fix: harden bounded state sidecars` | `FIXED_NOT_RELEASED` | cross-tab invalidation drop | Red foreign-area pulse accepted → green 13/13; localStorage captured once and `StorageEvent.storageArea` compared by object identity; pulse key registered as `CACHE_INVALIDATION_PULSE`; `check:registries` PASS |
| N-10 | Cursor runtime `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/cursor-pagination-runtime.test.ts` | `fix: harden bounded state sidecars` | `FIXED_NOT_RELEASED` | pagination abort semantics change | Red never-settling loader → green `PAGINATION_ABORTED` with the late page ignored |
| N-11 | Live composition | `corepack pnpm exec vitest run tests/unit/telemetry.test.ts -t capacity` | `fix: terminate telemetry work on disposal` | `FIXED_NOT_RELEASED` | capacity rejection on valid composition | Red 5/5 capacity cases → green; ceilings documented in VD-07 §7-4 |
### Storage and browser files (`docs/reviews/adapters/03-storage-and-browser-files.md`)
| ID | Activation | Red test command | Fix commit/PR | Rollout state | Rollback trigger | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| STO-01 | OPFS not composed in template; **Critical** for any product writer | `corepack pnpm exec vitest run tests/unit/opfs-byte-store.test.ts tests/unit/opfs-worker-runtime.test.ts tests/unit/indexeddb-opfs-journal.test.ts` | `fix: preserve OPFS recovery authority during cleanup` | `FIXED_NOT_RELEASED` | OPFS reconcile backlog or journal growth | Red 4 new saga cases → green 25/25 across the three OPFS suites; `check:types` PASS (incl. web-worker); `check:browser-file-storage-boundaries` PASS; `lint` PASS; `test:unit` 1511 passed with only the pre-existing environmental `ci-artifact-contract` failures |
| STO-02 | Browser file runtime not composed | `corepack pnpm exec vitest run tests/unit/browser-file-download.test.ts` | `fix: execute canonical browser download targets` | `FIXED_NOT_RELEASED` | download navigation blocked by canonical target | Red 2 failed (raw relative href handed to host) → green 17/17 |
| STO-03 | Public cache not composed | `corepack pnpm exec vitest run tests/unit/public-response-cache.test.ts` | `fix: make public cache staging repairable` | `FIXED_NOT_RELEASED` | composition rejection of an existing policy | Red 4 cases across STO-03..05 → green 21/21; `check:types` PASS; `check:browser-file-storage-boundaries` PASS; `lint` PASS |
| STO-04 | Public cache not composed | `corepack pnpm exec vitest run tests/unit/public-response-cache.test.ts` | `fix: make public cache staging repairable` | `FIXED_NOT_RELEASED` | restage loop or bandwidth spike | — |
| STO-05 | Public cache not composed | `corepack pnpm exec vitest run tests/unit/public-response-cache.test.ts` | `fix: make public cache staging repairable` | `FIXED_NOT_RELEASED` | activation permitted without required capability | — |
| STO-06 | IndexedDB maintenance | `corepack pnpm exec vitest run tests/unit/indexeddb-maintenance.test.ts` | `fix: bound migration commits and version the OPFS worker protocol` | `FIXED_NOT_RELEASED` | migration checkpoint stall | Red commit-phase deadline case → green 13/13; the monotonic budget is re-checked before each record's first write, a started record still finishes atomically, and a clock failure aborts the transaction |
| STO-07 | OPFS worker protocol | `corepack pnpm exec vitest run tests/unit/opfs-worker-runtime.test.ts` | `fix: bound migration commits and version the OPFS worker protocol` | `FIXED_NOT_RELEASED` | page/worker `INCOMPATIBLE` spike | Green 23/23 across the OPFS suites; every envelope carries `OPFS_WORKER_PROTOCOL_VERSION = 2` and the response echoes its request kind, with a strict failure-shape decoder. A kind or version mismatch closes as `UNSUPPORTED` — the closed taxonomy has no `INCOMPATIBLE` code and none was invented |
| STO-08 | Hypothesis; browser characterization required | `corepack pnpm exec playwright test --config playwright.capabilities.config.ts tests/browser-capabilities/browser-files.spec.ts` | none (source unchanged) | `UNVERIFIED` | n/a until characterized | chromium 2/2 PASS; webkit could not launch (`libevent-2.1-7t64`, `libavif16` missing — environmental). The existing spec does not exercise `Window.showOpenFilePicker`/`showSaveFilePicker`, which need a user gesture and a native dialog, so the receiver-binding hypothesis is **neither reproduced nor refuted**. No `SystemPickerHost` was introduced: the plan forbids implementing an uncharacterized hypothesis as a defect. |
| GAP-01 | Documented unimplemented (VD-15) | promotion evidence, not a red test | — | `PROMOTION_BLOCKED` | n/a | — |
| GAP-02 | Documented unimplemented (VD-15) | promotion evidence, not a red test | — | `PROMOTION_BLOCKED` | n/a | — |
| GAP-03 | Documented unimplemented (VD-15) | promotion evidence, not a red test | — | `PROMOTION_BLOCKED` | n/a | — |
### Realtime and Browser RPC (`docs/reviews/adapters/02-realtime-and-browser-rpc.md`)
| ID | Activation | Red test command | Fix commit/PR | Rollout state | Rollback trigger | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| R-01 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | stream lease deadlock | Stream cleanup is bounded; the generator no longer waits indefinitely on a non-cooperative `iterator.return()` |
| R-02 | Realtime `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/realtime/stream-coordinator.test.ts` | `fix: retain realtime work through draining` | `FIXED_NOT_RELEASED` | stream stuck in `DRAINING` | Red never-settling effect and recovery → green 27/27; `close()` returns `IDLE_TIMEOUT` while a task is retained and success only after actual settlement |
| R-03 | Realtime `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/realtime/live-poll-handoff-coordinator.test.ts` | `fix: retain realtime work through draining` | `FIXED_NOT_RELEASED` | retired-writer set growth | Red overflow fail-close then `close()` → green 11/11; retired writers are waited on and only removed once actually quiesced |
| R-04 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-contract.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | binding install rejection | Red post-validation mutation, accessor and symbol cases → green 19/19; getters are never invoked |
| R-05 | WebSocket protocol codec | `corepack pnpm exec vitest run tests/unit/realtime/websocket-protocol.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | frame rejection regression | Red oversize frame allocated an encoder copy → green 8/8; byte counts match `TextEncoder` including lone surrogates |
| R-06 | Browser RPC `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts` | `fix: install bounded Browser RPC stream leases` | `FIXED_NOT_RELEASED` | closed-failure taxonomy drift | Clock and fence reads are canonicalised into the closed Result taxonomy with single-exit cleanup |
| R-07 | Promotion blocker | concrete transport conformance evidence | — | `PROMOTION_BLOCKED` | n/a | — |
### Browser transfer (`docs/reviews/adapters/04-browser-transfer.md`)
| ID | Activation | Red test command | Fix commit/PR | Rollout state | Rollback trigger | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| BT-PRE-01 | Presigned `AVAILABLE_NOT_COMPOSED` | `corepack pnpm exec vitest run tests/unit/presigned-transfer.test.ts` | `fix: lazy presigned download leases` | `FIXED_NOT_RELEASED` | download lease leak | Red lazy-lease cases → green 29/29; `open()` performs no network I/O and `close()` is idempotent |
| BT-PRE-02 | Wire contract gap | `corepack pnpm exec vitest run tests/unit/presigned-transfer.test.ts` | `fix: complete presigned capability and upload transport contracts` | `FIXED_NOT_RELEASED` | provider `POLICY_REJECTED` spike | Red missing/V0/V2 protocol cases → green; request always declares `PRESIGNED_TRANSFER_V1` and a mismatched response is closed as `POLICY_REJECTED` before vault registration |
| BT-PRE-03 | Presigned provider | `corepack pnpm exec vitest run tests/unit/presigned-transfer.test.ts` | `fix: complete presigned capability and upload transport contracts` | `FIXED_NOT_RELEASED` | timeout not bounding fetch | Red non-cooperative fetch → green; the scope races the task, the late response body is cancelled, and a throwing scheduler leaks no listener |
| BT-PRE-04 | Presigned vault | `corepack pnpm exec vitest run tests/unit/presigned-transfer.test.ts` | `fix: complete presigned capability and upload transport contracts` | `FIXED_NOT_RELEASED` | issuer/consumer split break | Red 6-case issuer-seam table → green; the vault re-checks method, href/origin/path agreement, credentials, byte, digest and expiry invariants itself |
| BT-PRE-05 | Provider path decoding | `corepack pnpm exec vitest run tests/unit/presigned-transfer.test.ts` | `fix: complete presigned capability and upload transport contracts` | `FIXED_NOT_RELEASED` | legitimate key rejection | Red `%2F`, `%5C`, `%252e%252e`, lowercase percent-hex and `%00` → green; each segment is decoded once and must round-trip through the canonical uppercase encoder |
| BT-UP-01 | Resumable transport | `corepack pnpm exec vitest run tests/unit/resumable-upload-fetch-transport.test.ts` | `fix: harden resumable upload transport contracts` | `FIXED_NOT_RELEASED` | signal facade rejection | `isAbortSignal` now requires `removeEventListener` and release cleanup is isolated |
| BT-UP-02 | Resumable transport | `corepack pnpm exec vitest run tests/unit/resumable-upload-fetch-transport.test.ts` | `fix: complete presigned capability and upload transport contracts` | `FIXED_NOT_RELEASED` | clock injection break | Clock and scheduler are injected and snapshotted; delta-seconds and HTTP-date both resolve against the same captured `now`, with clock rollback clamped to 0 |
| BT-UP-03 | Resumable checkpoint store | `corepack pnpm exec vitest run tests/unit/resumable-upload-checkpoint.test.ts` | `fix: report unknown IndexedDB delete effects` | `FIXED_NOT_RELEASED` | pending-delete registry growth | Red blocked-deadline case → green `PENDING`/`UNKNOWN`; a realm-scoped registry blocks recreating the partition |
| BT-UP-04 | Presigned part executor | `corepack pnpm exec vitest run tests/unit/resumable-upload-checkpoint.test.ts` | `fix: harden resumable upload transport contracts` | `FIXED_NOT_RELEASED` | expiry check rejection | Non-finite and negative clocks return `UNAVAILABLE`/`RESUME` instead of bypassing expiry |
| BT-UP-05 | Refactor | `corepack pnpm exec vitest run tests/unit/resumable-upload-runtime.test.ts` | — | `NOT_PERFORMED` | characterization drift | **Attempted and reverted.** The five internal owners were extracted mechanically, but they require a shared-internals module for `RuntimeDependencies`, `ActiveResolution`, `ReconciliationResolution`, `FAILURE_CODES`, `RECOVERIES`, `reportProgress`, `observeTerminal` and ~40 further bindings to avoid an import cycle. Rather than risk the verified correctness work in this file, the extraction was reverted rather than half-landed. Behaviour and the public facade are unchanged; the file is still 2,239 lines. |
| BT-UP-06 | Refactor | `corepack pnpm exec vitest run tests/unit/resumable-upload-runtime.test.ts` | `fix: drain resumable upload teardown` | `FIXED_NOT_RELEASED` | drain not quiescent | Red single-flight dispose case → green 18/18; `close()` closes admission and starts the same drain, `dispose()` aborts the active-operation registry and awaits real settlement before closing the checkpoint store |
| BT-UP-07 | Documented gap (Web Locks matrix) | promotion evidence | — | `PROMOTION_BLOCKED` | n/a | — |
| BT-IMG-01 | Type-contract change | `corepack pnpm check:types:test` fixture | `fix: complete presigned capability and upload transport contracts` | `FIXED_NOT_RELEASED` | caller compile break | `resolve()` now requires the lifetime signal; `tests/fixtures/typecheck/invalid-image-cdn-resolve-signal.ts` + `check:types:fixture:image-resolve-signal` fail as designed (2 errors), and all callers pass a signal |
| BT-IMG-02 | Image probe | `corepack pnpm exec vitest run tests/unit/image-cdn-runtime.test.ts` | `fix: parse Cache-Control with quote awareness` | `FIXED_NOT_RELEASED` | Cache-Control parse rejection | Red unmatched-quote cases → green 25/25 |
| BT-IMG-03 | Refactor | `corepack pnpm exec vitest run tests/unit/image-cdn-runtime.test.ts` | — | `NOT_PERFORMED` | characterization drift | **Not performed.** Same reasoning as BT-UP-05: a pure cohesion refactor of `image-cdn-runtime.ts` (1,340 lines) with no finding closure. `image-header-metadata.ts` is deliberately left intact per the review. |
| BT-IMG-04 | Documented gap (descriptor provider) | promotion evidence | — | `PROMOTION_BLOCKED` | n/a | — |
| BT-X-01 | Shared abort mechanics | `corepack pnpm exec vitest run tests/unit/abortable-operation.test.ts` | `fix: share abort and deadline mechanics` | `FIXED_NOT_RELEASED` | late-result compensation regression | Golden suite 8/8: first terminal owner, idempotent close, throwing scheduler, observed late rejection, late-handle compensation |
### Service Worker and Web Push (`docs/reviews/adapters/05-service-worker-and-web-push.md`)
| ID | Activation | Red test command | Fix commit/PR | Rollout state | Rollback trigger | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| SW-URL-01 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: make Service Worker cache and removal outcomes truthful` | `FIXED_NOT_RELEASED` | static asset cache miss rate | Red generator-shaped root-relative asset vs absolute Request URL → green; manifest URLs canonicalized once against the registration scope |
| SW-01 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: make Service Worker cache and removal outcomes truthful` | `FIXED_NOT_RELEASED` | stale response served | Red previous-cache hit → green network fallback; only the current release cache is opened, matched and deleted from |
| SW-02 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: make Service Worker cache and removal outcomes truthful` | `FIXED_NOT_RELEASED` | foreign cache deletion | Red prefix deletion of `ca-static-v1-not-owned` and longer suffixes → green exact `isOwnedStaticCacheName` only |
| SW-03 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: make Service Worker cache and removal outcomes truthful` | `FIXED_NOT_RELEASED` | false removal success | Red `unregister() === false` reported as UNREGISTERED → green FAILED |
| SW-04 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: make Service Worker cache and removal outcomes truthful` | `FIXED_NOT_RELEASED` | removal outcome misreport | Red removal modes always DISABLED → green outcome matrix (ABSENT/UNREGISTERED/PURGED→DISABLED, OWNERSHIP_MISMATCH→INCOMPATIBLE, FAILED→FAILED) |
| SW-05 | Build gate | `corepack pnpm exec vitest run tests/unit/service-worker-build-input.test.ts` | `fix: make Service Worker cache and removal outcomes truthful` | `FIXED_NOT_RELEASED` | build admission rejection | Red tamper table (stale digest, byte length, cross-origin URL, dot segment, extension mismatch, unknown field, duplicate URL) → green; build gate decodes through the shared codec and recomputes the canonical digest |
| SW-06 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: harden Service Worker activation and install lifecycle` | `FIXED_NOT_RELEASED` | activation handshake failure | Red foreign-source drain, source swap and 10 concurrent activations → green 24/24; replies correlate by source object identity against the captured waiting worker or controller, and activation/reset are single-flight |
| SW-07 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: harden Service Worker activation and install lifecycle` | `FIXED_NOT_RELEASED` | activation blocked with zero clients | An empty in-scope client set is vacuously drained; `clients.matchAll()` failure still rejects |
| SW-08 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: harden Service Worker activation and install lifecycle` | `FIXED_NOT_RELEASED` | per-client failure escalation | Per-client `postMessage` isolation; `skipWaiting()` is the commit point and its failure is REJECTED, with accepted/reload notifications sent only afterwards as best effort |
| SW-09 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | `fix: harden Service Worker activation and install lifecycle` | `FIXED_NOT_RELEASED` | late install work observed | Red late-fetch case → green; a fenced worker starts no new candidate work, late response bodies are cancelled, digest throws map to a closed outcome, and a second exact-delete runs once the abandoned install settles without extending the public bound |
| SW-10 | Protocol V2 migration | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | — | `DEFERRED_TO_MIGRATION` | V1/V2 mismatch fail-close | Not closed here. Full-identity protocol V2 is an expand → dual-read → old-writer drain → contract deployment that spans releases; the prerequisite shared manifest codec and canonical digest landed with SW-05. |
| WP-01 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-fence-store.test.ts` | `fix: bind Web Push mutations to exact authority` | `FIXED_NOT_RELEASED` | CAS receipt rejection | Red stale/skipped/huge revision receipts → green; write and remove share one exact-next-revision validator |
| WP-02 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-fence-store.test.ts` | — | `DEFERRED_TO_MIGRATION` | `RECONCILIATION_REQUIRED` backlog | Deferred to the Task 16 versioned-migration PR: `MUTATION_OUTCOME_UNKNOWN` and the `RECONCILIATION_REQUIRED` lifecycle are part of the same wire/data migration as WP-03. |
| WP-03 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-subscription-adapter.test.ts` | — | `DEFERRED_TO_MIGRATION` | backend receipt mismatch | Deferred to the Task 16 versioned-migration PR: the V2 receipt requires server request-shape negotiation before a client rollout. |
| WP-04 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-subscription-adapter.test.ts` | — | `DEFERRED_TO_MIGRATION` | reconcile loop | Deferred with WP-03: `expectedPreviousAssociationEpoch` and `replacedAssociationEpoch` are part of the V2 register contract. |
| WP-05 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-subscription-adapter.test.ts` | `fix: bind Web Push mutations to exact authority` | `FIXED_NOT_RELEASED` | pre-abort observation drift | A pre-aborted command records the requested operation instead of always INSPECT |
| WP-06 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-worker-runtime.test.ts` | `fix: bind Web Push mutations to exact authority` | `FIXED_NOT_RELEASED` | truncation reported degraded | Client handoff and notification cleanup report `countBucket` and `truncated`; an incomplete cleanup returns `{ complete: false }` and is DEGRADED |
| WP-07 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-worker-runtime.test.ts` | `fix: bind Web Push mutations to exact authority` | `FIXED_NOT_RELEASED` | late native effect certainty | Native notification effect is tracked as NOT_APPLIED → MAYBE_APPLIED → CONFIRMED and observed as evidence only |
## Final evidence (plan Task 18)
Captured after every correctness task landed.
### Focused subsystem suites (fresh processes)
| Suite | Exit | Result |
| --- | ---: | --- |
| `tests/unit/browser-rpc` + `tests/unit/realtime` | 0 | 192 passed |
| OPFS, IndexedDB, public cache, download | 0 | 80 passed |
| abortable-operation, presigned, resumable, image CDN | 0 | 104 passed |
| Service Worker + Web Push | 0 | 66 passed |
### Repository gates
| Command | Exit | Result |
| --- | ---: | --- |
| `corepack pnpm check:types` | 0 | all six projects pass |
| `corepack pnpm lint` | 0 | `--max-warnings=0` clean |
| `corepack pnpm check:architecture` | 0 | 288 modules, 865 dependencies; 12 fixtures PASS; allowed PASS / 9 forbidden rejected |
| `corepack pnpm check:diagnostics` | 0 | 8 diagnostics and 5 telemetry producers PASS |
| `corepack pnpm check:browser-file-storage-boundaries` | 0 | PASS, 34 rejections |
| `corepack pnpm check:realtime-boundaries` | 0 | PASS |
| `git diff --check` | 0 | clean |
| `corepack pnpm test:component` | 0 | 126 passed |
| `corepack pnpm test:integration` | 0 | 52 passed |
| `corepack pnpm test:reference-feature` | 0 | 26 passed |
| `corepack pnpm test:recipes` | 0 | 17 passed |
| `corepack pnpm test:unit` | 1 | 1585 passed / 19 failed tests; the 19 are the unchanged pre-existing `ci-artifact-contract` sandbox failures |
| `corepack pnpm check:registries` | 0 | 11 registries PASS |
| `corepack pnpm verify:documentation` | 0 | PASS_SCOPED |
| `corepack pnpm check:types:fixture:image-resolve-signal` | 1 | **Expected non-zero.** Negative fixture proving `resolve()` now rejects a call without a lifetime signal (BT-IMG-01). |
`test:all` stops at `test:unit`, so the later suites above were run directly.
### Final disposition of all 64 findings
| State | Count |
| --- | ---: |
| `FIXED_NOT_RELEASED` | 51 |
| `PROMOTION_BLOCKED` (unchanged by design) | 6 |
| `DEFERRED_TO_MIGRATION` (`WP-02`, `WP-03`, `WP-04`, `SW-10`) | 4 |
| `NOT_PERFORMED` (`BT-UP-05`, `BT-IMG-03` cohesion refactors) | 2 |
| `UNVERIFIED` (`STO-08` browser hypothesis) | 1 |
No finding remains `NOT_STARTED`.
### Failures that are NOT claimed as green
| Gate | Status | Attribution |
| --- | --- | --- |
| `tests/unit/ci-artifact-contract.test.ts` | 19 failed | Identical to the baseline capture. Sandbox child-process, cgroup and `/tmp` permission behavior; unrelated to adapters. Count did not change across any task. |
| `corepack pnpm test:browser-capabilities` (webkit) | 6 failed / 24 passed | WebKit cannot launch: missing `libevent-2.1-7t64` and `libavif16`. Chromium passes. **UNVERIFIED**, not PASS. |
| `corepack pnpm test:browser-file-storage-removal` | exit 1 | The reduced-removal-fixture prunes the CI contract to 77/89/102 while `scripts/contracts/ci-gates.ts:481` demands exactly 81/93/105. That file, the CI contract and `scripts/lib/removal-fixture.ts` are **unchanged since the baseline revision** (`git diff --name-only 4dc033c..HEAD` outside `src/`, `tests/` and `docs/` lists only the two Service Worker build scripts), so this is pre-existing, not a regression from this work. |
| `corepack pnpm test:realtime-removal` | exit 1 | Same pre-existing reduced-fixture arithmetic. |
| Server/provider compatibility matrices (presigned V1, Web Push V1/V2, Service Worker V1/V2, OPFS V1/V2) | not run | No provider or multi-release infrastructure in this environment. **UNVERIFIED**. |
| Staging rollback drill | not run | Requires a staging deployment. **UNVERIFIED**. |
### Work deliberately not performed
| Plan task | Status | Reason |
| --- | --- | --- |
| Task 17 extraction (`BT-UP-05`, `BT-IMG-03`, OPFS/cache/download decomposition) | **NOT PERFORMED** | Pure cohesion refactor with no finding closure. `BT-UP-05` was attempted: the five internal owners extract cleanly, but they need a shared-internals module for ~40 types, constants and helpers to avoid an import cycle, so the attempt was reverted rather than half-landed. `BT-UP-06`, the one item in this group with behavioural content, **was** implemented. |
| `SW-10`, OPFS physical/protocol V2 rollout, presigned and Web Push receipt V2 (`WP-02`, `WP-03`, `WP-04`) | **DEFERRED_TO_MIGRATION** | These are expand → dual-read/emit → old-writer drain → contract deployments requiring server request-shape negotiation and multi-release drain windows. The prerequisite in-repo pieces landed: the shared Service Worker manifest codec and canonical digest (`SW-05`), the OPFS worker protocol version and strict correlation (`STO-07`), and the OPFS physical generation token (`STO-01`). |
| Promotion gaps `GAP-01`, `GAP-02`, `GAP-03`, `R-07`, `BT-UP-07`, `BT-IMG-04` | `PROMOTION_BLOCKED` | Unchanged by design. No availability state was raised and no optional capability was added to the default bootstrap. |
## Re-review remediation (2026-08-14)
Source: [`docs/reviews/adapters/RE-REVIEW-2026-08-14.md`](../reviews/adapters/RE-REVIEW-2026-08-14.md),
38 findings (High 19 / Medium 17 / Low 2) raised against `3b481eb`.
**GOV-02.** That re-review found the previous section of this ledger closed a
number of rows as `FIXED_NOT_RELEASED` that were in fact partial. The tables
below are written the other way round: a row is `FIXED` only where a new
adversarial test failed first on the pre-fix source and passes on the landed
one.
**All 38 second re-review findings are `FIXED` as scoped below.** The first pass
closed 26; the second closed the remaining twelve, which each needed a lifecycle
or contract change rather than a contained edit. A third re-review then found
that twenty of those closures held only on the paths their tests exercised; that
verdict and its remediation are recorded in the third re-review section further
down, and this section is left as written so the two passes stay comparable.
### Landed
| ID | Severity | Disposition | Commit | Red-then-green evidence |
| --- | --- | --- | --- | --- |
| LIVE-01 | High | `FIXED` | `f4bfdf0` | `tests/integration/http-execution-v3-live-authority.test.ts` — UNAVAILABLE, sync throw, async rejection and a malformed outcome each closed as `UNAUTHENTICATED` before the fix; all four now close as `AUTH_INTEGRATION_FAILURE` with zero fetches. |
| LIVE-02 | High | `FIXED` | `f4bfdf0` | `tests/unit/contract-registry-immutability.test.ts` — a borrowed `Map.prototype.clear` emptied the installed profile registry before the fix. |
| LIVE-03 | High | `FIXED` | `f4bfdf0` | Same suite — the composed HTTP registry was clearable and a post-composition mutation of a source policy changed `totalDeadlineMs` from 10000 to 999999. |
| LIVE-04 | Medium | `FIXED` | `f4bfdf0` | Same integration suite — a non-cooperative fetch and reader held the port result open; a body that finished after the deadline was admitted as SUCCESS. |
| LIVE-05 | High | `FIXED` | `f4bfdf0` | Same suite — a DEADLINE timeout emitted no `api.request.failed`. |
| LEG-01 | High | `FIXED` | `ca210d3` | `tests/integration/legacy-http-credential-authority.test.ts` — a recovery that answered after the deadline called `onUnauthenticated` once; it now calls it zero times, and only an adopted no-session result notifies. |
| LEG-02 | High | `FIXED` | `ca210d3` | Same suite — a bearer profile dispatched with no `Authorization` at all. |
| OPT-NET-01 | Medium | `FIXED` | `ca210d3` | `tests/unit/legacy-and-optional-network-remediation.test.ts` — a loader rejection with a live signal became `PAGINATION_ABORTED`. |
| OPT-NET-02 | Low | `FIXED` | `ca210d3` | Same suite — `defineMutationIntent` accepted control characters the executor rejected. |
| STO-RR-01 | High | `FIXED` | `6a8281a` | `tests/unit/opfs-worker-runtime.test.ts` — a strict non-reentrant lease manager made `FINALIZE_PUT` hang forever; `tests/unit/opfs-byte-store.test.ts` pins that a failed finalization is no longer a plain success. |
| STO-RR-02 | Medium | `FIXED` | `6a8281a` | Same suite — every failure answered with kind `CAPABILITIES`. |
| STO-RR-03 | Medium | `FIXED` | `6a8281a` | Same suite — `{code:"EVIL"}` reached the caller; a non-boolean `retryable` and a throwing getter left the RPC to time out. |
| STO-RR-04 | Medium | `FIXED` | `6a8281a` | `tests/unit/public-response-cache.test.ts` — a transient marker read failure deleted the active candidate. |
| STO-RR-05 | Medium | `FIXED` | `6a8281a` | Same suite — one failed repair fetch destroyed every healthy asset in the release. |
| RPC-RR-02 | Medium | `FIXED` | `bd90e0c` | `tests/unit/browser-rpc/browser-rpc-remediation.test.ts` — a throwing fence and a throwing `clock.sleep` escaped the Result contract. |
| RPC-RR-03 | High | `FIXED` | `bd90e0c` | Same suite — a transport accessor ran during validation, and the installed binding registries exposed `set`/`delete`/`clear`. |
| RPC-RR-04 | Medium | `FIXED` | `bd90e0c` | Same suite — extra, inherited, symbol-keyed and throwing-getter transport values passed. |
| SW-RR-01 | High | `FIXED` | `efc577d` | Bounded marker reader with a read deadline, reader cancel and fatal UTF-8 decode replaces `response.text()`. |
| SW-RR-02 | Medium | `FIXED` | `efc577d` | A `null` `event.source` no longer satisfies activation or reset completion. |
| SW-RR-03 | Medium | `FIXED` | `efc577d` | `tests/unit/service-worker-web-push-remediation.test.ts` plus the `check:adapter-inventory` gate — generator and decoder now share one exported table. |
| SW-RR-04 | Medium | `FIXED` | `efc577d` | `cache.match` rejection is closed as a miss so `respondWith` reaches its network fallback. |
| WP-RR-01 | Medium | `FIXED` | `efc577d` | `focus`/`openWindow` carry NOT_APPLIED → MAYBE_APPLIED → CONFIRMED and a late effect is observed exactly once. |
| TR-RR-08 | Medium | `FIXED` | `69cb7e3` | `tests/unit/resumable-upload-http-control-plane.test.ts` — a throwing getter escaped as `TypeError` out of `createSession`; symbol and non-enumerable extras passed the key check. |
| TR-RR-09 | Medium | `FIXED` | `fb5b449` | `tests/unit/image-cdn-runtime.test.ts` — the suite pinned the contradictory `private, no-store` as success; the recorded fail-closed matrix now applies. |
| GOV-01 | Low | `FIXED` | this commit | `scripts/check-adapter-inventory.ts` diffs `docs/reviews/adapters/INVENTORY.md` against `git ls-files src/adapters`. The missing `src/adapters/platform/abortable-operation.ts` row is restored and the total is 119/119. |
| GOV-02 | Medium | `FIXED` | this commit | This section replaces the over-closed rows with evidence-linked dispositions and an explicit not-done list. |
### Landed in the second pass
The twelve findings the first pass did not reach are now closed on the same
terms: a named adversarial test failed on the pre-fix source and passes on the
landed one.
| ID | Severity | Commit | Red-then-green evidence |
| --- | --- | --- | --- |
| RPC-RR-01 | High | `a7390e3` | `tests/unit/browser-rpc/browser-rpc-remediation.test.ts``openServerStream` returns a lease (`streamId`, `frames`, `cancel`, `waitClosed`) decoded from own data descriptors. A timed-out stream is cancelled exactly once, a second stream for the same operation is refused as `CONFLICT` / `RPC_STREAM_DRAINING` without reaching the transport, and admission resumes only after `waitClosed()` settles. |
| RT-RR-01 | High | `c0f53d1` | `tests/unit/realtime/stream-coordinator.test.ts` — a `close()` during a running apply reported success before the fix; tasks are now registered at invocation, so it reports `IDLE_TIMEOUT` and `DRAINING`. |
| RT-RR-02 | High | `c0f53d1` | Same suite — a queued event started running inside DRAINING, and a timed-out effect left its resume token in place. The queued event is now dropped as `CLOSED` at execution time and the token is discarded with `freshness: UNKNOWN`. |
| RT-RR-03 | Medium | `c0f53d1` | `tests/unit/realtime/live-poll-handoff-coordinator.test.ts` — a second `close()` replayed the cached timeout forever; only an in-flight close is shared now, fenced writers are retained until their tails settle, and a later close converges to success. |
| RT-RR-04 | High | `c0f53d1` | Same suite — `close()` reported quiescence while a checkpoint was still running; checkpoint work now joins the physical-task registry. |
| TR-RR-05 | High | `46e067e` | `tests/unit/abortable-operation.test.ts` — a rejection was reported as `TERMINAL/CLOSED` while `terminal()` said no owner, and a throwing scheduler released the caller listener leaving no owner at all, so later aborts were invisible. The primitive now distinguishes `REJECTED`, agrees with `terminal()`, snapshots the scheduler, closes atomically on install failure and compensates a late value exactly once. Both presigned subsystems migrated onto it, replacing two hand-written copies. |
| TR-RR-01 | High | `46e067e` | `tests/unit/presigned-transfer.test.ts``close()` released bookkeeping without aborting, and the consumer signal joined only after the fetch began. Both are fixed; the scheduler-failure test now pins fail-closed. |
| TR-RR-02 | High | `46e067e` | The upload scope is created before the digest and the digest races the caller and deadline; the vault claim and network call follow an owner re-check. |
| TR-RR-03 | High | `46e067e` | Same suite — the registration is a versioned exact union: unknown/missing protocol version, plaintext target, ambient credential and cookie headers, a non-2xx expected status and any extra own field are each refused at the issuer seam. |
| TR-RR-04 | Medium | `5a76f95` | Same suite — the presigned source was never closed on success, writer failure or abort; a holder now closes it exactly once at the outermost boundary on all three. |
| TR-RR-06 | High | `5a76f95` | `tests/unit/resumable-upload-runtime.test.ts` — a never-granting mutation lock made `dispose()` unbounded; it is now bounded by `cleanupDeadlineMs`, returns the drain result, and leaves the runtime `CLOSING` with the checkpoint store open when the drain is unproved. |
| TR-RR-07 | High | `5a76f95` | `tests/unit/image-cdn-runtime.test.ts` — after an abort a new verification was admitted while the abandoned verifier still ran; the slot is now held until the raw verifier settles. |
**No second re-review row remains `NOT_STARTED`**, and the
structural gate for the shared `abortable-operation` primitive is now active —
it was withheld until the primitive actually had production importers, because a
gate that fails CI for a documented but unfixed defect reports the wrong thing.
Contracts that changed shape, and are therefore breaking for an external
implementor:
| Contract | Change | Reason |
| --- | --- | --- |
| `BrowserRpcTransport.openServerStream` | returns `BrowserRpcServerStreamLease` instead of `AsyncIterable` | RPC-RR-01 needs cancellation and closure evidence |
| `AuthSessionPort.recover` | accepts an optional `CredentialOperationContext` | LEG-01; optional for one release |
| `PresignedCapabilityRegistration` | gains `protocol` | TR-RR-03 versioned exact union |
| `ResumableUploadRuntime.dispose` | returns `BrowserDataResult<void>` | TR-RR-06 bounded drain result |
| `ResumableUploadRuntimePolicy` | gains `cleanupDeadlineMs` | TR-RR-06 teardown bound |
| `ImageCdnPresentationPort.resolve` | `signal` required | BT-IMG-01, landed earlier |
### Gates after this pass
Run on the landed tree. Only what actually passed is claimed as passing.
| Command | Exit | Result |
| --- | ---: | --- |
| `corepack pnpm check:types` | 0 | all six projects |
| `corepack pnpm lint` | 0 | `--max-warnings=0` clean |
| `corepack pnpm check:architecture` | 0 | 289 modules, 868 dependencies; 12 fixtures PASS |
| `corepack pnpm check:adapter-inventory` | 0 | 119 files, 7 shared asset extensions, fixture linking, primitive importers |
| `corepack pnpm check:registries` | 0 | 11 registries PASS |
| `corepack pnpm check:diagnostics` | 0 | 8 diagnostics / 5 telemetry producers |
| `corepack pnpm check:browser-file-storage-boundaries` | 0 | PASS, 34 rejections |
| `corepack pnpm check:realtime-boundaries` | 0 | PASS |
| `corepack pnpm verify:documentation` | 0 | PASS_SCOPED |
| `git diff --check` | 0 | clean |
| `tests/unit` + `tests/integration`, no exclusions | — | 1643 passed / 1747; the 104 failures are the four environmental files below |
| `corepack pnpm test:component` | 0 | 126 passed |
| `corepack pnpm test:recipes` | 0 | 17 passed |
| `corepack pnpm test:reference-feature` | 0 | 26 passed |
### Environmental failures, not claimed as green
| Gate | Status | Attribution |
| --- | --- | --- |
| `tests/unit/ci-workflow-generation.test.ts` | 82 failed / 325 passed | Identical on the pre-change baseline (`git stash` comparison). The subprocess gates it spawns cannot run in this sandbox. |
| `tests/unit/ci-artifact-contract.test.ts` | fails | Unchanged pre-existing sandbox, cgroup and `/tmp` permission behaviour. |
| `tests/unit/security-followup.test.ts`, `tests/unit/provider-guardian-transaction.test.ts`, `tests/unit/risk-coverage.test.ts` | flaky under full-suite load | All three pass in a fresh process (78 passed together). They spawn and reap process groups, so their timing assertions are load sensitive. |
### Destructive fixture hazard — fixed
Outside the 38 findings, and found while running the suites for them.
Four sites linked the repository's installed dependencies into a throwaway
fixture with a single directory symlink at `<fixture>/node_modules`:
- `scripts/lib/removal-fixture.ts`
- `scripts/check-supply-chain-provider-fixtures.ts`
- `tests/integration/security-followup-archive.test.ts`
- `tests/unit/ci-artifact-contract.test.ts`
Each fixture then runs `pnpm` inside itself. pnpm does not recognise the modules
directory it finds there and purges it; with `CI=true` it does so without a
prompt. The purge followed the symlink and deleted the **repository's own**
`node_modules` mid-run — a test suite uninstalling the workspace it was running
in. That is what produced the cascading, file-unrelated failures a full
`test:unit` run reported, and it happened twice during this work.
`scripts/lib/fixture-node-modules.ts` replaces all four: `node_modules` is a
real directory whose entries are individual symlinks, so a recursive delete
unlinks the fixture's own links instead of walking through one link into the
shared tree. `tests/unit/fixture-node-modules.test.ts` performs the exact
recursive delete pnpm performs and asserts the source tree survives, and
`corepack pnpm check:adapter-inventory` fails on any reintroduction of the
directory-symlink form.
After the fix a full `tests/unit` + `tests/integration` run leaves the
dependencies intact and its failures are confined to the two environmental
files above plus the two flaky-under-load ones:
| File | Failed | Attribution |
| --- | ---: | --- |
| `tests/unit/ci-workflow-generation.test.ts` | 82 | Identical on the pre-change baseline (`git stash` comparison). Its subprocess gates cannot run in this sandbox. |
| `tests/unit/ci-artifact-contract.test.ts` | 19 | Unchanged pre-existing sandbox, cgroup and `/tmp` permission behaviour. |
| `tests/unit/security-followup.test.ts` | 2 | Passes in isolation. |
| `tests/unit/provider-guardian-transaction.test.ts` | 1 | Passes in isolation. |
1619 passed / 1723 total, and `tests/unit/removal-fixture.test.ts`,
`tests/unit/supply-chain.test.ts` and
`tests/integration/security-followup-archive.test.ts` — the three that had to be
excluded before — now pass in the full run.
## Third re-review (2026-08-14)
A third read-only re-review re-tested all 38 rows above against hostile,
non-cooperative, late-completing and mutable inputs. It confirmed 18 as fixed
and found 20 that held only on the paths their tests exercised, plus three new
findings and three governance defects. The common shape was the same in almost
every case: a value was **checked and then read again**, or a wrapper settled
and was mistaken for the physical work it was bounding.
`docs/operations/adapter-remediation-dispositions.json` is the machine-readable
record; `corepack pnpm check:remediation-ledger` joins it against this document,
verifies every named evidence path exists, and refuses a blanket closure
sentence while any row is still open. **GOV-03.** The previous section claimed
"All 38 are now FIXED" while six rows were reproducibly partial — a sentence is
cheap and a reviewer reads it as evidence, so the claim is now derived rather
than authored.
### Landed
| ID | Prior verdict | Disposition | Red-then-green evidence |
| --- | --- | --- | --- |
| NS-01 | `PARTIAL` | `FIXED` | `tests/integration/http-execution-v3-live-authority.test.ts` — a throwing `kind` getter escaped the auth boundary and an auth outage was reported as `NETWORK_FAILURE`; nine hostile credential shapes now close as `AUTH_INTEGRATION_FAILURE` with zero fetches, and each field is read exactly once. |
| NS-02 | `PARTIAL` | `FIXED` | `tests/unit/contract-registry-immutability.test.ts` — a policy that answered `10_000` to validation and `999_999` to the copy installed the second value; composition now snapshots first, so the out-of-ceiling value is refused. |
| NS-03 | `PARTIAL` | `FIXED` | `tests/integration/http-execution-v3-live-authority.test.ts` — a `NONE` probe left `body.locked === true` after a deadline; the reader is now cancelled once and its lock released. |
| NS-04 | `PARTIAL` | `FIXED` | `tests/unit/opfs-byte-store.test.ts` — a failed `journal.complete` still returned plain success with `SUCCEEDED` telemetry; it now returns a `RECONCILE` failure and leaves the row `COMMITTED`, and an unfinished delete is observed `DEGRADED`. |
| NS-05 | `PARTIAL` | `FIXED` | `tests/unit/opfs-worker-runtime.test.ts` — a bootstrap failure answered every request with kind `CAPABILITIES`, so the gateway replaced `BLOCKED` with `UNSUPPORTED`; all twelve kinds now round-trip their own correlation. |
| NS-06 | `PARTIAL` | `FIXED` | Same suite — a throwing `requestId` getter produced an RPC timeout and a stateful trap left the public promise pending forever; replies are decoded before the pending row is released and an uncorrelatable reply fails the channel closed. |
| NS-07 | `NEW` | `FIXED` | `tests/unit/cursor-pagination-runtime.test.ts` — raising `maxPages` after construction widened a validated cap from one page to three; caps and collaborators are captured once. |
| NS-08 | `NEW` | `FIXED` | `tests/unit/public-response-cache.test.ts` — a non-cooperative fetch held the mutation lock forever, and a digest finishing after the abort still wrote the asset and the activation marker; one terminal owner now covers the whole staging body. |
| RPC-01 | `PARTIAL` | `FIXED` | `tests/unit/browser-rpc/browser-rpc-remediation.test.ts` — a rejecting, throwing or non-promise `waitClosed()` was absorbed into success and a second physical stream opened; only a fulfilled contract-shaped receipt prunes the registration. |
| RPC-02 | `PARTIAL` | `FIXED` | Same suite — a throwing iterator `return` accessor replaced the selected timeout with a native `TypeError`, and the exported lease decoder threw on a hostile `Symbol.asyncIterator`. |
| RPC-03 | `PARTIAL` | `FIXED` | Same suite — a registry accessor ran twice during validation and rows hiding fields behind a prototype or a non-enumerable key installed; every registry is snapshotted before validation. |
| RPC-04 | `PARTIAL` | `FIXED` | Same suite — own `{ok,message,encodedBytes}` plus a prototype `injected` was a success, and a missing `message` reached a permissive schema as `undefined`. |
| RT-01 | `PARTIAL` | `FIXED` | `tests/unit/realtime/stream-coordinator.test.ts` — an authority that re-entered `close()` from inside its own invocation got `{ok:true}` while its effect was pending; the task is registered before the collaborator is called. |
| RT-02 | `NEW` | `FIXED` | Same suite — a throwing `scheduleTimeout` started a recovery that overlapped the running apply and made `close()` reject with a native `TypeError`; an uninstallable deadline now fails closed inside the result contract. |
| TR-01 | `PARTIAL` | `FIXED` | `tests/unit/presigned-transfer.test.ts` — a stateful issuer could show an allowed header set to the forbidden-header check and store `Authorization`; the registration and everything nested in it is snapshotted before validation. |
| TR-02 | `PARTIAL` | `FIXED` | Same suite — a source lease that resolved after an abort was never closed; a compensator sharing the holder's close-once latch closes it exactly once. |
| TR-03 | `PARTIAL` | `FIXED` | `tests/unit/abortable-operation.test.ts`, `tests/unit/image-cdn-runtime.test.ts`, `tests/unit/resumable-upload-fetch-transport.test.ts` — the outcome depended on a hard-coded four-microtask drain, and a throwing scheduler rejected `probe()`/`execute()` natively while leaking a caller listener. The primitive now settles once by observation order, and all four consumers use it with construction-time bound timer snapshots. |
| TR-04 | `PARTIAL` | `FIXED` | `tests/unit/resumable-upload-runtime.test.ts` — a provider outliving its attempt deadline let `dispose()` report a drained runtime and close the checkpoint store; raw provider work is now its own registry and both must be quiescent. |
| TR-05 | `PARTIAL` | `FIXED` | `tests/unit/resumable-upload-http-control-plane.test.ts` — a stateful `sessionId` passed the regex and returned `../../unsafe`; the decoder validates an owned snapshot read exactly once. |
| SW-01 | `PARTIAL` | `FIXED` | `tests/unit/service-worker-runtime.test.ts` — a single 1 MiB chunk was retained before the 257-byte ceiling was compared, a declared oversize left the body open and the reader lock was never released. |
| SW-02 | `PARTIAL` | `FIXED` | `tests/unit/service-worker-web-push-remediation.test.ts` — the generator emitted `/assets/bad@name-abcdefgh.js` and the decoder then refused the manifest it had just produced; both share one canonical path predicate and the generator self-validates its output. |
| WP-01 | `PARTIAL` | `FIXED` | `tests/unit/web-push-worker-runtime.test.ts` — an ordinary click was counted twice, a late rejection downgraded `MAYBE_APPLIED` to `NOT_APPLIED`, and the late observation ran outside `waitUntil`. |
| GOV-03 | `OPEN` | `FIXED` | `scripts/check-remediation-ledger.ts` — dispositions and evidence paths are machine-readable and a blanket closure sentence is blocked while any row is open. |
| GOV-04 | `OPEN` | `FIXED` | `scripts/check-adapter-inventory.ts` — the gate passed on any single importer; it now requires the four named consumers to resolve their import to the shared primitive and prints the exact set. |
| GOV-05 | `OPEN` | `FIXED` | `config/recipes/frontend-capability-recipes.json` — see the budget note below. |
### The file-transfer bundle budget
`check:optional-recipes:source` failed at the previous baseline too (52,078 >
52,000 gzip bytes), so it was not green before this work either. Duplicate abort
and deadline mechanics were consolidated first: the Image probe and the Resumable
fetch transport now use the shared `abortable-operation` primitive instead of
their own scopes, and four decoders share `src/contracts/exact-snapshot.ts`
rather than each carrying its own descriptor walk. The remainder is the
correctness code the third re-review asked for — exact decoders, late-value
compensators and physical-work registries — so the budget is reset to **54,600**
gzip bytes against a measured **53,810**, rather than the failure being carried
forward as if it were green.
### Regenerated evidence (this commit)
| Command | Exit | Result |
| --- | ---: | --- |
| `corepack pnpm check:types` | 0 | all six projects pass |
| `corepack pnpm check:architecture` | 0 | 290 modules, 879 dependencies; 12 fixtures PASS; allowed PASS / 9 forbidden rejected |
| `corepack pnpm check:adapter-inventory` | 0 | 119 files; 7 shared extensions; 5 shared-abort importers listed |
| `corepack pnpm check:optional-recipes:source` | 0 | file-transfer 53,810 / 54,600 gzip bytes |
| `corepack pnpm exec vitest run tests/unit/browser-rpc tests/unit/realtime` | 0 | 17 files / 234 passed |
| abortable-operation, presigned, resumable, image CDN | 0 | 7 files / 176 passed |
| OPFS, journal, public cache, cursor | 0 | 5 files / 100 passed |
| Service Worker + Web Push | 0 | 6 files / 79 passed |
| `corepack pnpm exec vitest run tests/integration` | 0 | 11 files / 82 passed |
The full `tests/unit` + `tests/integration` run is **1,845 passed / 1,864**,
128 of 129 files green. Every one of the 19 failures is in
`tests/unit/ci-artifact-contract.test.ts` and is the pre-existing sandbox,
cgroup, RLIMIT and `/tmp` permission behaviour already recorded above — the same
file failed identically before this work. No adapter test fails.
## Operational contract review (2026-08-15)
A fourth review looked past the adapter layer at the operational contract:
feature on/off, environment separation, folder boundaries, and which gates were
actually green. It found five red gates and three structural gaps. Every row
below names the defect, not the symptom.
| id | area | disposition | what was actually wrong |
| --- | --- | --- | --- |
| `OPS-01` | release | `FIXED` | `public/` is copied verbatim into `dist/`, so every build — production included — shipped the local runtime document. Runtime config now comes from `config/runtime/<profile>.json`. |
| `OPS-02` | release | `FIXED` | Release coherence proved the artifacts agreed with each other, never that they belonged in production. `FE-GATE-027` refuses an artifact whose `APP_ENV`, auth mode, endpoints or build identity do not match a declared `RELEASE_TARGET`, and refuses an undeclared target outright. |
| `OPS-03` | runtime | `FIXED` | `REQUEST_TIMEOUT_MS` was validated and then never passed to the V3 executor; every operation ran on its contract's own deadline. It is now a ceiling that may tighten a contract, never loosen one. |
| `OPS-04` | build | `FIXED` | `VITE_ROUTER_BASE_PATH` drove the router and the Service Worker scope but not Vite's asset `base`, so a sub-path deployment emitted root-absolute assets. One value now feeds all three. |
| `OPS-05` | provider | `FIXED` | bubblewrap 0.9.0 drops whatever follows the option stream inside an `--args` file, so the sandboxed command was never executed: bwrap printed usage and exited 1. Options stay hidden; the command travels on real argv. |
| `OPS-06` | provider | `FIXED` | The scope wrapper read its liveness pipe through `fs`, a blocking `read(2)` on a pipe the supervisor never closes. `process.exit` deadlocked joining that thread, so a completed provider was reported as a timeout kill. |
| `OPS-07` | release | `FIXED` | `mkdir`/`open` modes were left to the ambient umask, so a hardened runner produced directories it could not enter and handed `tar` a file it could not re-open. |
| `OPS-08` | release | `FIXED` | Promotion cleanup deleted this promotion's exact five through a pinned descriptor and only then noticed the leaf had been substituted, leaving a half-emptied directory a retry could not distinguish from a completed one. |
| `OPS-09` | removability | `FIXED` | The removal fixture was not a repository, had no `.gitignore`, and each removal script kept its own copy-target list that had drifted. Supply-chain generation therefore failed inside every fixture and took the whole provider suite down with it. |
| `OPS-10` | removability | `FIXED` | A platform integration file asserted the reference feature's route ids, so removing the feature left it importing a deleted module. The assertion moved to the feature's own test tree. |
| `OPS-11` | removability | `FIXED` | A removal fixture runs against a deliberately reduced CI contract; the canonical exact-count tests re-imposed the full authority on it and failed the fixture for the reduction it exists to prove. |
| `OPS-12` | browser | `FIXED` | Four browser-capability specs answered capability requests without the `protocol` field the hardened envelope requires, so every capability was refused and the download and part-upload paths asserted against an empty transcript. |
| `OPS-13` | browser | `FIXED` | A refused capability document answered `recovery: NONE`, contradicting both the design record and the vault, which already answers `REISSUE_CAPABILITY`. |
| `OPS-14` | performance | `FIXED` | Playwright matches accessible names by substring, so the navigation entry matched the home page's call to action too; the run died on a strict-mode violation before the first measurement and produced no evidence at all. |
| `OPS-15` | visual | `FIXED` | The platform overview baseline predated the reference routes moving from `integration-defined` to `session-required`, so the only visual gate covering that page failed for its own staleness. |
| `OPS-16` | architecture | `FIXED` | `src/contracts` imported `src/application` for the shared `Result` and the compatibility predicate; neither package owned the shared vocabulary. Both moved down to contracts. |
| `OPS-17` | architecture | `FIXED` | The documented "no adapter depends on another concrete adapter" rule had no executable form, and `diagnostics` imported a guard out of `telemetry`. The guard moved to the adapter kernel and the rule is now enforced with a same-directory backreference. |
| `OPS-18` | architecture | `PARTIAL` | Generic presentation still reads the installed-feature registries. The rule freezes the exact set of modules doing so today; a new edge fails. Lifting the assembly into `bootstrap` is not done. |
| `OPS-19` | documentation | `FIXED` | README and the manual accessibility checklist both claimed six routes while ten were registered, leaving four screens outside the declared manual review scope. The list is now derived from the route registry by `verify:documentation`. |
### Product feature selection (2026-08-15, second pass)
| id | disposition | what changed |
| --- | --- | --- |
| `OPS-20` | `FIXED` | Which features a build contains is now a declared manifest rather than five registries spreading a literal. `VITE_PRODUCT_FEATURES` narrows it at build time; a test fails if a new registry forgets to consult it. |
| `OPS-21` | `FIXED` | `FEATURE_OVERRIDES` in the runtime document takes an installed feature out of service without a rebuild. The router refuses its routes, not just the navigation, so a typed deep link cannot still mount it. |
| `OPS-22` | `FIXED` | Both inputs are subtractive by vocabulary: the override enum has no `ENABLED`, and a build-time selection naming a feature the source tree does not declare is refused rather than ignored. |
| `OPS-23` | `FIXED` | A sandbox that fails to launch now reports why. The supervisor consumed the child's output only to enforce a byte cap and discarded it, so a host restriction surfaced as an unexplained `exit=1`. Lines the sandbox tooling itself emits are kept; provider output is still discarded. |
An env var does **not** shrink the bundle, and the code says so. A static import
cannot be undone by a value, and making the import graph depend on a
configuration string is what §3.5 exists to prevent. Measured: `none` changes
the output by 58 bytes. Physical removal is FE-GATE-020's job.
### Host restriction discovered during this pass
`bwrap --unshare-net` no longer works on this machine:
```
$ printf '%s\0' --unshare-net --ro-bind /usr /usr ... | bwrap --args 3 -- /bin/true
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1
```
That reproduction contains none of this repository's code. Earlier in the same
session the identical sandbox ran to completion, so the restriction became
active partway through. While it holds, 16 of the 108 provider tests cannot run
here — they need a sandbox the kernel will not grant. They are not counted as
green and not counted as product defects; under a host that permits the
namespace the same file was 107/108.
### Still red after this pass
*applies effective aggregate cgroup limits without exposing command or
credentials* was rewritten. It used to read the live process tree with one
`ps` per pid and assert mid-run, which lost a race against a sandbox that now
completes in a few hundred milliseconds; it records the tree from `/proc` every
5ms and asserts on the recording after the run. That restructuring is also what
revealed the host restriction above — the supervisor had been failing to launch
the sandbox and the test was dying on the observation first.
Tests that spawn processes, build archives and sign evidence were given a
30s budget instead of the 10s default sized for pure-JS unit tests. The default
was not raised: that would hide a genuinely hung test.
### FE-GATE-020 after this pass
| fixture | before | after |
| --- | ---: | ---: |
| reference feature | failed before its first assertion | 1,612 pass / 1 fail |
| optional recipe | 39 failures | 1,386 pass / 2 fail |
| browser file + storage | 40 failures | 1,006 pass / 3 fail |
| realtime | not reached | 1,159 pass / 1 fail |
Every remaining failure is one of the three environment-limited tests above.
Lab performance now produces evidence, and that evidence shows the
named-interaction budget missed on this machine (367724ms against 200ms). The
metric measures a full lazy-route navigation while the budget is an
INP-shaped 200ms, so the two do not describe the same thing. No budget was
changed to make this green.
WebKit remains unavailable in this environment (`libevent-2.1-7t64`,
`libavif16` are not installed), so 14 browser-capability specs and the WebKit
E2E project are unverified here. Chromium and Firefox are 28/28 and visual is
5/5.
## Rules for updating this ledger
- A row moves out of `NOT_STARTED` only with a linked red test, its green run, and the commit id.
- `PROMOTION_BLOCKED` rows never become `DEFECT`; they close through an authorized product
selection change with the browser/provider evidence named in the plan.
- Environmental gate failures are copied verbatim and are never claimed as green.
@@ -13,6 +13,24 @@ probe는 현재 `DESIGNED_NOT_IMPLEMENTED`다. 아래 절차에서 이 기능을
자동 조치는 해당 runtime이 구현·조합된 제품에서만 실행한다. 현재 reference
primitive를 coordinator 완료 증거로 사용하지 않는다.
## OPFS 보상 실패와 reconcile (STO-01)
`put()`이 실패했는데 보상 cleanup effect가 확인되지 않으면 runtime은 실패를
`OBJECT_RECONCILE` / `CONFLICT`(recovery `RETRY`)로 보고하고 journal row를 남긴다.
이는 결함이 아니라 설계된 상태다.
1. journal에 `PREPARING` 또는 `FILES_READY` row가 남아 있는지 확인한다. 남아
있다면 staging bytes가 아직 존재할 수 있다는 뜻이다.
2. `maintenance.reconcile()`을 실행한다. reconcile은 같은 exact physical
generation token만 삭제하고, effect가 여전히 `EFFECT_UNKNOWN`이면 journal을
유지한 채 다시 `OBJECT_RECONCILE`을 반환한다.
3. journal row를 수동으로 삭제하지 않는다. row가 사라지면 stale staging을 추적할
근거가 사라지고 quota만 누수된다.
4. OPFS root나 journal database를 통째로 삭제하거나 schema를 downgrade하지
않는다. rollback은 새 v2 write admission을 닫고 v1+v2 reader를 유지하는 것으로
수행한다.
## 1. 공통 원칙
incident 중에도 다음 작업은 금지한다.
@@ -12,6 +12,26 @@ session/account Query lifecycle, strict query policy와 Web Storage v2 lifecycle
현재 `DESIGNED_NOT_IMPLEMENTED`다. 아래 목표 절차를 현재 runtime의 보장으로
해석하지 않는다.
## Public cache staging repair와 offline activation (STO-03 ~ STO-05)
- release marker는 "staging이 끝났다"는 **주장**이고 모든 entry의 존재·digest
증거가 아니다. 같은 manifest로 `stageRelease`를 다시 호출하면 runtime이
candidate를 재검증하고, browser eviction이나 부분 손상이 발견되면 그 owned
candidate만 삭제한 뒤 network에서 다시 stage한다. marker만 보고 성공을
반환하지 않는다.
- 검증 중 abort나 읽기 불가(UNKNOWN)는 stage 성공이 아니며 active pointer를
건드리지 않는다. candidate를 임의로 삭제하지도 않는다.
- `activateRelease``cleanupOwned`는 network I/O가 없다. fetcher 없이도
동작하므로 offline rollback과 quota recovery cleanup이 `UNSUPPORTED`로 막히지
않는다. 두 operation은 Cache Storage와 mutation lock만 요구하고 실패 시
recovery는 `RETRY`다. `stageRelease`만 fetcher를 요구하며 recovery는
`ONLINE_ONLY`다.
- variant를 사용하는 policy(`allowedVaryHeaderNames` 비어 있지 않음)는 반드시
`allowedResponseHeaderNames``vary`를 포함해야 한다. 아니면 composition이
`TypeError`로 즉시 실패한다. 저장된 variant가 같은 key로 충돌하는 상태를 만들지
않기 위한 cross-field invariant다.
## 1. 변경할 수 없는 복구 원칙
- 서버가 server state와 authorization의 source of truth다.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,223 @@
# TechLog UI migration baseline
## Source provenance
- Source path: `/home/donghyeon/workspace/techlog-studio-frontend`
- Inspected: 2026-08-15 (Asia/Seoul)
- Git revision/status: unavailable. The supplied source directory is an exported
checkout with no `.git` metadata; both `git rev-parse HEAD` and
`git status --short` report that it is not a Git repository. The source path
and the byte checksums below are the reproducible provenance available for
this baseline.
## Approved copied assets
| Source and target-relative path | SHA-256 |
| --- | --- |
| `public/favicon.svg` | `e6d2e59b7b5bbb0342e0fb496dfc262decbfe4426bbb7b047aec8d467d1dc6f7` |
| `public/media/fetch-strategy-boundary.svg` | `b07926823ed77fc200f962a1f64a440e130cefa6bed31144b611612af9b02606` |
Only these two SVGs are approved for this baseline. They must remain exact
byte-for-byte copies of the source assets. The evidence key
`fetch-strategy-boundary` resolves to `/media/fetch-strategy-boundary.svg`
with dimensions `1080 × 420`, trigger label `Fetch Join과 Batch Fetch 비교
다이어그램 크게 보기`, and dialog label `Fetch Join과 Batch Fetch의 페이징 경계
확대`.
## Required dependency pins
| Package | Exact version |
| --- | --- |
| `pretendard` | `1.3.9` |
| `@fontsource/ibm-plex-mono` | `5.3.0` |
| `unified` | `11.0.5` |
| `remark-parse` | `11.0.0` |
| `remark-gfm` | `4.0.1` |
| `remark-directive` | `4.0.0` |
No Next.js, Vinext, or Cloudflare package is part of the migration baseline.
## Expected route inventory
| Layout | Route ID | Path |
| --- | --- | --- |
| PUBLIC | `TECH_LOG_HOME` | `/` |
| PUBLIC | `TECH_LOG_EXPLORE` | `/explore` |
| PUBLIC | `TECH_LOG_EXPLORE_KIND` | `/explore/:kind` |
| PUBLIC | `TECH_LOG_CASE` | `/cases/:slug` |
| PUBLIC | `TECH_LOG_REFERENCE` | `/references/:slug` |
| PUBLIC | `TECH_LOG_QUESTION` | `/questions/:slug` |
| PUBLIC | `TECH_LOG_TOPIC` | `/topics/:slug` |
| PUBLIC | `TECH_LOG_PROJECTS` | `/projects` |
| PUBLIC | `TECH_LOG_PROJECT` | `/projects/:slug` |
| PUBLIC | `TECH_LOG_PROJECT_RECORDS` | `/projects/:slug/records` |
| PUBLIC | `TECH_LOG_PROJECT_DECISIONS` | `/projects/:slug/decisions` |
| PUBLIC | `TECH_LOG_PROJECT_ACTIVITY` | `/projects/:slug/activity` |
| PUBLIC | `TECH_LOG_RELEASES` | `/releases` |
| PUBLIC | `TECH_LOG_RELEASE` | `/releases/:version` |
| PUBLIC | `TECH_LOG_PROFILE` | `/profile` |
| PUBLIC | `TECH_LOG_SEARCH` | `/search` |
| STUDIO | `TECH_LOG_STUDIO_HOME` | `/studio` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENTS` | `/studio/documents` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_NEW` | `/studio/documents/new` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_EDIT` | `/studio/documents/:id/edit` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_VALIDATION` | `/studio/documents/:id/validation` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PREVIEW` | `/studio/documents/:id/preview` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PUBLISH` | `/studio/documents/:id/publish` |
| STUDIO | `TECH_LOG_STUDIO_PUBLICATIONS` | `/studio/publications` |
| STUDIO | `TECH_LOG_STUDIO_PUBLICATION_PREVIEW` | `/studio/publications/:publicationEventId/preview` |
| STUDIO | `TECH_LOG_STUDIO_NOT_FOUND` | `/studio/*` |
| PUBLIC | `NOT_FOUND` | `*` |
## Source stylesheet inventory
- `app/globals.css``presentation/styles/globals.css`
`901205054ee96fe15062aaef7ff39c701985fdece15b58f7656776d0b744e607`
- `app/studio.css``presentation/styles/studio.css`
`d0d958baeb55b74796988d8c0967a7c62d211e595224e07912aa1f884e466fdd`
- `app/studio-editor.css``presentation/styles/studio-editor.css`
`ae9a473192d24465021d021cd42a5a84f9bdf4c3af0664537b4e10006ca1f889`
- `components/studio/workflow.module.css`
`presentation/styles/workflow.module.css`
`8eaa478a88ef4d16429c7f6a32bb33631acb47deef0b40838edd0a72748f124e`
- `components/studio/publication-flow.module.css`
`presentation/styles/publication-flow.module.css`
`5b1fdebeb27248b5cebc700b12d15cf11fe70471a394c9dc643a98c4569e8674`
All five source/target pairs passed `cmp -s` and have identical SHA-256
digests. Production parity testing corrected the provisional bootstrap
assumption in the design: the target loads the byte-identical TechLog
`globals.css`, including its first-byte `@import "tailwindcss";`, exactly once
and does not load the starter `theme.css`. This reproduces the source cascade;
the resulting browser comparison is pixel-identical.
## Source-to-target browser parity
The source was copied to `/tmp/techlog-source-parity.I0CBK7` before build and
runtime caches were created. The supplied source directory was used only for
read-only file comparison; it was not edited. The temp-copy Vinext production
server on `4375` and the target `dist/server.mjs` production artifact on `4174`
were captured by one Playwright Chromium instance with two fresh contexts
under identical conditions:
- `ko-KR`, `Asia/Seoul`, light color scheme, reduced motion, device scale 1,
service workers blocked, and a fixed `2026-08-14T01:00:00.000Z` clock;
- 1000-pixel viewport height, full-page screenshots, `document.fonts.ready`,
Pretendard/IBM Plex font checks, zero-duration animation/transition/caret;
- no screenshot masks; exact RGBA pixel comparison plus normalized recursive
product-subtree tags, ordered children, complete classes, relevant
attributes, text/ARIA relationships, response metadata, boot lifecycle, and
console/request failure comparison.
The only attributes omitted by name after concrete diagnostics are React
Router `data-discover`, Next Image `data-nimg`/`decoding`/`srcset`, and Next SSR
`selected`; generated React IDs and CSS-module hashes are normalized by value.
No broad attribute class is omitted.
| Evidence group | Cases |
| --- | ---: |
| 27 canonical routes at 360 and 1440 pixels | 54 |
| Every additional known Public fixture slug/version | 18 |
| Public home breakpoint transitions | 12 |
| Studio screen/state fixtures | 19 |
| Ten unknown Public dynamic shapes at 360 and 1440 pixels | 20 |
| Search/menu/preview/dialog/publication interactions | 7 |
| **Total** | **130** |
Result: 130/130 passed, zero failed, zero different pixels, all recursive
DOM/class/attribute/text/ARIA trees and HTTP response metadata equal, and zero
unexplained source or target console/request errors. The target-only visual
regression suite exercises the same 130 cases against 129 committed PNGs; the
canonical and Studio-state 1440-pixel
`/studio/publications` cases intentionally share one identical snapshot.
Source screenshots were temporary comparison inputs and were not copied into
the target snapshot directory.
The checked clean-checkout command is
`TZ=Asia/Seoul corepack pnpm verify:tech-log-source-parity`; it runs directly
under the installed Node 24 runtime and has no `tsx` dependency. Durable
evidence is committed at
`docs/operations/evidence/tech-log-source-parity.json`. It records 178 source
checksums with tree digest
`6724c2f898eefc62d2fc0ee695bccc3ae61a69c5153ed43c69f2cf99ee45bca5`,
target candidate `3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, build-manifest digest
`3579650faa482f566553c00d8b4a05a05b4f7a1ab93b83e48676289bbcf02984`,
Vite-manifest digest
`cfd583ed7b6c27dce7f47389447608636b6b9df3e28df00c6416674da2c7c46d`,
case inventory digest
`5689bcdb5d5637205cdeb92b7c57f72d99f0b039818b8f90afdde526bbafe0ac`,
and evidence payload digest
`f046047beded19ce468be607dcf99c6b74d4e07323457ec7145c56d2f67d2c79`.
The production source responds to both a missing dynamic slug such as
`/projects/missing-project` and an unmatched path such as
`/definitely-not-a-product-route` with HTTP 404,
`text/plain;charset=UTF-8`, and the exact nine-byte body `Not Found`. The
target production server intentionally removes the Public shell for those
paths and matches that response in direct HTTP and Chromium regressions. Known
Public routes and all known Studio routes remain SPA-served; `/studio/*`
unknown paths preserve the source's in-shell HTML with HTTP 404. This is the
observed production source contract, not a generic runtime error.
## Accessibility review boundary
Automated Chromium `@a11y` coverage passes 29/29. The source-controlled manual
inventory contains exactly the 27 installed route IDs and removes the obsolete
starter records. Every human record remains `pending-manual-review` until one
reviewer evaluates keyboard, focus, modal/error behavior, color, reduced
motion, and screen-reader output against immutable candidate
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`. Therefore
`corepack pnpm review:a11y-manual` intentionally exits nonzero with 27
incomplete records and `release IDs do not match`; neither the manual gate nor
`FE-GATE-009` is represented as passing. The exact completion format is in
`docs/accessibility/manual-checklist.md`, and signed evidence must be committed
separately after it cites the candidate SHA.
## Governed route migration
The migration records 23 TechLog-owned breaking-change evidence IDs with owner
`tech-log-frontend`, an atomic same-release route/runtime/manifest migration,
and rollback to `05e3d50ba01f01c27f257d2e9040c2bc413ea053`. The accepted registry
snapshot digest is
`428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b`;
its approval reason is `Install approved TechLog Public and Studio route
contract`. Final compatibility impact is `none` with no unacknowledged change.
## Restricted-environment test baseline
The exact repository aggregate command was run in the managed workspace:
```bash
corepack pnpm test:all
```
Its fresh staged-candidate runtime-schema phase passed 3 files/40 tests. The
unit phase passed 122 files/1,773 tests and reported 19 failures, all in the
pre-existing `ci-artifact-contract` provider/cgroup, RLIMIT/EMFILE,
restrictive-umask, `/tmp`, timing, and identity environment cases. No
`tests/features/tech-log` test failed. A pre-staging diagnostic run also found
39 `APP_HOME` release-inventory failures because new serving files were not yet
visible to `git ls-files`; staging the complete candidate fixed that test
precondition, and all 39 disappeared. The same run's one guardian aggregate
timeout passed 1/1 in isolation and 21/21 in the fresh staged aggregate.
The failing files were then reproduced in isolation outside that child-process
restriction:
```bash
corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts tests/unit/ci-workflow-generation.test.ts tests/unit/http-scenario-evidence.test.ts
```
Result: 3 files/529 tests, 510 passed and the same 19 environment-only
`ci-artifact-contract` cases failed. The 407 `ci-workflow-generation` and 14
`http-scenario-evidence` tests all passed. These 19 contain no TechLog code or
test. The aggregate phases after unit were also run directly: component 18
files/124 tests, integration 11 files/82 tests under the required child-process
scope, reference feature 4 files/13 tests, and recipes 2 files/17 tests all
passed.
The inventories in this document are human review baselines. Automated
coverage is intentionally limited to the two SVG byte contracts, evidence
asset lookup behavior, package manager frozen-lockfile verification, committed
target visual regressions, and governed registry/release gates. The external
source path is never required by committed CI tests.
@@ -0,0 +1,277 @@
# Template merge — `main``feature/techlog-ui-migration`
Record of how the frontend template sync on `main` was integrated into the
TechLog UI migration branch, and of every decision taken to resolve a conflict.
## Why this direction first
The template sync landed on `main` while the UI migration ran in a worktree.
Two orders were possible.
Merging the feature branch into `main` first would have resolved 15 conflicts
directly on the integration branch: a bad resolution would already be on `main`,
and it would have to be repaired forward, on the branch other work depends on.
Merging `main` into the worktree first keeps the resolution where the UI work
lives. Every gate runs against the resolved tree before anything reaches `main`,
and a resolution that turns out wrong is discarded by resetting one feature
branch. `main` is then only ever fast-forwarded, so it never holds a state that
was not already proved in the worktree.
That is the order used here.
## Starting state
| | |
| --- | --- |
| merge base | `325a2a0` |
| `main` | `bdee07a``chore: sync the frontend template from a0fbafb to 5434760`, 1 commit, 101 files, +3116/448 |
| `feature/techlog-ui-migration` | `0355b64` — 29 commits |
| files changed by `main` | 101 |
| files changed by the feature branch | 423 |
| overlap | 23 |
Rollback refs were created before touching anything:
```
backup/ui-before-template-merge → 0355b64
backup/main-before-ff → bdee07a
```
### Pre-merge baseline on the feature branch
Captured so that a pre-existing failure could not be mistaken for a merge
regression.
| Gate | Result |
| --- | --- |
| `check:types` | pass |
| `lint` | pass |
| `verify:documentation` | `PASS_SCOPED` |
| `tests/unit` + `tests/component` | 1815 passed / 101 failed |
The 101 failures were confined to `tests/unit/ci-artifact-contract.test.ts` (19)
and `tests/unit/ci-workflow-generation.test.ts` (82) — sandbox subprocess gates
that cannot run in this environment.
## Conflicts and how each was decided
15 conflicts. The rule applied throughout: **keep the template's mechanism, keep
the product's content, and never invent a third state that neither branch
would accept.**
### Deletions the UI migration made deliberately (2)
| Path | Decision |
| --- | --- |
| `src/presentation/examples/platform-overview-page.tsx` | deletion kept |
| `tests/visual/.../platform-overview-light-chromium-visual-linux.png` | deletion kept |
`main` modified both; the feature branch deleted them in `c5c8b94`. Nothing on
the branch references either, so the deletion stands.
### `src/features/installed-feature-contracts.ts`
The template introduced a product manifest: registries are composed from
`INSTALLED_PRODUCT_FEATURES` so that narrowing the selection withdraws a
feature's routes, operations, schemas and messages.
The manifest composition is kept for operations, schemas, mappers and
invalidation. The **route registry is deliberately not composed from
`contract.routes`**, because the reference feature still declares
`REFERENCE_RESOURCE_*` routes whose screens this product deleted during the
migration. Reducing over them would register paths with no component behind
them — a typed deep link resolving to nothing.
The registry therefore lists the platform routes (empty on this branch) and
TechLog's. This was caught by running the gates: the first resolution did
compose from `contract.routes`, and `tests/unit/product-features.test.ts`
rejected it.
`ROUTE_FEATURE_OWNER` was narrowed to registered routes for the same reason.
Attributing an unregistered route to a feature claims the kill switch governs
something no router can mount.
### `src/features/installed-feature-runtimes.tsx`
The template gates the reference feature's route codecs and components on the
manifest. This product deleted that feature's presentation layer, so the import
does not resolve and there is nothing to gate. The gating was removed and the
reason recorded in the file; it belongs back the day those screens return.
Consequence: this file no longer references the manifest, so it was moved to the
exempt list in `tests/unit/product-features.test.ts` with the same note.
`tests/component/product-feature-switch.test.tsx` was rewritten to hold the
invariant that still applies here — no registered route without a component —
which is exactly the trap the first resolution fell into.
### `src/features/installed-feature-adapters.ts`
Both the manifest check and TechLog's input are kept. The reference feature's
input stays `Partial` because the manifest may narrow it out; TechLog's is total
because it is this product's own UI and is always installed. A build that
narrows the reference feature out still ships TechLog.
### `src/features/installed-feature-messages.ts`
The template's rationale — message keys stay total so `message()` cannot become
partial — is kept, and TechLog's catalog is merged in on the same terms.
### `src/presentation/routes/app-router.tsx`
The template looked the route definition and runtime component up by id inside
the route element; this branch passes both as props from the grouped route
contract. The prop-driven signature is kept.
The template's **feature kill switch is adopted**: a route whose owning feature
the runtime document disabled renders the disabled surface instead of mounting.
Withdrawing it from navigation alone would leave a working deep link. `getRoute`
was dropped from the imports because the definition arrives as a prop.
### `vite.config.ts`
Two independent additions — TechLog route chunking and `routerBasePath` — both
kept. A brace was lost in the first concatenation and caught by `check:types`.
### `scripts/build-frontend.ts`
Both new steps exist in the merged body, so the header comment was renumbered:
runtime config becomes step 4, the TechLog serving boundary step 7, the release
manifest step 8, and the inline step comments were corrected to match.
### `scripts/test-performance.ts`
The template clicked a navigation link before measuring; this product measures
its own landing route, which `goto` already reached, and has no `targetLabel`.
The click was dropped. The template's lesson was kept as a comment because it
applies to the next click that lands here: Playwright matches accessible names
by substring, so a nav entry can also match a call to action and resolve to two
links — a strict-mode violation that produces no performance evidence at all.
### Derived baselines — recomputed, not chosen (3)
`scripts/contracts/ci-gates.ts`, `tests/unit/task3-selective-integration.test.ts`
and `tests/unit/ci-workflow-generation.test.ts` each pin counts and a digest
describing the gate contract. **Neither side's numbers describe the merged
`config/ci/gates.json`**, so taking either would have been wrong. They were
recomputed from the merged file:
| Value | Result |
| --- | --- |
| canonical gate shape SHA-256 | `5063586d799f51de94c0f0ddaf9b75e180825bba5051bc309550425013ea81ef` |
| gates | 27 |
| commands | 82 |
| command references | 94 |
| evidence artifact references | 107 |
| artifacts | 128 (126 product + 2 from the template) |
### `README.md`, `docs/accessibility/manual-checklist.md`
The template enumerated its own example routes. Replacing them with generic
prose broke `verify:documentation`, which requires both documents to name every
installed route id — a rule the template sync itself introduced. Both documents
now enumerate this product's 27 routes.
## Result
Merge commit parents: `0355b64` (UI) and `bdee07a` (template).
### Gates on the merged tree
| Gate | Result |
| --- | --- |
| `check:types` | pass (6 projects) |
| `lint` | pass, `--max-warnings=0` |
| `check:architecture` | 399 modules, 1232 dependencies, 12 fixtures pass |
| `check:adapter-inventory` | 120 files, 5 importers of the shared abort primitive |
| `check:remediation-ledger` | 25 dispositions, 0 open |
| `check:diagnostics` | 8 diagnostics, 5 telemetry producers |
| `check:i18n` | 197 keys, 4 locales |
| `check:design-system` | 48 tokens |
| `verify:documentation` | `PASS_SCOPED` |
| `test:integration` | 81 passed |
| `test:recipes` | 17 passed |
| `test:runtime-schema` | 40 passed |
| `tests/unit` + `tests/component` | 1851 passed / 98 failed |
The 98 failures are the same two sandbox files as the baseline —
`ci-workflow-generation` (82) and `ci-artifact-contract` (16, down from 19). No
file fails that did not fail before the merge.
## Mistake made during this merge
`git stash` was run inside the worktree while the merge was still in progress,
to compare a gate against the pre-merge tree. That removed `MERGE_HEAD`: the
resolved content survived, but git no longer knew a merge was underway, and
committing then would have produced a single-parent commit — leaving `main` off
the ancestry and breaking the fast-forward that step 2 depends on. `MERGE_HEAD`
was restored to `bdee07a` before committing, and the resulting commit has both
parents.
To compare against a pre-merge state, use a separate checkout rather than
stashing an in-progress merge.
## Correction after review
Two of the resolutions above were wrong, and were fixed in a follow-up commit.
The template's demonstration screens — `platform-overview-page`, the UI and
state galleries, the auth example and the reference feature's screens — exist to
explain the template. A product replaces them with its domain, and deleting them
is the expected end state, not a regression. Two gates were nevertheless coupled
to them, and the first resolution accommodated that coupling instead of fixing
it.
### `tests/unit/product-features.test.ts` — a hard-coded exemption became a rule
The guard requires every installed registry to compose from the manifest.
`installed-feature-runtimes.tsx` was added to its exempt list once the reference
feature's presentation layer was gone. That silenced the guard for that file
permanently.
It now derives its own scope: a registry must gate on the manifest **when it
imports a module belonging to a manifest-declared feature**. A product whose
registries compose only its own domain drops out of the rule honestly, and the
guard fires again the moment a declared feature is imported without gating —
verified by removing the manifest reference from
`installed-feature-adapters.ts` and watching the guard fail. A counter asserts
the sweep is still watching at least one file, so an empty scope cannot pass
silently.
### `tests/component/product-feature-switch.test.tsx` — coverage restored
The end-to-end kill-switch assertions were replaced with composition checks
because the screens they rendered were gone. The mechanism under test is the
ownership lookup plus `isFeatureActive`, which has nothing to do with which
screens ship, so **the ownership map is now the fixture**: one real registered
route is attributed to a real installed feature, and the router, shell,
components and codecs are all the product's own. The deep-link half is asserted
end to end again.
### What that restoration exposed
The navigation-withdrawal half **is not implemented in this product**. It lives
in the template's `PrimaryNavigation`, and this product does not render the
template's `AppShell` at all — the public site header is a hand-written list of
paths in `src/features/tech-log/presentation/public/components/site-header.tsx`,
and the studio has its own shell.
So a disabled feature's route is refused by the router but its link would still
be advertised. That is harmless only while no feature-owned route is navigable,
which is true today and is now asserted. If that assertion fails, the header has
to consult `ROUTE_FEATURE_OWNER` — or navigation has to move back onto
`NAVIGATION_ROUTES` — before the route ships.
## Follow-ups this merge deliberately did not decide
1. **TechLog is outside the product manifest.** It is composed directly rather
than as a `SelectableProductFeature`, so the runtime kill switch does not
govern it. That is defensible — a product's own domain is not an optional
feature — but it means the switch governs nothing user-visible today.
2. **The reference feature declares routes it cannot serve.** Its screens were
deleted with the rest of the demonstration UI, but its contract still
declares `REFERENCE_RESOURCE_*` routes. Either the declarations go, or the
feature does. `TechLog` does not import it (`git grep reference-feature --
src/features/tech-log` is empty), so removing it is a live option and
FE-GATE-020 exists to prove it can be removed.
3. **The public site header is not feature-aware.** See above.
@@ -0,0 +1,990 @@
# Network and state adapters implementation review
- Review date: 2026-08-13 (Asia/Seoul)
- Reviewed revision: 4dc033cf33a5b6173bbf960d5eb464a406dc4c92
- Mode: code review only; no implementation source was changed
- Primary scope: src/adapters/http, auth, query-cache, cross-context-invalidation, platform, diagnostics, telemetry
- Traced boundaries: corresponding contracts, application ports, bootstrap composition, reference feature adapters, tests, architecture decisions, and operating documentation
## 1. Outcome and priority
No Critical issue was found. Five High findings are implementation blockers or near-term correctness/security work:
1. N-01: the production V3 HTTP observation is always rejected by the diagnostics projector, and the V3 path never emits terminal-failure telemetry.
2. N-02: V3 declares authProfileId but neither composes nor enforces a profile; a credential collaborator can change transport-owned Accept and credentials, while a declared bearer profile can send no Authorization header.
3. N-03: after a command attempt has been dispatched, a retry-time final-invariant fence can downgrade MAYBE_APPLIED to NOT_STARTED.
4. N-04: telemetry dispose only removes pagehide; queued callbacks, future emit calls, and in-flight delivery survive runtime teardown.
5. N-05: the conditional-validator key codec is delimiter-ambiguous and lets two valid bindings overwrite each other. It is not composed into HTTP yet, so this is a rollout blocker rather than a current request-path incident.
The current reference feature uses V3. The older createHttpClient path remains exported and is still the documented rollback/compatibility seam, so its replay and cancellation defects cannot be dismissed as dead code.
## 2. Method, severity, and confidence
Severity:
| Level | Meaning |
| --- | --- |
| Critical | immediate broad confidentiality/integrity loss, arbitrary execution, or unrecoverable state corruption |
| High | security boundary bypass, wrong command-effect verdict, silent loss of required production evidence, or unsafe replay/rollout blocker |
| Medium | bounded correctness, cancellation, cleanup, or cross-context isolation defect with a narrower activation condition |
| Low | hardening or contract/documentation mismatch without a demonstrated material product failure |
Confidence:
| Level | Meaning |
| --- | --- |
| Very high | direct control/data-flow proof and a minimal failing reproduction |
| High | direct code proof and aligned contract/documentation evidence |
| Medium | implementation evidence exists but product requirement or browser/provider behavior must be selected |
| Low | hypothesis requiring characterization before acceptance |
Validation performed without retaining test changes:
- A temporary five-case Vitest characterization was added, run, and removed. All five expected-correct assertions failed: V3 diagnostic projection, telemetry-after-dispose, conditional-validator collision, credential transport ownership, and retry-time effect preservation.
- Related baseline: 21 test files, 144 tests passed.
- Static producer check: corepack pnpm check:diagnostics passed with “8 diagnostics and 5 telemetry producers”.
- The temporary test was deleted and the implementation worktree was clean before this report was written. Other agents later created unrelated docs/reviews entries; this review does not modify them.
This distinction matters: the green suite proves current intended behaviors, while the five failures identify missing assertions rather than contradicting existing passing tests.
## 3. Complete primary-scope inventory
All 24 files below were read in full.
| File | Responsibility | Direct dependencies / consumers | Review disposition |
| --- | --- | --- | --- |
| src/adapters/auth/external-session-adapter.ts | Adapts the external session owner to AuthSessionPort; validates credential header names and values; supplies demo, anonymous, and unavailable variants | application/ports/auth-session-port.ts; bootstrap/runtime-adapters.ts; HTTP V2/V3 | Preserve token opacity and allowlist. Modify for cooperative cancellation and required-profile header enforcement. |
| src/adapters/cross-context-invalidation/browser-cross-context-host.ts | Safely captures browser BroadcastChannel, localStorage, storage events, and secure random capabilities | contracts/cache-invalidation.ts; browser-cross-context-invalidation.ts; runtime-adapters.ts | Preserve fail-closed capability capture. Modify to capture one localStorage identity and validate native StorageEvent.storageArea. |
| src/adapters/cross-context-invalidation/browser-cross-context-invalidation.ts | Versioned invalidate-only wire protocol, BroadcastChannel/storage fallback, TTL, dedupe, per-source sequence/gap detection, bounded tracking, subscription and close | contracts/cache-invalidation.ts; host adapter; TanStack coordinator | Preserve closed envelope, bounded maps, and idempotent close. Modify exact storage-area admission; dual-transport fan-out is a separate product choice. |
| src/adapters/cross-context-invalidation/index.ts | Public barrel for host/runtime types and constructors | bootstrap and query-cache coordinator | Modify exports only if the storage event facade type changes. |
| src/adapters/diagnostics/bounded-diagnostics.ts | Bounded in-memory diagnostics projection/sink and safe pre-mount boot evidence | DiagnosticsPort; contracts/diagnostics.ts and telemetry.ts; bootstrap | Preserve fail-isolated projection and cloning. Harden non-finite capacity. V3 producer fix belongs primarily in bootstrap. |
| src/adapters/http/bounded-body-reader.ts | Declared-length and streamed byte ceilings, stream cancellation/release isolation, forbidden-body probing, strict UTF-8/JSON decode | V3 executor; bounded-body-reader tests | Keep as the single bounded response primitive. It already contains the cleanup behavior missing from bounded-json.ts. |
| src/adapters/http/bounded-json.ts | Legacy response stream reader and JSON decoder | legacy client.ts | Replace internals with bounded-body-reader delegation, then remove with V2 retirement. Current cancel/release failures can reject. |
| src/adapters/http/client.ts | Legacy/V2 operation lookup, profiles, auth recovery, retries, total deadline, response validation/mapping, diagnostics and telemetry | legacy contracts and ports; runtime-adapters createRuntimeHttpClient | Compatibility-only but exported. Fix empty keyed replay, cooperative auth cancellation, and common body-reader use before relying on it for rollback. Deprecate after callers are migrated. |
| src/adapters/http/http-contract-bridge.ts | V3 request projection, URL/body bounds, credential patch type, final request invariant | external-contract-runtime.ts; V3 executor | Preserve descriptor-owned request projection. Change credential authority: patch cannot own credentials or transport headers; final invariant must compare exact resolved profile. |
| src/adapters/http/http-effect-certainty.ts | Converts physical-attempt state and problem descriptors into mutation certainty/UI projection | V3 executor; contracts | Preserve explicit certainty vocabulary. Add a monotonic logical-execution certainty join used across retries. |
| src/adapters/http/http-execution-v3.ts | Descriptor-driven V3 lifetime: validation, projection, credentials, deadline, retry, fetch, response admission, effect verdict and observation | external contracts, scope, mutation intent, bridge, bounded reader, certainty, retry policy | Main correction site for N-01, N-02, N-03 and cooperative cancellation. Keep one retry authority and closed result union. |
| src/adapters/http/request-builder.ts | Legacy path/query construction and origin/base-prefix checks | legacy client; ApiOperation | Keep while V2 remains. Do not reuse it to weaken V3 descriptor projection. |
| src/adapters/http/resource-mapper.ts | Thin legacy operation-payload mapper delegation | boundary-mapper; legacy client | No standalone defect. Remove only with V2, not as part of the correctness patch. |
| src/adapters/http/retry-policy.ts | Legacy retry decision/backoff plus parseRetryAfter reused by V3 | legacy client and V3 executor | Keep deterministic parse/backoff seam. V2 must additionally prove a valid key before keyed replay. |
| src/adapters/http/schema-registry.ts | Legacy Zod envelope/request/payload validation and clone | legacy client/tests | No standalone defect. Remains V2-only and should not be merged with installed external validators. |
| src/adapters/platform/browser-lifecycle.ts | Single owner of visibility/network/focus/page/beforeunload listeners and lifecycle snapshots | optional-runtime-host.ts | Preserve centralized listener ownership and idempotent dispose. Clarify or redesign dirty-source attachment semantics; selection item O-02. |
| src/adapters/platform/browser-mutation-intent-factory.ts | Secure intent/idempotency UUIDs plus monotonic creation time, normalized by defineMutationIntent | MutationIntentFactory port; bootstrap | Keep. Share one idempotency-key validator so external inputs and generated values have identical bounds. |
| src/adapters/platform/system-clock.ts | Wall clock and abortable sleep with listener/timer cleanup | ClockPort; legacy HTTP | Keep. Existing unit test covers abort cleanup behavior. |
| src/adapters/query-cache/conditional-validator-store.ts | In-memory ETag CAS sidecar keyed by scope, definition, identity, representation and cache revision | bootstrap scope reset; future conditional HTTP/query join | Fix tuple codec before composition. Preserve validator grammar, generation/revision checks and bounded capacity. |
| src/adapters/query-cache/cursor-pagination-runtime.ts | Bounded cursor chain, page/snapshot/loop/item/byte validation | cursor pagination contract; currently available but not composed | Add post-await abort admission or an abort race. Current pre-await-only check can admit a late page. |
| src/adapters/query-cache/server-state-scope-runtime.ts | Synchronous session-generation fence, ordered reset participants, cache reset, identity replacement and lifecycle notifications | AuthSessionPort, query invalidation, scope contract; bootstrap | Keep synchronous FENCED-before-await design. Consider async shutdown only as O-03; no current stale-generation admission was found. |
| src/adapters/query-cache/tanstack-cache-coordinator.ts | Maps registry topics to query namespace invalidation, coalesces remote hints, defers under mutation leases, resets/disposes | TanStack Query, invalidation contracts, cross-context runtime | Keep invalidate-only remote authority, generation guard, reset serialization and bounded registry validation. Add teardown characterization if dispose becomes async. |
| src/adapters/query-cache/tanstack-query-cache.ts | Creates QueryClient defaults and QueryCachePort read/write/invalidate adapter with diagnostics | TanStack Query, QueryCachePort, errors/diagnostics | Keep retry disabled and clone-on-write. Clone-on-read is an optional port-semantics decision, not a confirmed production defect. |
| src/adapters/telemetry/best-effort-telemetry.ts | Allowlisted bounded oldest-drop telemetry queue, scheduled/pagehide flush, sink isolation and evidence | TelemetryPort, telemetry/diagnostic contracts, bootstrap | Add terminal lifecycle state, joined flush promise and in-flight abort; ensure runtime composition disposes it. |
## 4. Traced boundary inventory
### Application ports
| File | Relevant contract |
| --- | --- |
| src/application/ports/auth-session-port.ts | Session state, credential patch, recovery; currently has no AbortSignal/deadline context. |
| src/application/ports/query-cache-port.ts | Closed read/write/invalidate result; value is unknown and read mutability is unspecified. |
| src/application/ports/clock-port.ts | Time and abortable sleep. |
| src/application/ports/diagnostics-port.ts | Non-throwing logical diagnostics producer boundary. |
| src/application/ports/telemetry-port.ts | Fire-and-forget semantic event emission. |
| src/application/ports/mutation-intent-factory.ts | Intent identity and keyed-command creation. |
| src/application/result.ts | Closed application result used by pagination and feature projection. |
### Contracts
Reviewed: server-state-scope.ts, cursor-pagination.ts, diagnostic-buckets.ts, mutation-intent.ts, boundary-mapper.ts, rest-profiles.ts, api-operations.ts, errors.ts, diagnostics.ts, telemetry.ts, query-invalidation.ts, query-keys.ts, cache-invalidation.ts, and external-contract-runtime.ts.
Key joins:
- external-contract-runtime.ts:117-148 declares authProfileId but validates only non-empty text at 300-344.
- rest-profiles.ts:11-37 already provides the profile/strategy shape and exact credentials mode used by V2.
- diagnostics.ts:22-42 is a closed context allowlist and 83-99 rejects the whole record on an unknown key.
- telemetry.ts defines api.request.failed required attributes: error_kind, http_status_group, attempt_count_bucket, and route_id.
- server-state-scope.ts makes synchronous signal abortion/isCurrent the generation admission boundary.
- cache-invalidation.ts supplies the closed wire grammar used by the cross-context runtime.
### Bootstrap and feature path
Reviewed: runtime-adapters.ts, server-state-generation-store.ts, create-runtime-composition.ts, composition-root.ts, runtime-application.tsx, main.tsx, optional-runtime-host.ts, installed-contract-contributions.ts, installed-feature-adapters.ts, reference create-reference-feature-input.ts, reference-http-gateway.ts, reference feature contract contribution, reference feature API, application-query.ts, and server-state-generation-provider.tsx.
Production request flow:
reference-http-gateway (has routeId)
-> createReferenceFeatureInstalledInput (drops routeId)
-> runtime-adapters contractOperations
-> createContractHttpExecutor (V3)
-> runtime-adapters observe
-> bounded diagnostics projector
The legacy createRuntimeHttpClient is still exported at runtime-adapters.ts:142-163 but is not the installed reference feature request path.
## 5. Confirmed defects
### N-01 — V3 HTTP diagnostics are silently dropped and terminal telemetry is absent
- Severity: High
- Confidence: Very high
- Activation: current production reference-feature V3 path
Evidence:
- http-execution-v3.ts:150-155 defines an observation with diagnosticsOperation, outcome, attempts and certainty.
- http-execution-v3.ts:354-365 emits that shape exactly once.
- runtime-adapters.ts:331-343 maps attempts and certainty as literal context keys.
- diagnostics.ts:22-42 allows attempt_count_bucket but not attempts or certainty.
- diagnostics.ts:83-99 rejects the complete diagnostic on the first unknown context key.
- reference-http-gateway.ts:14-42 and 69-103 constructs a low-cardinality routeId.
- create-reference-feature-input.ts:41-54 forwards signal and intent but discards routeId.
- runtime-adapters.ts:331-347 has no V3 telemetry emit at all.
- VD-07 lines 36-39 requires exactly one logical HTTP diagnostic and exactly one terminal non-abort failure telemetry event.
Minimal reproduction:
Input was the exact runtime-adapters V3 record context:
{
operation_id: "reference.list",
outcome: "TRANSPORT_FAILURE",
attempts: 2,
certainty: "TIMEOUT"
}
Expected projectDiagnosticRecord(...).success true; actual false.
Impact:
- Success, retry recovery, failure and cancellation on the installed V3 feature leave no HTTP diagnostic record.
- V3 terminal failures leave no api.request.failed event even when telemetry is enabled.
- check:diagnostics remains green because it checks producer presence/source policy, not whether the concrete producer output passes the projector.
Required decision:
- Observation is a safe typed internal record, not an arbitrary context map.
- routeId is required at the installed operation-executor boundary.
- Raw attempt count/duration/status stay internal; only buckets reach diagnostics/telemetry.
- Cancellation and scope-fence outcomes produce diagnostics once but never api.request.failed.
- Diagnostics/telemetry failures remain unable to affect the HTTP outcome.
Proposed signature:
export type HttpExecutionObservation = Readonly<{
routeId: string;
operationId: string;
diagnosticsOperation: string;
outcome: HttpExecutionOutcome<unknown, unknown>["kind"];
errorKind: string;
status?: number;
attemptCount: number;
durationMs: number;
effect: HttpEffectCertainty;
cancellationOwner?: CancellationOwner;
}>;
export interface HttpExecutionContext {
readonly routeId: string;
readonly signal?: AbortSignal;
readonly scope: CacheScopeSnapshot;
readonly intent?: MutationIntent;
}
Projection in runtime-adapters:
diagnostics.record({
eventId: "http.request.completed",
context: {
route_id: observation.routeId,
operation_id: observation.operationId,
operation: observation.diagnosticsOperation,
outcome: observation.outcome,
error_kind: observation.errorKind,
http_status_group: statusGroup(observation.status),
attempt_count_bucket: attemptBucket(observation.attemptCount),
duration_bucket: durationBucket(observation.durationMs)
}
});
For terminal non-abort failures, emit api.request.failed using the same safe route/operation/status/attempt/duration fields. Do not add attempts or certainty as unregistered context. If product operators need effect certainty, add the explicit effect_certainty key and a closed value policy to both contracts and ADR; do not pass the current free string.
### N-02 — V3 auth profile is declarative only; credential code can alter transport policy
- Severity: High
- Confidence: Very high for generic V3 authority violation; High for current missing-bearer behavior
- Activation: current bootstrap permits a bearer-declared demo request with an empty patch; arbitrary Accept/credentials requires a custom/defective credential collaborator
Evidence:
- external-contract-runtime.ts:117-125 contains authProfileId.
- external-contract-runtime.ts:300-344 checks only non-empty identity, not registry existence or coherence.
- runtime-adapters.ts:302-326 receives operation.authProfileId but ignores it, always returns credentials: omit, and accepts an authenticated empty patch.
- http-contract-bridge.ts:14-22 lets the credential patch select any RequestCredentials.
- http-execution-v3.ts:475-483 rejects only idempotency-key.
- http-execution-v3.ts:486-489 spreads credential headers after transport-owned Accept, so patch Accept wins.
- http-contract-bridge.ts:253-273 only checks that credentials is one of three valid Fetch values and generally allows Accept/Content-Type; it does not prove profile equality or header ownership.
- reference contribution lines 133-141, 177-185, 224-236 declares REFERENCE_EXTERNAL_BEARER for all operations.
- demo-session credentialPatch is empty at external-session-adapter.ts:109, yet authenticated demo requests are sent.
- VD-23 lines 211-259 says transport owns Accept/Content-Type and an auth profile exact-fixes Fetch credentials.
- The existing V2 profile registry at rest-profiles.ts:11-37 and 77-106 is a working local pattern.
Minimal reproduction:
A credential collaborator returned:
{
kind: "READY",
headers: { Accept: "text/plain" },
credentials: "include"
}
The request completed successfully and fetch observed Accept text/plain and credentials include. Expected fetch count was zero or transport-owned application/json/omit.
Current mitigation and residual issue:
- external-session-adapter.ts:18-46 currently restricts the real owner to authorization and x-csrf-token, so the Accept injection is blocked in this bootstrap path.
- That does not restore generic executor authority, and required Authorization is not checked. Empty bearer remains possible in the current demo path.
- Therefore do not characterize this as arbitrary external-owner header injection in the current bootstrap; characterize it as an executor contract violation plus a current profile-completeness failure.
Required decision:
- Reuse the existing Profile/Strategy registry; do not introduce a general interceptor chain.
- The resolved profile, not the credential patch, owns credentials.
- Credential patch contains only a typed, runtime-validated subset of credential headers.
- A bearer profile requires authorization; an anonymous profile permits none.
- Unknown/incoherent profiles fail during composition. Missing required headers or extra headers return AUTH_INTEGRATION_FAILURE with effect NOT_STARTED and fetch count zero.
- UNAUTHENTICATED remains a user/session state, not an integration/configuration error.
Proposed signatures:
export type CredentialHeaderName =
| "authorization"
| "x-csrf-token"
| "x-tenant-context";
export type RestAuthProfile = Readonly<{
authProfileId: string;
transport: "ANONYMOUS" | "BEARER_HEADER" | "SAME_ORIGIN_COOKIE";
credentials: "omit" | "same-origin" | "include";
allowedCredentialHeaders: readonly CredentialHeaderName[];
requiredCredentialHeaders: readonly CredentialHeaderName[];
}>;
export type CredentialPatchOutcome =
| Readonly<{
kind: "READY";
headers: Readonly<Partial<Record<CredentialHeaderName, string>>>;
}>
| Readonly<{ kind: "UNAUTHENTICATED" }>
| Readonly<{ kind: "UNAVAILABLE" }>
| Readonly<{ kind: "SCOPE_FENCED" }>;
attachCredentials(
operation: Readonly<{
operationId: string;
authProfileId: string;
method: string;
}>,
context: Readonly<{ signal: AbortSignal }>
): Promise<CredentialPatchOutcome> | CredentialPatchOutcome;
The executor dependency receives a validated ReadonlyMap<string, RestAuthProfile>. Final invariant compares init.credentials to the selected profile and rejects missing/extra credential headers.
Demo migration must be explicit. Recommended repository choice: allow createDemoSessionAdapter to receive a demo credential patch from bootstrap and supply a fixed non-secret Authorization marker only in AUTH_MODE=demo; keep REFERENCE_EXTERNAL_BEARER strict. Do not silently weaken the bearer profile to make tests pass. If a product backend wants anonymous demo calls, it needs a distinct anonymous contract/profile selected at composition.
### N-03 — retry-time scope fence downgrades a previously dispatched command to NOT_STARTED
- Severity: High
- Confidence: Very high
- Activation: latent for a future IDEMPOTENT command with retryBudget greater than zero; the current reference create is KEYED with retryBudget zero
Evidence:
- After response admission, attemptState becomes SETTLED at http-execution-v3.ts:648-656.
- A retry continues at 657-692.
- The next iteration checks scope at 511-516 and uses current attemptState, which still yields MAYBE_APPLIED.
- There is a second scope check inside final invariants at 554-563.
- If the scope changes between those two checks, lines 568-575 call preDispatchEffect(isCommand), returning NOT_STARTED and forgetting the prior attempt.
- Deep design lines 1683-1690 says dispatch followed by timeout/network/abort/body loss is MAYBE_APPLIED.
Minimal reproduction:
- Contract: commandEffect non-null, retrySemantics IDEMPOTENT, retryBudget 1.
- Attempt 1: fetch returns 429.
- Sleep resolves.
- Scope is current at retry-loop entry and false at final invariant.
- Expected SCOPE_FENCED with MAYBE_APPLIED.
- Actual SCOPE_FENCED with NOT_STARTED.
Root cause:
PhysicalAttemptState is being used as both current-attempt state and logical-execution history. Reset/final-invariant code reasons only about “this retry has not sent” and loses “a previous physical attempt was sent”.
Required decision:
Maintain a monotonic logical certainty accumulator for the whole execution. A new unsent retry cannot lower prior MAYBE_APPLIED. Final-invariant failures use the joined logical certainty; the first attempt can still return NOT_STARTED.
Proposed helper:
export function joinMutationEffectCertainty(
current: MutationEffectCertainty,
observed: MutationEffectCertainty
): MutationEffectCertainty;
Join rules:
- MAYBE_APPLIED dominates NOT_STARTED and NOT_APPLIED.
- APPLIED_CONFIRMED is terminal and cannot enter an automatic retry.
- NOT_APPLIED dominates NOT_STARTED for internal history.
- Query operations remain NOT_APPLICABLE and do not use the mutation lattice.
Also check caller/scope/deadline ownership after every awaited admission and before returning success. A separately named characterization should decide the response-completed-versus-caller-abort race; do not fold an unverified race rule into this patch without a test.
### N-04 — telemetry continues work after dispose and composition never disposes it
- Severity: High
- Confidence: Very high
- Activation: current when telemetry is enabled; no-op telemetry is unaffected
Evidence:
- best-effort-telemetry.ts:95-101 schedules a callback that always calls flush.
- emit at 104-125 has no disposed check.
- flush at 127-147 has no disposed check or in-flight AbortController.
- dispose at 154-156 removes only pagehide.
- runtime-adapters.ts:412-416 clears validators/scope/generation but omits telemetry.dispose.
- create-runtime-composition.ts:60-66 calls infrastructure.dispose after optional shutdown, so the omission reaches application teardown.
- flush at 127-130 returns an already-resolved promise when another flush is active, so await adapter.flush does not mean “the active delivery has settled”.
Minimal reproduction:
- Queue one valid api.request.failed with a captured scheduler callback.
- Call dispose.
- Run the captured callback and call emit again.
- Expected no fetch and pendingCount zero.
- Actual one fetch; later emit is also accepted.
Impact:
- HMR/test/runtime teardown can send queued or future events after the owning composition is gone.
- In-flight work has no cancellation owner.
- This is a lifecycle/privacy contract defect even though delivery is best-effort.
Required decision:
Use a small terminal lifecycle state, not a durable queue:
type TelemetryLifecycle = "ACTIVE" | "DISPOSED";
flush(): Promise<void>;
dispose(): void;
Semantics:
- emit after dispose is a no-op.
- dispose removes pagehide, clears queued events, invalidates scheduled callbacks, and aborts the current sink request.
- flush joins and returns the active flush promise.
- completion of a sink that ignored abort cannot reschedule or update post-dispose delivery state.
- disposal drops data silently; it must not recursively emit a drop event while shutting down.
- runtime infrastructure.dispose calls telemetry.dispose before destroying diagnostics/state dependencies.
A separate async shutdown or persistent retry queue is unnecessary for current best-effort policy.
### N-05 — conditional-validator composite key is collision-prone
- Severity: High when composed; current effective risk Medium / activation blocker
- Confidence: Very high
- Activation: docs classify sidecar AVAILABLE_NOT_COMPOSED; bootstrap creates/clears it but HTTP does not use it
Evidence:
- conditional-validator-store.ts:43-58 joins four unescaped components with colon.
- identityToken permits colon at 47.
- scope fingerprint permits colon in server-state-scope-runtime.ts:196-200.
- definitionId is only checked for truthiness at line 46.
- architecture status at api-contract-schema-mapper-and-server-state.md:108 explicitly says AVAILABLE_NOT_COMPOSED.
Collision using valid values and the same scope/version:
A: definitionId = "resource:detail"
identityToken = "identity-token-00000001"
B: definitionId = "resource"
identityToken = "detail:identity-token-00000001"
Both encode to the same string. Installing B overwrites A; prepare(A) returns Bs ETag.
Impact after composition:
A validator from one definition could be sent for another and a 304 could admit the wrong cached representation/revision relationship.
Required decision:
Use an injective deterministic tuple codec, not a repository abstraction. JSON.stringify of a validated fixed tuple is sufficient:
type ConditionalValidatorKeyTuple = readonly [
scopeFingerprint: string,
definitionId: string,
identityToken: string,
representationVersion: number
];
Validate and byte-bound definitionId and fingerprint at this trust boundary. No public store API change or persisted-data migration is needed because the store is in-memory and not composed into HTTP yet.
### N-06 — legacy keyed commands can automatically retry with no Idempotency-Key
- Severity: High on the compatibility/rollback path
- Confidence: High
- Activation: exported createRuntimeHttpClient/createHttpClient; not the current installed reference path
Evidence:
- client.ts:241-246 uses nullish coalescing, so caller value "" is retained rather than replaced.
- client.ts:534 sets Idempotency-Key only if the value is truthy.
- retry-policy.ts:45-68 allows both safe and keyed retries.
- Therefore a keyed command with explicit empty key can replay after a retryable response while sending no key.
- The existing keyed integration test uses a non-empty logical-command value; there is no invalid-key case.
- VD-23 lines 691-703 permits V1 fallback only if hardening remains.
Required decision:
Export one defineIdempotencyKey validator from mutation-intent.ts and use it in both V2 and V3. Reject empty, whitespace-only, control-character, and over-byte-budget keys before credentials, timers, or fetch. Do not trim or silently regenerate a caller-supplied invalid value. Return VALIDATION_REJECTED / IDEMPOTENCY_KEY_INVALID, attempt count zero.
### N-07 — legacy total deadline does not bound or cancel credential attachment
- Severity: High on the compatibility/rollback path
- Confidence: High
- Activation: auth-required V2 operation with a non-cooperative external owner
Evidence:
- client.ts:517-526 creates the attempt controller/timer.
- client.ts:570-586 awaits authSession.credentialPatch directly.
- auth-session-port.ts:15-27 exposes no signal/deadline to credentialPatch.
- If the owner never settles, aborting the attempt controller does not settle the await, so execute can exceed its total deadline indefinitely.
- Recovery is raced at client.ts:297-324 and 666-697, but authSession.recover itself receives no signal; late owner work can continue.
- V3 has the better local waiting pattern at http-execution-v3.ts:427-466, although its underlying credential work is not cooperatively signaled either.
- VD-23 lines 339-349 explicitly includes credential/recovery in total deadline and requires auth waiter cleanup.
Proposed compatible port extension:
export type AuthOperationContext = Readonly<{
signal: AbortSignal;
deadlineAtMonotonicMs: number;
}>;
credentialPatch(
binding: CredentialRequestBinding,
context?: AuthOperationContext
): Promise<CredentialPatch>;
recover(context?: AuthOperationContext):
Promise<"restored" | "no-session">;
Make context optional for one release to preserve existing owner implementations, but both clients must race owner promises against the lifetime signal immediately. Extend ExternalSessionOwner attachCredential/recoverSession the same way, pass the context through, and ignore all late completions. In the following breaking release, require the context from external owners.
Error semantics:
- deadline owner: REQUEST_TIMEOUT / TIMEOUT.
- caller owner: REQUEST_ABORTED / CANCELLED.
- scope owner in V3: ABORTED_BY_SCOPE or SCOPE_FENCED, preserving logical effect.
- ordinary owner rejection: AUTH_INTEGRATION_FAILURE.
- none of these paths may fetch.
### N-08 — legacy bounded JSON can reject and leave response cleanup inconsistent
- Severity: Medium
- Confidence: High
- Activation: V2 response path
Evidence:
- bounded-json.ts:9-12 awaits response.body.cancel outside a catch.
- lines 24-26 awaits reader.cancel; a rejection escapes the closed result.
- lines 30-33 does not cancel after reader failure and releaseLock can throw.
- client.ts:714-723 returns immediately on content-type mismatch without cancelling the response body.
- bounded-body-reader.ts:31-76 and 148-153 already isolates cancellation/release errors and is well tested.
Required decision:
Make bounded-body-reader the common primitive. Keep readBoundedJsons public return codes temporarily by delegating and mapping:
- RESPONSE_TOO_LARGE -> RESPONSE_BODY_LIMIT.
- UTF8_INVALID / JSON_INVALID / RESPONSE_STREAM_FAILURE -> MALFORMED_JSON for legacy compatibility.
Cancel on V2 content-type mismatch. Do not maintain two stream-reader strategies.
### N-09 — localStorage fallback cannot prove the event came from localStorage
- Severity: Medium
- Confidence: Very high
- Activation: storage fallback; invalidation is hint-only, so effect is stale/refetch pressure rather than data/authorization corruption
Evidence:
- StoragePulseEvent at browser-cross-context-invalidation.ts:98-101 contains only key and newValue.
- receiveStorage at 179-193 checks exact key/value but cannot check area.
- browser-cross-context-host.ts:206-248 discards native storageArea.
- client-cache-and-storage.md:82-83, 939-948 and checklist 1638 explicitly documents this missing check.
- Existing native browser tests cover homogeneous BroadcastChannel and homogeneous storage fallback, not a foreign storage area.
Required decision:
Capture localStorage once and derive both the write facade and event validator from the same object identity. Do not call a hostile getter twice.
Proposed facade:
export type StoragePulseEvent = Readonly<{
key: string | null;
newValue: string | null;
storageArea: "EXPECTED_LOCAL_STORAGE" | "OTHER_OR_UNKNOWN";
}>;
Core receiveStorage admits only EXPECTED_LOCAL_STORAGE. Register the pulse key in storage-keys.ts at the same time:
CACHE_INVALIDATION_PULSE:
backend localStorage
classification opaque-cache
valueCodec opaque-string-v1
ttl null
migration discard
quotaFallback no-persist
The storage adapter need not own pulse I/O; the registry owns its physical-key policy.
### N-10 — cursor pagination can admit a page after cancellation
- Severity: Medium
- Confidence: High
- Activation: AVAILABLE_NOT_COMPOSED pagination runtime
Evidence:
- cursor-pagination-runtime.ts:29-33 checks signal only before await loadPage.
- There is no post-await signal check before page validation/accumulation and success at lines 34-59.
- A non-cooperative loadPage that resolves after abort can make the last page return success.
- The architecture status claims an abort test, but cursor-pagination-runtime.test.ts currently covers finite chain, invalid invariants, loop and snapshot drift only.
Required decision:
Race loadPage with the signal or check immediately after await and before observing the page. Prefer an awaitWithAbort helper so a never-settling loader cannot hold loadAll forever. Late page completion is ignored. Return REQUEST_ABORTED / PAGINATION_ABORTED without partial items.
### N-11 — non-finite queue capacities bypass boundedness
- Severity: Low
- Confidence: High
- Activation: custom adapter construction only; bootstrap uses defaults
Evidence:
- bounded-diagnostics.ts:21 uses Math.max(1, maxEntries). NaN remains NaN and Infinity remains Infinity.
- best-effort-telemetry.ts:49 has the same issue.
- Comparisons against NaN/Infinity can disable intended eviction.
Fix: require Number.isSafeInteger and a documented upper ceiling, throwing TypeError at construction. This is configuration validation, not a runtime drop.
## 6. Selection-dependent improvements and explicitly separated hypotheses
These are not confirmed defects at the same level as N-01 through N-11.
### O-01 — mixed BroadcastChannel/storage-only tabs
- Confidence: Medium
- Evidence: browser-cross-context-invalidation.ts:318-342 returns immediately after a successful BroadcastChannel post and does not pulse storage. A second tab whose BroadcastChannel constructor failed but whose storage works listens only to storage.
- Existing tests at cross-tab-invalidation.test.ts:447-488 cover a sender whose BroadcastChannel post fails, then storage fallback. Browser capability tests cover BroadcastChannel/BroadcastChannel and storage/storage, not BroadcastChannel sender/storage-only receiver.
- Product decision: if per-tab capability asymmetry must be supported, mirror every accepted BroadcastChannel event to storage and rely on existing eventId dedupe. If “priority fallback” assumes partition-homogeneous capability, document that assumption and keep single-write behavior.
- Trade-off: mirroring increases synchronous localStorage writes and storage-event fan-out. This protocol is a best-effort hint with focus/stale revalidation, so do not build a durable/exactly-once bus.
### O-02 — beforeunload attachment comment does not match implementation
- Confidence: High for mismatch; Low material impact
- browser-lifecycle.ts:39-43 says the listener exists only while a source reports dirty.
- syncBeforeUnload at 135-143 attaches whenever any source is registered; it cannot observe a callback changing from false to true.
- onBeforeUnload rechecks actual dirtiness, so users are not incorrectly prompted.
- Preferred minimal action: document “while at least one dirty reporter is registered”. Add an observable update handle only if listener-count optimization is a real requirement.
### O-03 — async scope/coordinator teardown
- Confidence: Medium
- ServerStateScopeRuntime.dispose and QueryInvalidationCoordinator.dispose are void while reset/flush promises may exist.
- Current generation checks and disposed flags prevent reactivation; no stale cache admission was demonstrated.
- If runtime shutdown needs a “all background state work settled” guarantee, introduce async close and await it in composition. Otherwise characterize late work and retain the simpler void API.
### O-04 — QueryCachePort clone-on-read
- Confidence: Medium
- tanstack-query-cache.ts clones writes but returns TanStacks object reference on read.
- Docs say cached mapped values are immutable, but QueryCachePort returns unknown rather than a readonly type.
- Decide whether the port guarantees immutable values or isolation. If isolation is required, clone on read and return QUERY_CACHE_FAILURE on clone failure. Do not add cost to production TanStack hooks based only on this legacy port.
### O-05 — caller-abort versus already-buffered successful response
- Confidence: Medium; not included in confirmed findings
- V3 aborts the fetch signal, but admitResponse does not directly inspect terminalCancellation after a custom/buffered reader resolves.
- Before changing semantics, add a deterministic test where readBoundedResponseBytes aborts the caller and then returns valid bytes. Product must choose first-terminal-owner-wins versus completed-response-wins. The designs CancellationOwner wording suggests first-owner-wins, but this report does not claim it without characterization.
## 7. Patterns to apply, and patterns to reject
Apply:
1. Profile/Strategy registry for auth. The operation selects an immutable profile; the credential owner supplies only proof material.
2. Stable tuple codec for validator keys. It directly solves injectivity and keeps storage private.
3. Monotonic certainty lattice for logical command execution. Physical attempts cannot downgrade already-observed uncertainty.
4. Structured cancellation context. One lifetime signal/deadline is passed through credential, recovery, retry sleep, fetch and response admission.
5. Small terminal lifecycle state for telemetry. ACTIVE/DISPOSED plus one joined flush promise is sufficient.
6. Adapter-boundary predicate for StorageEvent.storageArea. Browser identity checks belong at native capability capture.
7. Characterization-first consolidation for legacy body reading. Delegate to the proven bounded reader while preserving old error codes.
Reject:
- A generic HTTP interceptor/middleware pipeline: it obscures authority/order and recreates the transport-header bug.
- A generic repository abstraction for query cache, ETag store and storage pulse: their consistency and lifecycle semantics differ.
- Durable/exactly-once cross-tab messaging: invalidation is a bounded hint; server revalidation remains authoritative.
- Persistent telemetry retry/offline queue: current contract is best-effort and has no consent/retention decision.
- A new retry library or circuit breaker: current retry bounds are explicit and adequate once replay proof/certainty is corrected.
- Event sourcing for scope/reset: synchronous generation fencing plus ordered participants is simpler and already correct.
## 8. Exact implementation manifest
Implement as small reviewable changes. “Delete: none” and “Move: none” applies to the immediate remediation; legacy removals occur only after the compatibility window.
### Change set A — V3 observability and monotonic effect
Modify:
- src/adapters/http/http-execution-v3.ts
- src/adapters/http/http-effect-certainty.ts
- src/bootstrap/runtime-adapters.ts
- src/features/reference-feature/adapters/create-reference-feature-input.ts
- src/contracts/diagnostics.ts only if effect_certainty is approved; otherwise do not modify its allowlist
- tests/unit/http-execution-v3.test.ts
- tests/unit/runtime-adapters.test.ts
- tests/features/reference-feature/reference-runtime-composition.test.ts
- docs/architecture/decisions/VD-07-diagnostics-and-telemetry-exporter.md
- docs/architecture/2026-07-30-frontend-runtime-capability-repository-aligned-implementation-closed-deep-design.md
Create:
- tests/integration/http-execution-v3-observability.test.ts
Delete: none.
Move: none.
### Change set B — profile-authoritative credentials and auth cancellation
Modify:
- src/contracts/rest-profiles.ts
- src/contracts/external-contract-runtime.ts for strict authProfileId grammar only
- src/adapters/http/http-contract-bridge.ts
- src/adapters/http/http-execution-v3.ts
- src/application/ports/auth-session-port.ts
- src/adapters/auth/external-session-adapter.ts
- src/bootstrap/runtime-adapters.ts
- src/features/reference-feature/adapters/create-reference-feature-input.ts to map AUTH_INTEGRATION_FAILURE
- tests/unit/rest-profile-contract.test.ts
- tests/unit/auth-session-adapter.test.ts
- tests/unit/http-execution-v3.test.ts
- tests/unit/runtime-adapters.test.ts
- tests/features/reference-feature/reference-runtime-composition.test.ts
- docs/architecture/decisions/VD-23-api-transport-selection-and-rest-execution.md
Create:
- tests/integration/http-execution-v3-auth-profile.test.ts
Delete: none.
Move: none.
### Change set C — telemetry lifecycle
Modify:
- src/adapters/telemetry/best-effort-telemetry.ts
- src/bootstrap/runtime-adapters.ts
- tests/unit/telemetry.test.ts
- tests/unit/runtime-adapters.test.ts
- docs/architecture/decisions/VD-07-diagnostics-and-telemetry-exporter.md
Create: none.
Delete: none.
Move: none.
### Change set D — state sidecars and cancellation
Modify:
- src/adapters/query-cache/conditional-validator-store.ts
- src/adapters/query-cache/cursor-pagination-runtime.ts
- tests/unit/conditional-validator-store.test.ts
- tests/unit/cursor-pagination-runtime.test.ts
- docs/architecture/api-contract-schema-mapper-and-server-state.md
Create: none.
Delete: none.
Move: none.
### Change set E — legacy rollback hardening and reader consolidation
Modify:
- src/contracts/mutation-intent.ts
- src/application/ports/auth-session-port.ts
- src/adapters/auth/external-session-adapter.ts
- src/adapters/http/client.ts
- src/adapters/http/http-execution-v3.ts to reuse the common key validator/context
- src/adapters/http/bounded-json.ts to delegate to bounded-body-reader
- tests/integration/http-client.test.ts
- tests/integration/auth-recovery.test.ts
- tests/integration/http-execution-contract.test.ts
- tests/unit/bounded-body-reader.test.ts
- docs/architecture/decisions/VD-23-api-transport-selection-and-rest-execution.md
Create:
- tests/unit/bounded-json-compatibility.test.ts
Delete immediately: none.
Move: none.
Later removal after zero runtime callers and an expired rollback window:
- Delete src/adapters/http/client.ts
- Delete src/adapters/http/bounded-json.ts
- Delete src/adapters/http/request-builder.ts
- Delete src/adapters/http/resource-mapper.ts
- Delete src/adapters/http/schema-registry.ts
- Remove createRuntimeHttpClient from src/bootstrap/runtime-adapters.ts
- Remove V2-only tests/fixtures after V3 equivalents exist
Do not delete retry-policy.ts while V3 imports parseRetryAfter.
### Change set F — exact storage fallback admission
Modify:
- src/contracts/storage-keys.ts
- src/adapters/cross-context-invalidation/browser-cross-context-host.ts
- src/adapters/cross-context-invalidation/browser-cross-context-invalidation.ts
- src/adapters/cross-context-invalidation/index.ts
- tests/unit/cross-tab-invalidation.test.ts
- tests/browser-capabilities/cross-context-invalidation.spec.ts
- docs/architecture/client-cache-and-storage.md
- docs/architecture/decisions/VD-13-client-cache-scope-and-persistence.md
Create:
- tests/unit/browser-cross-context-host.test.ts
Delete: none.
Move: none.
Optional O-01 mirroring must be a separate change set and must not be bundled with the exact storageArea security check.
### Change set G — capacity and lifecycle documentation hardening
Modify:
- src/adapters/diagnostics/bounded-diagnostics.ts
- src/adapters/telemetry/best-effort-telemetry.ts
- tests/unit/diagnostics.test.ts
- tests/unit/telemetry.test.ts
- src/adapters/platform/browser-lifecycle.ts comment only, unless observable dirty state is selected
Create a browser-lifecycle unit test only if behavior changes.
Delete: none.
Move: none.
## 9. TDD matrix
Write each test red first.
| Test name | Input/setup | Expected result |
| --- | --- | --- |
| records_v3_terminal_outcome_with_allowlisted_context_once | V3 success and terminal 503; concrete diagnostics adapter | one record per logical execution; route/operation/status/attempt/duration are safe bucket keys; dropped map empty |
| emits_v3_terminal_non_abort_failure_once | V3 retry exhaustion | one api.request.failed after final attempt, none per attempt |
| does_not_emit_v3_failure_telemetry_for_caller_or_scope_abort | caller abort and scope fence | diagnostic once, telemetry zero |
| forwards_reference_route_id_to_v3_observation | list/detail/create gateway requests | exact registry route IDs reach observation; no raw URL/intent |
| rejects_unknown_auth_profile_during_runtime_composition | installed contract refers to missing profile | composition throws before any feature can execute |
| rejects_bearer_ready_patch_without_authorization | authenticated session, empty READY patch | AUTH_INTEGRATION_FAILURE, NOT_STARTED, fetch zero |
| rejects_credential_patch_that_owns_accept_content_type_or_credentials | hostile credential adapter | integration/contract failure, fetch zero |
| sends_exact_profile_credentials_and_required_headers | valid bearer and cookie profiles | exact init.credentials and header subset |
| forwards_lifetime_abort_to_external_credential_owner | hanging owner, deadline/caller abort | owner signal aborts; execution settles with correct closed error |
| preserves_prior_maybe_applied_when_retry_is_fenced_before_dispatch | IDEMPOTENT command, first 429, scope false only at retry final invariant | SCOPE_FENCED and MAYBE_APPLIED; fetch called once |
| never_decreases_logical_command_certainty_across_attempts | table of NOT_STARTED/NOT_APPLIED/MAYBE combinations | join follows lattice |
| dispose_prevents_queued_and_future_telemetry_delivery | captured schedule, emit, dispose, callback, emit | fetch zero, queue zero |
| dispose_aborts_active_telemetry_delivery_without_reschedule | fetch waits on signal, dispose | signal aborted, no reschedule |
| concurrent_flush_joins_active_delivery | call flush twice while sink pending | both promises settle only after same fetch settles; fetch once |
| runtime_dispose_disposes_telemetry_before_state_dependencies | spied telemetry/lifecycle | pagehide removed and sink aborted during composition dispose |
| keeps_delimiter_ambiguous_validator_bindings_distinct | the A/B binding pair from N-05 | prepare(A)=etag A, prepare(B)=etag B |
| rejects_unbounded_or_invalid_validator_definition_identity | empty/oversize/invalid fingerprint | install false, no row |
| rejects_empty_control_and_oversize_legacy_idempotency_keys_before_send | "", whitespace, control, 257-byte key | VALIDATION_REJECTED/IDEMPOTENCY_KEY_INVALID; auth/fetch/timer zero |
| reuses_one_valid_legacy_key_on_every_retry | keyed 503 then success | identical non-empty header on each physical attempt |
| settles_legacy_hanging_credential_at_total_deadline | owner never resolves | REQUEST_TIMEOUT; fetch zero; listeners/timer removed |
| ignores_late_legacy_recovery_completion | recovery resolves after abort/scope transition | terminal result unchanged; no replay/notification from late completion |
| bounded_json_never_rejects_when_cancel_or_release_fails | hostile stream methods | closed legacy error, no rejection |
| content_type_mismatch_cancels_legacy_response_body_once | non-JSON response with cancellable stream | CONTENT_TYPE_MISMATCH and cancel called once |
| ignores_storage_event_from_non_local_storage_area | exact pulse key/value but OTHER_OR_UNKNOWN area | delivery zero |
| accepts_storage_event_only_from_captured_local_storage | native-like event with captured identity | delivery once |
| captures_local_storage_getter_once | getter returns different objects per access | getter called once; event/write identity coherent |
| ignores_late_cursor_page_after_abort | loader aborts signal then resolves final page | REQUEST_ABORTED/PAGINATION_ABORTED |
| settles_never_resolving_cursor_loader_on_abort | loader never settles | loadAll settles promptly with abort |
| rejects_non_finite_adapter_capacities | NaN, Infinity, fractional, excessive values | TypeError at construction |
Targeted commands:
corepack pnpm exec vitest run \
tests/unit/http-execution-v3.test.ts \
tests/integration/http-execution-v3-observability.test.ts \
tests/integration/http-execution-v3-auth-profile.test.ts \
tests/features/reference-feature/reference-runtime-composition.test.ts
corepack pnpm exec vitest run \
tests/unit/telemetry.test.ts \
tests/unit/runtime-adapters.test.ts \
tests/unit/diagnostics.test.ts
corepack pnpm exec vitest run \
tests/unit/conditional-validator-store.test.ts \
tests/unit/cursor-pagination-runtime.test.ts \
tests/unit/cross-tab-invalidation.test.ts \
tests/unit/browser-cross-context-host.test.ts
corepack pnpm exec vitest run \
tests/integration/http-client.test.ts \
tests/integration/auth-recovery.test.ts \
tests/integration/http-execution-contract.test.ts \
tests/unit/bounded-json-compatibility.test.ts
Browser evidence:
corepack pnpm test:browser-capabilities
Required final gates:
corepack pnpm check:types
corepack pnpm lint
corepack pnpm check:architecture
corepack pnpm check:diagnostics
corepack pnpm test:unit
corepack pnpm test:integration
corepack pnpm test:reference-feature
If storage registry changes, also run:
corepack pnpm check:registries
corepack pnpm verify:compatibility
## 10. Compatibility and migration order
1. Land characterization tests only. They must fail for N-01 through N-05 and remain isolated from implementation.
2. Add routeId to InstalledContractOperationExecutor and HttpExecutionContext. Update every compile-time call site in reference gateway/tests in one commit. This is source-breaking but has no wire change.
3. Add typed V3 observation fields and runtime projection. Keep diagnostic/telemetry registries closed; use existing buckets. Deploy read operations first and verify non-empty, non-dropped V3 records.
4. Add logical certainty accumulation. No wire/API change outside the exported outcome values; downstream code must already handle MAYBE_APPLIED.
5. Extend RestAuthProfile with requiredCredentialHeaders and build the profile index at composition. Deploy fail-closed validation before changing credential owners.
6. Extend auth cancellation context as optional. Update internal/demo/external adapters and both clients. After one compatibility release, make it required for external owners.
7. Make demo authentication explicit. Do not relax REFERENCE_EXTERNAL_BEARER. Validate the selected demo behavior only against loopback/test provider evidence before enabling.
8. Fix telemetry lifecycle and call dispose from infrastructure teardown. This changes only post-dispose behavior and flush-await semantics.
9. Replace validator key codec before the first conditional HTTP composition. It is memory-only, so no data migration is required.
10. Harden V2 idempotency/cancellation/body reading before documenting it as a rollback target. Add deprecation notices and audit createRuntimeHttpClient callers.
11. Add storage registry entry and exact storageArea check without changing the invalidation wire envelope/version.
12. Run native browser capability evidence. Decide mixed-transport mirroring separately.
13. Only after zero V2 callers, V3/provider evidence, and expiration of the rollback window, delete legacy files.
Compatibility notes:
| Change | Compatibility |
| --- | --- |
| Required routeId | TypeScript source break; no network wire break. Update all executor callers atomically. |
| Observation shape | Internal dependency seam but exported type; tests/custom composition must update. |
| AUTH_INTEGRATION_FAILURE outcome | Exhaustive switch source break; add mapping to existing ApiFailure AUTH_INTEGRATION_FAILURE. |
| RestAuthProfile required headers | Source break for custom profiles; provide migration error naming profile ID only. |
| Optional AuthOperationContext phase | Backward compatible for owner implementation types; behavior improves immediately for updated owners. |
| Telemetry dispose | Intentional behavioral change only after ownership ends. |
| Validator key codec | No persisted state and no HTTP composition; safe replacement. |
| StoragePulseEvent area enum | Test/host facade source break; wire envelope unchanged. |
| Invalid legacy idempotency key | Intentional fail-fast behavior; callers relying on empty keys must be fixed, not grandfathered. |
## 11. Rollback sequence
Rollback must preserve security/correctness invariants.
1. Disable affected command operations first; do not route a command to legacy V2 unless V2 idempotency, auth deadline, final invariant and provider evidence are already fixed.
2. If observability sink causes incidents, set telemetry config off or wire noOpTelemetry. Keep V3 diagnostic projection, redaction registries and producer tests.
3. If strict auth composition rejects a bad deployment, fail the operation/provider as unavailable and repair the profile/owner. Do not restore broad credential headers or patch-owned credentials.
4. Read-only V3 operations may fall back only to a hardened V2 path with unexpired provider/security evidence, matching VD-23 lines 700-703.
5. The logical-effect accumulator must not be rolled back independently; downstream reconciliation depends on conservative MAYBE_APPLIED.
6. Conditional-validator codec rollback is simply disabling conditional request composition and clearing the in-memory store.
7. Storage-area hardening rollback should degrade to BroadcastChannel/local-only revalidation, not accept unverified storage events.
8. Cross-context wire version remains unchanged, so no coordinated tab upgrade is needed.
9. Roll back contract artifact, frontend and backend as one coherent set where operation/profile semantics changed.
10. Keep new regression tests during rollback; change only routing/configuration.
## 12. Existing tests/docs cross-check and false-positive controls
### What the passing tests genuinely prove
- http-execution-v3.test.ts proves descriptor projection, keyed intent validation, one-key reuse, no query key, schema containment, post-dispatch command uncertainty, scope fencing after a response, credential-wait deadline, deadline retry suppression, retry-sleep cancellation and forbidden-body stream failure.
- bounded-body-reader.test.ts has strong hostile stream/cancellation/release coverage; this is why consolidation is preferred.
- server-state-scope-runtime.test.ts proves synchronous fencing, participant order, fail-closed reset/activation and identity close.
- tanstack-cache-coordinator.test.ts proves topic mapping, lease deferral, reset ordering and disposal behavior under current contract.
- cross-tab-invalidation.test.ts proves invalid/stale/self/duplicate/gap handling, bounded fallback and cleanup.
- browser capability spec proves actual BroadcastChannel/BroadcastChannel and storage/storage delivery in supported browsers.
- diagnostics.test.ts and telemetry.test.ts prove projector allowlists, bounded queues, hostile context containment and pagehide listener removal.
- integration/http-diagnostics.test.ts proves exactly-once diagnostics/telemetry for legacy createHttpClient.
- reference runtime composition proves V3 URLs/headers and that private intent values do not appear in collected evidence.
- 21 selected files / 144 tests pass, so findings do not rely on a generally broken baseline.
### Why those tests do not invalidate the findings
- Legacy HTTP diagnostics tests import createHttpClient, not createContractHttpExecutor. They cannot validate V3 runtime-adapters projection.
- reference runtime composition only asserts private values are absent; an empty diagnostics array also satisfies it.
- check:diagnostics counts/inspects source producers but does not execute their concrete context through projectDiagnosticRecord.
- auth-session tests validate the external owners current header allowlist, but V3s exported CredentialPatchOutcome and final invariant still grant broader authority; they also do not require Authorization for the declared bearer profile.
- current V3 command is KEYED with retryBudget zero. It does not exercise an IDEMPOTENT retry followed by a final-invariant fence.
- telemetrys disposal test calls dispose after pagehide has already flushed and checks only listener removal.
- conditional-validator tests use delimiter-unambiguous values and docs explicitly mark the sidecar not composed.
- storage tests check exact key and envelope but the facade has no storageArea field to assert.
- browser docs explicitly list pulse registration and storageArea as unfinished, confirming N-09 rather than contradicting it.
- mixed-transport asymmetry is left as O-01 because the documented priority fallback can reasonably be read as a deliberate single-transport policy.
- beforeunload does not prompt falsely because the event callback rechecks dirty state; only the attachment comment is mismatched.
- no claim is made that conditional validators or cursor pagination currently corrupt the installed reference HTTP path; both are activation blockers for future composition.
## 13. Design worth preserving
- V3 keeps operation semantics in installed descriptors and re-verifies a bounded final request rather than accepting arbitrary URLs/headers from features.
- Bounded response admission avoids Response.json, enforces byte ceilings, uses strict UTF-8, and isolates stream cleanup failures.
- Mutation intent and command effect are explicit public concepts; post-dispatch uncertainty is represented instead of guessed from HTTP status.
- Server-state scope fences synchronously before any reset await, aborts the old signal, closes identity registries and creates a new QueryClient generation.
- Query invalidation sends only registry topic/version/epoch, never query keys, cached data, account IDs or mutation payloads. Remote authority is invalidate-only.
- Cross-context event parsing is closed and bounded with TTL, event dedupe, source epoch/sequence and gap escalation.
- TanStack retry is disabled so the HTTP layer remains the single retry authority.
- Diagnostics/telemetry have closed registries, low-cardinality value policies, redaction and failure isolation.
- External auth owner never returns raw tokens to application code; it returns a constrained header patch.
- Composition tears down optional capabilities before base state, which is the right dependency order.
- No unnecessary persistence/offline mutation queue/exactly-once protocol is claimed.
## 14. Recommended delivery order
P0:
1. N-01 V3 observability.
2. N-02 profile-authoritative auth.
3. N-03 monotonic command certainty.
4. N-04 telemetry terminal lifecycle.
P1 before enabling currently available capabilities or trusting rollback:
5. N-05 conditional-validator key codec.
6. N-06/N-07 V2 replay and auth deadline.
7. N-09 exact storageArea and registered pulse.
8. N-10 pagination cancellation.
P2 cleanup:
9. N-08 body-reader consolidation.
10. N-11 capacity validation.
11. O-02 documentation alignment.
12. Decide O-01/O-03/O-04/O-05 with explicit product requirements and characterization tests.
This ordering closes silent current-path failures and security/effect authority first, then makes latent capabilities safe to compose, and only then removes duplication.
@@ -0,0 +1,459 @@
# Realtime / Browser RPC adapter 구현 리뷰
- 리뷰 기준: `4dc033c` (2026-08-13, Asia/Seoul)
- 구현 범위: `src/adapters/realtime/**`, `src/adapters/browser-rpc/**`
- 추적 범위: 대응 contracts, application ports, bootstrap 조립, unit/boundary tests, architecture docs
- 방식: 코드 리뷰만 수행했다. 이 문서 외 구현 파일은 수정하지 않았다.
- 결론: **Critical 0, High 4, Medium 3**이다. R-01~R-06은 코드상 확정된 lifecycle/immutability/resource 문제이고, R-07은 문서에도 미완료라고 명시된 production promotion blocker다. 두 runtime 모두 현재 `AVAILABLE_NOT_COMPOSED`이므로 production traffic 사고로 과장하지 않는다.
## 1. 21/21 파일 inventory와 책임
아래 경로는 모두 저장소 루트 기준 full path이며, 범위의 구현 파일 21개를 모두 읽었다.
| # | full path | 책임 | 주요 의존성 / downstream | 판정 |
|---:|---|---|---|---|
| 1 | `src/adapters/browser-rpc/browser-rpc-runtime.ts` | operation을 unary/server-stream application port로 bind하고 request schema/encoder, deadline/retry, transport, response schema/mapper, generation fence를 순서대로 집행 | `application/ports/browser-rpc`, `ClockPort`, Browser RPC contract, schema/mapper registry, `transport.ts`, `AppFailure` | R-01, R-04, R-06, R-07 |
| 2 | `src/adapters/browser-rpc/index.ts` | Browser RPC public adapter export surface | runtime, transport, unavailable adapter | 새 lease/install type export 필요 |
| 3 | `src/adapters/browser-rpc/transport.ts` | provider-neutral unary/stream transport result와 runtime identity 계약 | `src/contracts/browser-rpc.ts` | R-01, R-07 |
| 4 | `src/adapters/browser-rpc/unavailable-browser-rpc-transport.ts` | 선택되지 않은 runtime의 명시적 fail-closed Null Object | `transport.ts` | 유지; 새 stream lease shape만 맞춤 |
| 5 | `src/adapters/realtime/event-codec.ts` | raw JSON byte/shape/registry/schema 검증, immutable DTO와 semantic fingerprint 생성 | realtime contracts, schema registry, JSON scanner, result codec | 유지 |
| 6 | `src/adapters/realtime/event-consumer.ts` | SSE/WS cursor 규칙을 codec 결과와 결합하고 common stream coordinator outcome으로 투영 | realtime ports/contracts, event codec, stream coordinator | 유지 |
| 7 | `src/adapters/realtime/index.ts` | common realtime adapter public export surface | codec, consumer, reconnect, handoff, stream, sub-index | R-02/R-03 lifecycle type export 필요 |
| 8 | `src/adapters/realtime/json-member-scanner.ts` | `JSON.parse` 전 duplicate member와 structure budget을 비재귀적으로 검사 | 독립 utility | 유지 |
| 9 | `src/adapters/realtime/live-poll-handoff-coordinator.ts` | LIVE/POLL 단일 effect writer, generation fence, quiescence/checkpoint, probe buffer와 전환 | `ClockPort`, realtime result/ports | R-03 |
| 10 | `src/adapters/realtime/polling/bounded-poll-coordinator.ts` | visible/online finite lease, single-flight poll, retry hint, response/apply deadline, non-cooperative task drain | bounded polling policy, `ClockPort`, realtime result | 유지 |
| 11 | `src/adapters/realtime/polling/index.ts` | polling public exports | bounded poll coordinator | 유지 |
| 12 | `src/adapters/realtime/reconnect-coordinator.ts` | 단일 reconnect owner, online gate, retry budget, session close authority, exact recovery proof, post-abort DRAINING | reconnect policy, `ClockPort`, realtime ports/result | 유지 |
| 13 | `src/adapters/realtime/reconnect-policy.ts` | immutable reconnect policy, full jitter, elapsed budget, Retry-After 계산/검증 | 독립 policy | 유지 |
| 14 | `src/adapters/realtime/result.ts` | hostile/mutable collaborator result를 exact own-data snapshot으로 canonicalize | realtime ports/contracts | 유지; R-04의 기준 패턴 |
| 15 | `src/adapters/realtime/sse/fetch-sse-connection.ts` | fixed same-origin fetch-stream SSE, response/media/open gate, read/event deadline, cursor rule, bounded reader cancel | `ClockPort`, event authority/result, parser, reconnect policy | 유지; R-02 common drain과 함께 검증 |
| 16 | `src/adapters/realtime/sse/index.ts` | SSE public exports | fetch connection, parser | 유지 |
| 17 | `src/adapters/realtime/sse/sse-parser.ts` | strict incremental UTF-8 SSE parser, BOM/line ending/id/retry/event buffer ceiling | realtime contracts/result | 유지 |
| 18 | `src/adapters/realtime/stream-coordinator.ts` | per-stream sequential effect, dedupe/order, recovery, checkpoint/barrier, scope generation fence | event authority port, realtime contracts, mapper registry, codec/result | R-02 |
| 19 | `src/adapters/realtime/websocket/index.ts` | WebSocket connection/protocol public exports | connection, protocol | 유지 |
| 20 | `src/adapters/realtime/websocket/websocket-connection.ts` | 한 physical WS의 handshake/subscription/tombstone/FIFO/heartbeat/apply gate/recovery close | `ClockPort`, realtime contracts/result, WS protocol | 유지; R-02 upstream timeout과 함께 검증 |
| 21 | `src/adapters/realtime/websocket/websocket-protocol.ts` | exact closed JSON frame decode/encode, duplicate key/structure/sequence/frame byte 검증 | realtime contracts, JSON scanner | R-05 |
## 2. 추적한 contracts, ports, bootstrap, tests, docs
| 계층 | 읽은 파일과 근거 | 대조 결과 |
|---|---|---|
| Realtime contracts | `src/contracts/realtime-streams.ts`, `src/contracts/realtime-events.ts` | registry가 stream/event/recovery/queue ceiling을 닫고 cursor/sequence/scope 문법을 소유한다. adapter가 이를 우회하지 않는다. |
| Realtime ports | `src/application/ports/realtime/shared.ts:1-66`, `src/application/ports/realtime/event-authority.ts:15-202`, `src/application/ports/realtime/index.ts:1-31` | native error/payload/cursor 없는 closed result, exact recovery checkpoint identity, effect/recovery commit authority를 확인했다. R-02 lifecycle inspection 확장이 필요하다. |
| Browser RPC contract | `src/contracts/browser-rpc.ts:66-179,256-469,472-591` | wire/profile/operation join과 hard limit은 풍부하지만 validate-only mutable binding이다(R-04). `maxBufferedBytes`는 선언/검증만 된다(R-07). |
| Browser RPC port | `src/application/ports/browser-rpc/browser-rpc.ts:4-35`, `src/application/ports/browser-rpc/index.ts` | application에는 typed unary/stream Result만 보이고 generated type/frame/endpoint는 노출되지 않는다. 변경 불필요. |
| Clock / Result | `src/application/ports/clock-port.ts`, `src/adapters/platform/system-clock.ts`, `src/application/result.ts`, `src/contracts/errors.ts` | injected clock/fence failure도 port Result 의미로 닫아야 한다(R-06). |
| Bootstrap | `src/bootstrap/optional-runtime-host.ts:21-29,65-70,87-92,142-150` | `realtime: null`, health `UNAVAILABLE`, 제품 contribution 전 미조립은 의도다. Browser RPC 조립도 없다. 미조립 자체는 결함이 아니다. |
| Boundary gates | `scripts/check-realtime-boundaries.ts`, `scripts/lib/realtime-boundaries.ts`, `scripts/check-realtime-boundary-fixtures.ts`, `scripts/test-realtime-runtime-removal.ts`; `tests/fixtures/realtime-boundaries/allowed/**`, `forbidden/**` | native realtime API 소유권과 unselected composition을 정적 검사한다. Browser RPC에는 아직 같은 별도 boundary gate가 없다. |
| Realtime docs | `docs/architecture/decisions/VD-28-realtime-events-web-push-and-bounded-polling.md`, `docs/architecture/realtime-events-web-push-and-bounded-polling.md`, `docs/architecture/optional-adapter-recipes.md` | fixed endpoint, exact barrier, single writer, overflow fail-close, bounded cleanup/DRAINING, 미조립 상태를 코드와 대조했다. |
| Browser RPC docs | `docs/architecture/protobuf-browser-transport-and-rest-gateway.md`, `docs/architecture/decisions/VD-27-grpc-web-unary-and-server-stream.md`, `docs/architecture/decisions/VD-29-connect-web-and-browser-protobuf-runtime.md` | common lifecycle만 구현됐고 concrete framing/raw-byte/provider/browser conformance는 pending이라고 명시한다. |
대조한 16개 테스트 파일:
- `tests/unit/browser-rpc/browser-rpc-contract.test.ts`
- `tests/unit/browser-rpc/browser-rpc-runtime.test.ts`
- `tests/unit/realtime/bounded-poll-coordinator.test.ts`
- `tests/unit/realtime/bounded-polling-policy.test.ts`
- `tests/unit/realtime/event-codec.test.ts`
- `tests/unit/realtime/event-consumer.test.ts`
- `tests/unit/realtime/fetch-sse-connection.test.ts`
- `tests/unit/realtime/live-poll-handoff-coordinator.test.ts`
- `tests/unit/realtime/realtime-reconnect-coordinator.test.ts`
- `tests/unit/realtime/realtime-reconnect-policy.test.ts`
- `tests/unit/realtime/realtime-stream-registry.test.ts`
- `tests/unit/realtime/result.test.ts`
- `tests/unit/realtime/sse-parser.test.ts`
- `tests/unit/realtime/stream-coordinator.test.ts`
- `tests/unit/realtime/websocket-connection.test.ts`
- `tests/unit/realtime/websocket-protocol.test.ts`
## 3. 분류
### 확정 결함
| ID | 심각도 | 확신도 | 요약 |
|---|---|---|---|
| R-01 | High | High | Browser RPC server-stream 종료가 non-cooperative iterator에서 무기한 멈춘다. |
| R-02 | High | High | common stream coordinator가 non-cooperative effect/recovery 하나로 영구 wedge된다. |
| R-03 | High | High | LIVE↔POLL overflow fail-close가 active lease를 잃어 이후 close가 거짓 성공한다. |
| R-04 | High | High | Browser RPC bindings는 validate-then-use TOCTOU이며 exact immutable install이 아니다. |
| R-05 | Medium | High | WS frame byte ceiling 전에 입력 전체 UTF-8 copy를 추가 할당한다. |
| R-06 | Medium | High | Browser RPC clock/fence 예외가 Result 경계를 탈출하고 cleanup을 건너뛴다. |
### 미조립 단계 promotion blocker / 선택 개선
| ID | 심각도 | 확신도 | 요약 |
|---|---|---|---|
| R-07 | Medium, promotion blocker | High | `maxBufferedBytes`의 concrete transport 집행 및 provider/browser conformance가 아직 없다. 문서에도 pending으로 명시되어 현재 common runtime bug로 세지 않는다. |
## 4. 확정 결함 상세
### R-01 — Browser RPC server-stream 종료가 non-cooperative iterator에서 무기한 멈춘다
- 심각도: **High**
- 확신도: **High**
- 근거:
- `src/adapters/browser-rpc/transport.ts:53-61`은 stream을 `AsyncIterable` 하나로 표현한다. 명시적 `cancel`/`waitClosed`/cleanup bound가 없다.
- `src/adapters/browser-rpc/browser-rpc-runtime.ts:488-512``iterator.next()`를 deadline과 race하지만, timeout 뒤 원래 `next()` task는 남을 수 있다.
- `src/adapters/browser-rpc/browser-rpc-runtime.ts:630-640``finally`에서 `await iterator.return()`을 deadline 없이 기다린다. pending `next()`가 signal을 무시하면 async generator의 queued `return()`도 완료되지 않는다.
- 영향: caller abort, idle/total timeout, response limit, consumer `break` 뒤 application iterator completion이 무기한 pending이다. 외부 abort listener 수명도 `finally` 완료 전까지 닫히지 않는다. timeout Result를 선택했어도 iterator가 끝나지 않아 total deadline 의미가 깨진다.
- 기존 증거와 gap: `tests/unit/browser-rpc/browser-rpc-runtime.test.ts:343-369`은 cooperative generator가 abort를 보고 `finally`로 끝나는 경우만 확인한다. `docs/architecture/protobuf-browser-transport-and-rest-gateway.md:381-399`는 reader cancel/release, bounded consumer queue, terminal envelope, EOF non-success를 요구한다.
- 적용 패턴: **Explicit Stream Lease + structured concurrency + retained DRAINING task**. 암묵적인 `AsyncIterable.return()`에 transport lifecycle authority를 숨기지 않는다.
- 결정:
1. app-facing generator는 idle/total/limit/caller abort 후 cleanup bound 안에 끝난다.
2. commit/admission generation은 즉시 fence한다.
3. underlying task가 bound 안에 끝나지 않으면 transport lease는 `DRAINING`에 남고 실제 `waitClosed()` settlement까지 추적한다.
4. `return()`/`waitClosed()` rejection은 이미 선택한 application failure를 덮지 않는다.
### R-02 — common stream coordinator가 non-cooperative authority 하나로 영구 wedge된다
- 심각도: **High**
- 확신도: **High**
- 근거:
- `src/adapters/realtime/stream-coordinator.ts:231-247`은 event를 `state.tail`에 직렬 연결한다.
- `src/adapters/realtime/stream-coordinator.ts:373-408`은 effect authority를 직접 `await`한다. AbortSignal을 무시하는 Promise에 deadline/drain state가 없다.
- recovery는 `src/adapters/realtime/stream-coordinator.ts:497-529`에서 기존 tail을 기다리고 `:543-560`에서 recovery authority를 다시 무기한 기다린다.
- `close():809-834`는 controller만 abort하고 즉시 `void`로 끝나 실제 settlement/DRAINING을 나타내지 않는다.
- 영향: WS `maxApplyMs`(`websocket-connection.ts:1060-1080`)나 SSE event timeout(`fetch-sse-connection.ts:321-355`)은 transport caller만 끝낸다. common tail은 pending이라 새 generation event와 queue-overflow recovery까지 영구 대기한다. generation fence는 late commit을 막지만 liveness/resource convergence는 보장하지 않는다.
- 기존 증거와 gap:
- `tests/unit/realtime/stream-coordinator.test.ts:383-466`의 in-flight effect는 결국 resolve되고 `:791-821`의 non-cooperative recovery도 테스트 끝에서 settle한다. never-settling authority와 bounded close는 없다.
- `VD-28...md:218-224,250-256,437-446`은 terminal/idempotent close와 bound를 넘긴 task가 실제 settle할 때까지 `DRAINING`을 유지하도록 정한다.
- 적용 패턴: **per-stream State Machine + Task Lease Registry + generation capability**.
- 결정:
1. freshness와 별도로 lifecycle `OPEN | DRAINING | CLOSED`를 둔다.
2. effect/recovery deadline에 commit capability를 영구 false로 만들고 abort한다.
3. caller에는 `IDLE_TIMEOUT` (`operation: APPLY | RECOVER`, non-retryable)을 bounded하게 반환하고 실제 task는 retain한다.
4. DRAINING 중 새 event/recovery를 허용하지 않는다. actual settle 뒤 `STALE`에서 authoritative recovery를 요구하거나 close 요청이면 `CLOSED`로 간다.
5. `close()``Promise<RealtimeResult<void>>`로 bounded quiescence 결과를 반환한다.
### R-03 — LIVE↔POLL overflow 뒤 active writer reference를 잃는다
- 심각도: **High**
- 확신도: **High**
- 근거:
- `src/adapters/realtime/live-poll-handoff-coordinator.ts:274-286`은 active tail overflow 시 `failClosed()`를 호출한다.
- `failClosed():774-788`은 controller를 abort한 뒤 `active = null`로 지우지만 해당 lease/tail을 retired set에 보존하지 않는다.
- `performClose():672-700`은 현재 active/probe/quiescing/transitionCandidate만 모으므로 이미 버린 non-cooperative active writer를 기다리지 않고 success할 수 있다.
- 영향: 256건/4MiB overflow로 generation 전체를 닫았지만 effect는 계속 실행 중이고 lifecycle owner가 추적하지 않는다. teardown success가 quiescence를 뜻하지 않아 새 runtime과 old task가 겹칠 수 있다. `isCurrent()`는 commit만 fence한다.
- 기존 증거와 gap:
- `tests/unit/realtime/live-poll-handoff-coordinator.test.ts:166-215`는 non-cooperative overflow를 만들지만 이후 `close()`를 호출하지 않는다.
- `:466-493`의 close test는 reference를 잃기 전 active writer만 다룬다.
- ADR `VD-28...md:422-427,443-446`은 overflow full-generation fail-close와 actual settlement까지 DRAINING을 요구한다.
- 적용 패턴: **Retired Lease Registry + two-phase close**.
- 결정: `failClosed()`는 모든 lease를 abort하고 `retiredWriters`에 옮겨 admission을 닫는다. `close()`는 current+retired를 dedupe해 bounded하게 기다리고, timeout에는 `IDLE_TIMEOUT/CLOSE`를 반환하되 마지막 tail settlement까지 DRAINING을 유지한다.
### R-04 — Browser RPC bindings가 validate-then-use TOCTOU이다
- 심각도: **High**
- 확신도: **High**
- 근거:
- `src/contracts/browser-rpc.ts:256-285``define*`는 shallow spread/freeze만 하고 exact own key/data descriptor를 검사하지 않는다. extra/accessor property가 남는다.
- `validateBrowserRpcContractBindings():330-469`은 원본 registry/row를 읽어 `true`만 반환하며 installed snapshot을 만들지 않는다.
- `src/adapters/browser-rpc/browser-rpc-runtime.ts:102-128`은 factory에서 검증한 뒤 `bind()` 때 원본 `dependencies.*`를 다시 읽는다.
- `src/adapters/browser-rpc/browser-rpc-runtime.ts:644-687`도 validation용 runtime identity만 복사하며 operations/profiles/schema/mappers/encoders/transports 원본을 계속 사용한다.
- 영향: TypeScript `Readonly`는 runtime 보호가 아니다. factory 이후 mutation으로 replay policy, attempt/deadline, byte ceiling, mapper/transport selection을 validation과 다르게 만들 수 있다. operation/profile 객체의 extra property도 transport가 해석할 수 있다.
- 기존 증거와 gap: `tests/unit/browser-rpc/browser-rpc-contract.test.ts:173-202`는 raw invalid row를 재검증하지만 검증 후 mutation, accessor non-invocation, extra/symbol key 거절은 없다. realtime `result.test.ts:15-151`과 reconnect policy tests에는 exact descriptor snapshot 패턴이 이미 있다.
- 적용 패턴: **Parse/Validate/Install anti-corruption layer + immutable exact registry snapshot**.
- 결정:
1. factory 시작 시 registry own descriptors를 한 번 캡처하고 null-prototype exact map으로 복사/freeze한다.
2. operation/profile/encoder/schema/mapper/transport row를 허용 key의 own data property로 snapshot한다. getter, extra, symbol, revoked proxy는 composition-time `TypeError`다.
3. runtime과 transport call은 installed snapshot만 사용한다.
4. parse/map/encode/invoke function identity는 snapshot하되 row/registry를 재독하지 않는다.
### R-05 — WS byte cap 전에 전체 UTF-8 copy를 할당한다
- 심각도: **Medium**
- 확신도: **High**
- 근거: `src/adapters/realtime/websocket/websocket-protocol.ts:215-228`은 먼저 `utf8ByteLength(input)`을 호출하고 `:469-470``new TextEncoder().encode(input)`으로 전체 크기의 두 번째 buffer를 만든다.
- 영향: hostile/buggy server가 큰 text frame을 보냈을 때 negotiated cap으로 즉시 거절하지 못하고 cap 확인 전에 전체 UTF-8 copy를 추가 할당한다. browser가 원본 string을 materialize했다는 사실과 adapter의 추가 peak allocation은 별개다.
- 기존 증거와 gap: `tests/unit/realtime/websocket-protocol.test.ts:130-166`은 결과 코드와 multibyte bytes는 확인하지만 pre-allocation reject는 확인하지 않는다. `event-codec.ts:102-115``raw.length > maxBytes` 선검사를 이미 사용한다.
- 적용 패턴: **admission before allocation + bounded incremental accounting**.
- 결정: `input.length > maxFrameBytes`를 먼저 거절한다. 남은 입력은 allocation 없는 code-point loop로 UTF-8 bytes를 누적해 초과 즉시 중단하며 lone surrogate는 `TextEncoder`와 동일하게 replacement 3 bytes로 센다.
### R-06 — Browser RPC collaborator exception이 Result 경계를 탈출한다
- 심각도: **Medium**
- 확신도: **High**
- 근거:
- unary `src/adapters/browser-rpc/browser-rpc-runtime.ts:188-191,219-221,307-323``clock.now()`를 safe wrapper 없이 호출한다.
- `mapResponse():778-786,835-845``generationFence.isCurrent()``clock.now()`도 throw를 잡지 않는다.
- `raceWithin():1094-1120`은 abort listener를 붙인 뒤 `clock.sleep()` synchronous throw 또는 race 예외를 감싸는 `finally`가 없다.
- unary 전체에 outer `try/finally`가 없어 `linked.cleanup():198`은 정상 `finish()` 경로에서만 보장된다.
- 영향: application port가 `Promise<Result<...>>`/`AsyncIterable<Result<...>>` 대신 native rejection을 노출한다. clock/scope owner 실패 시 listener/timer cleanup과 observation도 빠질 수 있다.
- 기존 증거와 gap: standard `systemClock`, 정상 fence, generation change는 테스트하지만 throwing clock/fence와 listener balance는 없다. bounded poll/reconnect는 `safeNow`, `safeIsCurrent`, `finally` cleanup을 이미 사용한다.
- 적용 패턴: **Result boundary guard + RAII-style finally**.
- 결정: clock failure는 `SERVER_FAILURE/RPC_RUNTIME_DEPENDENCY_FAILED`, capture/isCurrent 실패는 fail-closed `SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE`로 canonicalize한다. linked listener/timer는 단일 outer `finally`에서 정확히 한 번 해제한다.
## 5. 미조립/promotion blocker
### R-07 — `maxBufferedBytes` 집행 증거가 없다
- 심각도: **Medium, production promotion blocker**
- 확신도: **High**
- 확정 사실:
- `src/contracts/browser-rpc.ts:114-120,519-528``maxBufferedBytes`를 선언/검증한다.
- common runtime은 `src/adapters/browser-rpc/browser-rpc-runtime.ts:587-593`에서 yielded message count/per-message/aggregate만 센다.
- `src/adapters/browser-rpc/transport.ts:58-60`의 bare `AsyncIterable`에는 buffer admission/inspection contract가 없다.
- 문서로 확인한 현재 상태: `docs/architecture/protobuf-browser-transport-and-rest-gateway.md:42-61,363-366,381-399`는 selected transport/raw-byte cap/provider-browser conformance가 pending이라고 명시한다. 따라서 common runtime이 wire framing/internal buffer를 직접 집행하지 않는 것 자체는 현재 결함이 아니다.
- promotion 위험: callback/stock client가 consumer보다 빨리 frame을 쌓으면 common runtime이 item을 받기 전에 heap cap이 깨질 수 있다. `maxTotalResponseBytes`는 aggregate이고 `maxBufferedBytes`와 다른 backpressure 축이다.
- 적용 패턴: **transport conformance contract + enqueue-time backpressure admission**.
- 결정: concrete Connect/gRPC-Web transport가 enqueue 전에 `operation.maxBufferedBytes`, raw/decompressed ceiling을 집행하고 overflow 시 lease cancel + `RESPONSE_BODY_LIMIT`을 낸다는 conformance suite를 통과하기 전 bootstrap/product traffic을 금지한다. common runtime의 message/aggregate guard는 second line으로 유지한다.
## 6. 상태머신, protocol, framing, backpressure와 cleanup 결정
| 축 | 명시 결정 | 이유 |
|---|---|---|
| Common stream state | freshness `UNKNOWN/CURRENT/STALE/RESYNCING`와 lifecycle `OPEN/DRAINING/CLOSED`를 직교 축으로 둔다. timeout/abort 뒤 actual task가 남으면 DRAINING이다. | commit fence와 resource settlement는 다른 사실이다(R-02). |
| Reconnect | 기존 `IDLE/RUNNING/DRAINING/CLOSED`, 단일 retry owner, full jitter, exact bounded server hint, exact branded recovery proof를 유지한다. offline에는 retry timer를 두지 않고 protocol 자동 downgrade를 금지한다. | 구현/ADR/test가 일치한다. |
| Poll lease | 기존 single-flight `IDLE/RUNNING/DRAINING/CLOSED`, visible+online finite lease, one-request HTTP retry owner를 유지한다. | non-cooperative execute/apply를 이미 fence+track한다. |
| LIVE↔POLL handoff | Poll은 probe 동안 유일 authoritative writer다. old writer fence→abort→quiesce→checkpoint→buffer drain 뒤 LIVE를 활성화한다. overflow는 generation terminal이며 retired lease actual settlement까지 DRAINING이다. | silent overlap/lost update 방지(R-03). |
| SSE framing | strict UTF-8, blank-line terminated SSE, incomplete EOF discard, CURSOR일 때만 explicit `id`, exact status/media/same-origin 규칙을 유지한다. | tests/docs와 일치한다. |
| WS framing | text JSON + exact frame keys + duplicate-member/structure/uint64 검증을 유지한다. byte cap은 allocation 전에 집행한다. malformed/overflow는 whole generation close + snapshot recovery다. | classic WS에는 receive pause가 없고 delta drop은 안전하지 않다. |
| Browser RPC framing | common runtime은 logical message/terminal/failure만 받는다. Connect 5-byte envelope/EndStream과 gRPC-Web trailer authority는 concrete transport가 각각 소유하며 서로 추론/혼합하지 않는다. EOF alone은 success가 아니다. | provider-neutral layer와 wire semantics를 분리한다. |
| Backpressure | WS inbound/outbound와 `bufferedAmount`, handoff queues, Browser RPC transport buffer를 count+bytes로 admission한다. cap 초과는 silent drop/자동 상향 없이 terminal close/failure다. | state-bearing delta의 부분 유실은 복구 없이는 안전하지 않다. |
| Cancel/timer/listener | listener를 얻은 scope의 `finally`에서 제거하고 모든 sleep timer controller를 abort한다. non-cooperative task의 caller wait만 bounded하고 reference는 actual settlement까지 retain한다. | bounded response와 resource convergence를 함께 만족한다. |
| Error semantics | `QUEUE_OVERFLOW`=admission/backpressure와 recovery 필요, `IDLE_TIMEOUT`=handler/quiescence cleanup deadline, `APPLY_FAILED`=authority reject/throw/invalid result, `PROVIDER_UNAVAILABLE`=clock/host dependency 실패, `PROTOCOL_MISMATCH`=shape/framing 위반, `SCOPE_FENCED`=old generation. raw/native 원인은 노출하지 않는다. | retry/rollback/운영 대응을 원인별로 닫는다. |
## 7. 제안 인터페이스와 정확한 파일 작업
### 7.1 새/변경 interface signature
```ts
// src/adapters/browser-rpc/transport.ts
export type BrowserRpcStreamCancelReason =
| "CALLER_ABORT"
| "IDLE_TIMEOUT"
| "TOTAL_DEADLINE"
| "LIMIT_EXCEEDED"
| "CONTRACT_FAILURE"
| "CONSUMER_CLOSED";
export type BrowserRpcTransportStream = Readonly<{
frames: AsyncIterable<BrowserRpcStreamFrame>;
cancel(reason: BrowserRpcStreamCancelReason): void;
waitClosed(): Promise<void>;
}>;
export type BrowserRpcTransport = BrowserRpcRuntimeBindingIdentity & Readonly<{
invokeUnary?(call: BrowserRpcTransportCall): Promise<BrowserRpcUnaryTransportResult>;
openServerStream?(call: BrowserRpcTransportCall): BrowserRpcTransportStream;
}>;
```
```ts
// src/contracts/browser-rpc.ts
export type InstalledBrowserRpcContractBindings = Readonly<{
operations: Readonly<Record<string, BrowserRpcOperationV3>>;
profiles: Readonly<Record<string, BrowserRpcProviderProfile>>;
schemaCodecs: Readonly<Record<string, RuntimeSchemaCodec>>;
mappers: Readonly<Record<string, InstalledBoundaryMapper>>;
requestEncoders: Readonly<Record<string, BrowserRpcRequestEncoder>>;
runtimeBindings: Readonly<Record<string, BrowserRpcRuntimeBindingIdentity>>;
}>;
export function installBrowserRpcContractBindings(
bindings: BrowserRpcContractBindings,
): InstalledBrowserRpcContractBindings;
```
```ts
// src/adapters/browser-rpc/browser-rpc-runtime.ts
export type BrowserRpcRuntimeDependencies = Readonly<{
// existing registries/collaborators stay
streamCleanupTimeoutMs?: number; // default 2_000, implementation max 30_000
}>;
```
```ts
// src/application/ports/realtime/event-authority.ts
export type RealtimeStreamLifecycle = "OPEN" | "DRAINING" | "CLOSED";
export type RealtimeStreamInspection = Readonly<{
lifecycle: RealtimeStreamLifecycle;
// existing freshness/queue/dedupe/barrier fields unchanged
}>;
```
```ts
// src/adapters/realtime/stream-coordinator.ts
export type RealtimeStreamTaskLimits = Readonly<{
effectTimeoutMs: number;
recoveryTimeoutMs: number;
drainTimeoutMs: number;
}>;
export type RealtimeStreamCoordinatorDependencies = Readonly<{
// existing dependencies stay
clock?: ClockPort;
taskLimits: RealtimeStreamTaskLimits;
}>;
export type RealtimeStreamCoordinator = Readonly<{
// existing methods stay
close(): Promise<RealtimeResult<void>>;
}>;
```
```ts
// src/adapters/realtime/live-poll-handoff-coordinator.ts
export type LivePollHandoffState =
| "LIVE_ACTIVE"
| "POLL_ACTIVE"
| "LIVE_PROBING"
| "DRAINING"
| "CLOSED";
export type LivePollHandoffInspection = Readonly<{
// existing fields stay
drainingWriters: number;
}>;
```
### 7.2 정확한 생성/수정/삭제/이동 목록
**생성:** 없음. lifecycle/install type은 기존 owner 파일에 둔다. 이 리뷰 문서 `docs/reviews/adapters/02-realtime-and-browser-rpc.md`만 리뷰 산출물로 새로 생성했다.
**수정:**
1. `src/contracts/browser-rpc.ts` — exact descriptor snapshot installer와 installed type.
2. `src/adapters/browser-rpc/transport.ts` — explicit stream lease/cancel/closed receipt.
3. `src/adapters/browser-rpc/browser-rpc-runtime.ts` — installed snapshot만 사용, bounded stream cleanup/DRAINING, safe clock/fence, outer cleanup.
4. `src/adapters/browser-rpc/unavailable-browser-rpc-transport.ts` — unavailable stream을 즉시 closed lease로 반환.
5. `src/adapters/browser-rpc/index.ts` — installed/stream lifecycle types export.
6. `src/application/ports/realtime/event-authority.ts` — stream lifecycle inspection.
7. `src/application/ports/realtime/index.ts``RealtimeStreamLifecycle` export.
8. `src/adapters/realtime/stream-coordinator.ts` — bounded task lease registry, lifecycle state, async close.
9. `src/adapters/realtime/live-poll-handoff-coordinator.ts` — retired writer set과 DRAINING convergence.
10. `src/adapters/realtime/index.ts` — lifecycle/limit types export.
11. `src/adapters/realtime/websocket/websocket-protocol.ts` — allocation-free bounded UTF-8 counter.
12. `tests/unit/browser-rpc/browser-rpc-contract.test.ts` — mutation/accessor/extra-key installer tests.
13. `tests/unit/browser-rpc/browser-rpc-runtime.test.ts` — non-cooperative stream, throwing clock/fence, cleanup balance tests와 fixture lease 전환.
14. `tests/unit/realtime/stream-coordinator.test.ts` — never-settling effect/recovery, DRAINING/async close tests.
15. `tests/unit/realtime/live-poll-handoff-coordinator.test.ts` — overflow 뒤 retired writer close test.
16. `tests/unit/realtime/websocket-protocol.test.ts` — oversize preflight/multibyte/lone-surrogate tests.
17. `docs/architecture/protobuf-browser-transport-and-rest-gateway.md` — stream lease, buffer owner, promotion evidence.
18. `docs/architecture/realtime-events-web-push-and-bounded-polling.md` — common stream/handoff DRAINING와 error semantics.
19. `docs/architecture/decisions/VD-28-realtime-events-web-push-and-bounded-polling.md` — actual-settlement lifecycle amendment.
**삭제:** 없음.
**이동:** 없음.
**의도적으로 변경하지 않음:** `src/bootstrap/optional-runtime-host.ts`는 제품/provider 선택 전 `null/UNAVAILABLE` 유지가 맞다. `src/application/ports/browser-rpc/browser-rpc.ts`의 app-facing API도 변경할 필요가 없다.
## 8. TDD 테스트 계획
먼저 아래 테스트를 실패시키고(red), 최소 구현 후 개별 green, 마지막에 전체 범위를 실행한다.
| 테스트 이름 | 입력/준비 | 기대 결과 |
|---|---|---|
| `bounds_non_cooperative_stream_cancel_and_completes_consumer` | Browser RPC stream의 `next()``waitClosed()`가 signal/cancel을 무시; idle deadline 진행 | caller iterator는 cleanup bound 안에 `REQUEST_TIMEOUT` 후 done; `cancel("IDLE_TIMEOUT")` 1회; lease DRAINING |
| `rejects_new_stream_while_prior_lease_is_draining` | 위 stream actual settlement 전 같은 transport에 두 번째 open | network side effect 없이 `SERVER_FAILURE/RPC_STREAM_DRAINING`; old settle 후 새 open 가능 |
| `consumer_break_cancels_and_bounds_stream_cleanup` | 첫 message 뒤 consumer `break`; close non-cooperative | `cancel("CONSUMER_CLOSED")`; generator return bounded; listener/timer 0 |
| `runtime_snapshots_bindings_before_later_mutation` | factory 뒤 원본 operation retry/deadline/profile/transport map mutation | execute는 installed snapshot만 사용; mutation이 의미 변경 불가 |
| `binding_installer_rejects_extra_and_accessor_keys_without_invoking_them` | operation/profile/registry에 getter, symbol, extra key | getter 호출 0; composition-time `TypeError` |
| `returns_canonical_failure_and_cleans_listener_when_clock_throws` | transport 전/후 `clock.now`/`sleep` synchronous throw; listener-counting signal | rejection 없음; 지정 `AppFailure`; listener/timer 0; observation 1회 |
| `fences_when_generation_fence_throws` | `capture` 또는 `isCurrent` throw | `SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE`; mapped value 미commit |
| `keeps_stream_draining_until_non_cooperative_effect_actually_settles` | effect Promise never settles; fake clock가 effect/drain deadline 진행 | accept는 bounded `IDLE_TIMEOUT/APPLY`; `isCurrent=false`; DRAINING; 새 effect 0 |
| `bounds_non_cooperative_recovery_and_rejects_late_checkpoint` | recovery가 timeout 뒤 늦게 success checkpoint 반환 | bounded `IDLE_TIMEOUT/RECOVER`; late checkpoint 미commit; settle 후 STALE/recovery 필요 |
| `close_waits_for_all_tracked_stream_tasks_and_times_out` | effect와 recovery pending 중 close | controller 모두 abort; bound 뒤 `IDLE_TIMEOUT/CLOSE`; settlement까지 DRAINING, 이후 CLOSED |
| `close_after_active_queue_overflow_tracks_retired_writer` | handoff active effect never settles, queue cap 초과 후 close | overflow `QUEUE_OVERFLOW`; close 즉시 success 금지; bound 뒤 `IDLE_TIMEOUT`; late settle 시 draining 0/CLOSED |
| `rejects_oversized_ascii_frame_before_utf8_copy` | `"x".repeat(maxFrameBytes + 1)` | `FRAME_TOO_LARGE`; full-size byte copy 경로 없음 |
| `counts_multibyte_and_lone_surrogate_like_text_encoder` | ASCII/2-byte/3-byte/surrogate pair/lone surrogate 경계 | 기존 byte 의미와 동일한 exact accept/reject |
| `transport_conformance_enforces_max_buffered_bytes_before_enqueue` | push/callback fake transport가 consumer 정지 중 cap+1 byte enqueue | enqueue 거부, lease cancel, raw/message 미노출; provider suite 없이는 promotion 금지 |
실행 명령:
```sh
corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-contract.test.ts
corepack pnpm exec vitest run tests/unit/browser-rpc/browser-rpc-runtime.test.ts
corepack pnpm exec vitest run tests/unit/realtime/stream-coordinator.test.ts
corepack pnpm exec vitest run tests/unit/realtime/live-poll-handoff-coordinator.test.ts
corepack pnpm exec vitest run tests/unit/realtime/websocket-protocol.test.ts
corepack pnpm exec vitest run tests/unit/realtime tests/unit/browser-rpc --reporter=dot --maxWorkers=4
corepack pnpm run check:types:app
corepack pnpm run check:types:test
corepack pnpm run check:realtime-boundaries
corepack pnpm run check:realtime-boundaries:fixture
```
제품 transport 선택 시 별도 필수 evidence:
```sh
# 실제 provider contribution이 script 이름과 target browser matrix를 고정해야 한다.
corepack pnpm run test:browser-rpc-transport-conformance
corepack pnpm run test:browser-rpc-target-browsers
```
## 9. compatibility, migration, rollback
Migration 순서:
1. 새 tests와 lifecycle inspection을 먼저 추가한다. runtime은 미조립 상태라 production traffic 영향은 없다.
2. `createBrowserRpcRuntime`은 raw input을 받아 내부에서 installer를 호출해 기존 caller signature를 유지한다. mutation에 의존한 fixture는 composition-time 오류로 고친다.
3. 한 migration release 동안 기존 `AsyncIterable` transport를 internal adapter로 `BrowserRpcTransportStream`에 감쌀 수 있다. deprecated wrapper의 `waitClosed``iterator.return()` settlement이고 common cleanup bound가 이를 감싼다. provider 선택 전 legacy branch를 제거한다.
4. common stream `close(): Promise<Result>`로 바꾸고 모든 test/향후 composition owner는 `await`한다. 기존 fire-and-forget 호출은 typecheck로 식별한다.
5. handoff retired set을 도입하고 `DRAINING`에는 writer/probe admission을 막는다.
6. WS byte counter는 wire/error shape가 같아 독립적으로 먼저 적용할 수 있다.
7. actual provider/browser/load evidence와 R-01~R-07 closure 전까지 `AVAILABLE_NOT_COMPOSED`를 유지한다. bootstrap composition은 마지막 단계다.
Compatibility 결정:
- application-facing Browser RPC unary/stream port shape는 유지한다.
- wire protocol, frame shape, failure kind, retry owner는 바꾸지 않는다.
- `RealtimeStreamInspection.lifecycle`는 additive다. `close` 반환형은 source-compatible fire-and-forget일 수 있으나 lifecycle correctness를 위해 owner는 await하도록 migration한다.
- exact installer가 과거 extra/accessor/mutable row를 거절하는 것은 의도된 fail-closed tightening이다.
Rollback 순서:
1. traffic admission을 `DISABLED`로 전환한다.
2. connection/runtime lifecycle을 `DRAINING`으로 만들고 actual leases settlement 또는 bounded failure를 기록한다.
3. 살아 있는 lease를 버리고 즉시 이전 runtime을 열지 않는다.
4. source commit을 revert하되 installed snapshot과 allocation-before-cap 수정은 보안/정확성 강화이므로 우선 유지한다.
5. cursor/checkpoint를 합성하지 않고 authoritative snapshot recovery를 수행한다.
6. SSE↔WS, Connect↔gRPC-Web↔REST, live↔Poll을 장애 때문에 즉석 자동 전환하지 않는다. fallback은 registry/ADR에 선언된 새 semantic operation/generation으로만 시작한다.
## 10. 유지할 좋은 설계
1. `RealtimeFailure`/`AppFailure`로 native error, raw close reason, payload, cursor, provider metadata를 경계 밖에 내보내지 않는다.
2. realtime result/registry의 exact own-data snapshot, accessor 거절, immutable recovery checkpoint object identity.
3. fixed same-origin SSE/WS endpoint, URL/subprotocol credential 금지, exact media/subprotocol 검증.
4. SSE, WebSocket, Browser RPC stream, Poll을 서로 다른 delivery/protocol 의미로 유지하고 자동 downgrade/replay하지 않는다.
5. WS inbound/outbound FIFO, count+byte+`bufferedAmount` ceiling과 overflow whole-generation recovery.
6. reconnect의 단일 retry owner, full jitter, hint not-before, finite budget, stable proof 뒤 reset, post-abort DRAINING.
7. Poll의 visible/online finite single-flight lease와 non-cooperative execute/apply tracking.
8. LIVE↔POLL의 one-writer generation, probe buffer, activation 전 quiescence/checkpoint.
9. SSE parser의 incremental strict UTF-8, incomplete EOF discard, bounded reader cancellation.
10. unavailable Browser RPC adapter와 optional runtime host의 `null/UNAVAILABLE`; 조용한 network fallback이 없다.
## 11. false-positive 방지 대조
| 의심 항목 | 최종 판정과 근거 |
|---|---|
| Realtime/Browser RPC가 bootstrap에 조립되지 않음 | 결함 아님. `optional-runtime-host.ts:91-92,143`와 architecture docs가 제품 선택 전 미조립을 요구한다. |
| Common Browser RPC가 Connect/gRPC-Web raw framing을 decode하지 않음 | 결함 아님. `protobuf...md:57-61,381-399`상 concrete transport 책임이다. R-07은 이 미완료 상태를 무시한 promotion만 막는다. |
| Reconnect가 offline 동안 timer 없이 기다림 | 의도. ADR과 `realtime-reconnect-coordinator.test.ts:370-406`가 explicit online signal을 요구한다. |
| healthy session `waitClosed()`에 deadline 없음 | 의도. ADR은 abort 후 drain만 bounded하고 active close receipt는 authoritative하게 기다린다. |
| WS overflow에서 일부 event drop 대신 connection close | 의도. ADR과 `websocket-connection.test.ts:579-651`은 receive pause 없는 classic WS에서 snapshot recovery를 택한다. |
| SSE 204와 incomplete EOF | 각각 terminal/no reconnect와 incomplete discard가 맞다. fetch/parser tests가 확인한다. |
| exact recovery object identity | 의도된 capability token이다. `event-authority.ts:17-23`, reconnect/stream barrier tests가 clone/forgery를 막는다. |
| Handoff overflow 자체 | 이미 fail-close한다. R-03은 overflow 판정이 아니라 그 직후 retired tail reference를 잃는 cleanup bug다. |
| Transport에 effect timeout이 이미 있음 | transport caller는 bounded해도 common `state.tail`은 settle하지 않는다. R-02는 commit fence가 아니라 retained task/liveness 문제다. |
## 12. baseline 검증
1. `corepack pnpm exec vitest run tests/unit/realtime tests/unit/browser-rpc --reporter=dot --maxWorkers=4`
- exit 0, **16 files / 185 tests passed**.
2. `corepack pnpm run check:realtime-boundaries`
- exit 0, `Realtime boundaries: PASS (src)`.
3. `check:realtime-boundaries:fixture` wrapper는 이 sandbox에서 child-process 제한 때문에 진단 없이 exit 1이었다. 같은 allowed/forbidden child 명령을 직접 실행해 allowed exit 0, forbidden exit 1과 세 규칙 `UNSELECTED_REALTIME_RUNTIME_COMPOSED`, `PRESENTATION_INTERVAL_OWNER`, `NATIVE_REALTIME_API_OUTSIDE_ADAPTER`를 확인했다. adapter defect로 세지 않는다.
4. `test:realtime-removal`의 별도 복제에서 범위 tests는 통과했으나 저장소 전체 baseline의 CI authority count drift, 누락 `.npmrc`, child `spawnSync ... EPERM`, architecture report 문제로 최종 exit 1이었다. 검토 범위 failure 증거로 사용하지 않는다.
## 13. 구현 우선순위
1. R-03 retired writer tracking: 국소적이고 확정적인 cleanup bug다.
2. R-02 common stream task lifecycle: SSE/WS 양쪽 liveness 기반을 닫는다.
3. R-01 Browser RPC explicit stream lease와 bounded cleanup.
4. R-04 installed immutable bindings, 이어 R-06 exception/cleanup guard.
5. R-05 allocation-before-cap 제거.
6. R-07 concrete transport conformance는 provider 선택과 함께 수행하되 완료 전 production composition을 금지한다.
@@ -0,0 +1,565 @@
# Storage / browser-file adapters 구현 준비 코드 리뷰
검토 저장소: `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template`
검토 범위: `src/adapters/storage/**`, `src/adapters/browser-files/**`, `src/adapters/browser-file-storage/**`, `src/adapters/cache-storage/**` 및 직접 연결된 application port, contract, bootstrap, test, architecture/operations 문서
검토 방식: 구현 파일을 수정하지 않은 read-only 리뷰. 아래 line은 현재 worktree 기준이다.
## 0. 결론과 우선순위
| ID | 판정 | 심각도 | 확신도 | 요약 |
| --- | --- | --- | --- | --- |
| STO-01 | 확정 결함 | **Critical** | 높음 | OPFS pre-commit 보상 cleanup 실패/취소를 무시하고 journal을 rollback한다. 늦게 도착한 generation-only cleanup이 후속 write의 같은 logical generation을 삭제할 수 있고, 그렇지 않아도 복구 근거와 quota를 잃는다. |
| STO-02 | 확정 결함 | **High** | 높음 | browser-managed download는 `baseOrigin`으로 상대 URL을 검증하지만 원문 `href``document.baseURI`로 실행한다. `<base>`가 있으면 검증한 origin과 실제 navigation origin이 달라진다. |
| STO-03 | 확정 결함 | **Medium** | 높음 | public cache policy가 `allowedVaryHeaderNames`를 허용하면서 response allowlist에서 `vary`를 제거하는 모순을 허용한다. stage는 성공할 수 있지만 저장 variant가 충돌하고 activation이 실패한다. |
| STO-04 | 확정 결함 | **Medium** | 높음 | 동일 manifest 재-stage가 marker와 count만 신뢰한다. marker 작성 뒤 browser eviction/부분 손상된 candidate를 성공으로 재사용하여 self-heal하지 못한다. activation은 fail-closed지만 staging success 의미가 약해진다. |
| STO-05 | 확정 결함 | **Medium** | 높음 | cache `activateRelease`/`cleanupOwned`가 네트워크 fetch를 쓰지 않는데도 공통 availability guard가 `fetcher`를 필수로 요구한다. offline activation/rollback/cleanup이 불필요하게 `UNSUPPORTED`가 된다. |
| STO-06 | 확정 계약 위반 | **Medium** | 높음 | IndexedDB codec migration은 commit transaction 내부의 연속 native operation 사이에 monotonic deadline을 재확인하지 않는다. 문서/port의 cooperative duration contract보다 오래 실행될 수 있다. |
| STO-07 | hardening 후보 | **Medium** | 높음 | OPFS worker envelope에 protocol version/response kind가 없고 client response parser가 `{requestId, ok}`만 검사한다. page/worker release 불일치와 malformed response를 `INCOMPATIBLE`로 닫을 수 없다. |
| STO-08 | 브라우저 검증 필요 | **Low** | 중간 | enhanced open/save picker 함수를 `Window`가 아니라 options 객체에 bind한다. Web IDL brand check가 있는 engine에서는 `Illegal invocation` 가능성이 있으나 현재 unit fake는 이를 검증하지 않는다. 실제 browser test로 먼저 확정한다. |
| GAP-01 | 문서화된 미구현 | **High readiness gap** | 높음 | preview pixel/decoded-byte/frame/decode probe가 없다. VD-15가 이미 `DESIGNED_NOT_IMPLEMENTED`로 명시했으므로 regression으로 오인하지 말고, untrusted image preview 조립의 promotion blocker로 취급한다. |
| GAP-02 | 문서화된 미구현 | **High readiness gap** | 높음 | Cache inspect/cleanup은 cursor/count/deadline 없이 전체 owned namespace를 순회한다. VD-15가 정확히 현 상태를 기록한다. |
| GAP-03 | 문서화된 미구현 | **High readiness gap** | 높음 | origin-wide pressure/write-admission/GC, OPFS/Cache forward migration, real OPFS preflight가 아직 없다. 기존 per-store primitive를 완성 증거로 삼지 않는다. |
즉시 순서는 **STO-01 write 차단/수정 → STO-02 canonical URL 실행 → STO-03~05 cache 불변식 → STO-06/07 hardening**이다. GAP 항목은 해당 capability를 제품에 선택·조립하기 전에 별도 promotion gate로 구현한다.
## 1. 누락 없는 범위 inventory: 책임과 의존성
### 1.1 `browser-file-storage`
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
| --- | --- | --- |
| `src/adapters/browser-file-storage/index.ts` | browser data 공통 Result와 StorageManager adapter barrel export | 내부 두 모듈만 export. 경계가 작고 유지 대상. |
| `src/adapters/browser-file-storage/result.ts` | native 예외를 closed `BrowserDataFailure`로 정규화하고 안전한 observation 제공 | `application/ports/browser-file-storage/shared.ts`; raw path/name/message 비노출, observer 예외 격리가 좋다. |
| `src/adapters/browser-file-storage/storage-manager-adapter.ts` | `estimate/persisted/persist` snapshot, pressure bucket, user-activation-bound persistence 요청 | storage durability port/result. estimate를 예약량으로 오인하지 않고 irreversible `persist()` truth를 보존한다. origin coordinator는 의도적으로 없음(GAP-03). |
### 1.2 `browser-files`
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
| --- | --- | --- |
| `src/adapters/browser-files/browser-file-picker.ts` | native input baseline 및 enhanced system picker, activation/abort/dismissal, vault capture | file port, vault, policy registry. baseline/enhancement 분리가 좋다. `showOpenFilePicker.bind(options)`는 STO-08. |
| `src/adapters/browser-files/browser-file-policy-registry.ts` | composition-owned selection/inspection/preview/download policy 등록·identity 확인·hard-cap reduction | file contracts, `file-policy.ts`. `WeakSet`/identity binding과 frozen snapshot을 유지한다. |
| `src/adapters/browser-files/browser-file-vault.ts` | transient native File/handle 보관, opaque ref, inspection receipt, bounded range/source | file port/shared/result/policy registry. File이 application 경계를 넘지 않고 receipt가 exact file/profile에 묶이는 설계가 좋다. |
| `src/adapters/browser-files/create-browser-file-runtime.ts` | vault/picker/preview/download를 선택적으로 조립하고 일괄 dispose | 위 adapters 및 application contracts. optional capability를 제품 선택 없이 bootstrap에 암묵 조립하지 않는 점을 유지. preview 조립 전 GAP-01 gate 필요. |
| `src/adapters/browser-files/download-delivery-adapter.ts` | browser handoff, foreground save stream, bounded object URL download, integrity/progress/cancellation | file/authorized-download ports, policy registry, object URL lease, Result. STO-02와 STO-08; stream close truth/backpressure는 유지. |
| `src/adapters/browser-files/file-observer.ts` | file-safe observation DTO를 공통 browser observation으로 변환 | shared port/result. raw filename/ref 비노출 유지. |
| `src/adapters/browser-files/file-policy.ts` | policy input validation, MIME/extension/signature/hard byte caps, immutable resolved policy | file/shared contracts. closed allowlist 및 absolute ceiling을 유지. |
| `src/adapters/browser-files/index.ts` | browser-file public exports | 위 모듈. native implementation detail export 확장을 피한다. |
| `src/adapters/browser-files/object-url-lease.ts` | 중앙 object URL lease cap/registry, transient preview, idempotent revoke/dispose | file/shared contracts, vault, policy registry. URL lifecycle은 좋으나 `create()` 256-303은 decode probe 없이 URL을 발급(GAP-01). |
### 1.3 `cache-storage`
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
| --- | --- | --- |
| `src/adapters/cache-storage/index.ts` | public cache policy/adapter barrel | optional public static cache만 export; private/range cache로 일반화하지 않는다. |
| `src/adapters/cache-storage/public-cache-policy.ts` | same-origin/public-only release 정책, URL/header/query/size/retention hard limits | cache ports/shared. STO-03 policy cross-field invariant 누락. 기본 policy에는 `vary`가 있어 기본-path 테스트는 통과한다. |
| `src/adapters/cache-storage/public-response-cache-adapter.ts` | manifest canonicalization/digest, anonymous fetch, bounded body 검증, candidate marker-last staging, explicit activation, exact lookup/reverify, owned cleanup/inspect | cache ports/result/policy, CacheStorage/fetch/Crypto/Web Lock snapshot. STO-03~05 및 GAP-02. private/auth/opaque/206 거절과 current+previous 보존은 유지. |
### 1.4 `storage` root / IndexedDB
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
| --- | --- | --- |
| `src/adapters/storage/browser-storage-adapter.ts` | registry key별 local/session/memory 저장, TTL, failure overlay/tombstone, quota fallback | `storage-keys`, `StoragePort`, codec, diagnostics. strict registry와 stale persistent suppression을 유지. adjacent physical-key migration/sweep는 문서상 미구현. |
| `src/adapters/storage/browser-storage-codec.ts` | bounded exact JSON envelope, exotic/accessor/unsafe-key/cycle/depth/node 거절 | 독립 codec. prototype pollution/JSON silent coercion 방어가 좋다. |
| `src/adapters/storage/indexeddb/index.ts` | IndexedDB runtime/maintenance/governance/migration export | native IDB type을 application port 밖으로 내보내지 않는 구조 유지. |
| `src/adapters/storage/indexeddb/indexeddb-failure.ts` | IDB/DOM failure를 closed browser failure로 변환 | common Result. raw native detail 비노출 유지. |
| `src/adapters/storage/indexeddb/indexeddb-governance.ts` | opaque dataset scope/physical DB identity 및 frozen policy binding | indexeddb/shared ports. account/business ID를 physical name에 쓰지 않는 양방향 binding 유지. |
| `src/adapters/storage/indexeddb/indexeddb-maintenance.ts` | post-open codec migration 및 idempotency receipt prune, keyset checkpoint, budget/revision fencing | IndexedDB port/types/failure/governance. async transform outside tx, row+sidecar+budget+checkpoint atomic commit은 좋다. STO-06 및 temporal drain lease 개선 후보. |
| `src/adapters/storage/indexeddb/indexeddb-migrations.ts` | additive-only contiguous DDL planner/validator | indexeddb types. destructive DDL 거절 유지. |
| `src/adapters/storage/indexeddb/indexeddb-runtime.ts` | generic repository open/read/query/CAS/delete, idempotency, retention, lifecycle purge, connection lifecycle | indexeddb ports/types/governance/failure/migrations. transaction `complete` truth, versionchange close, shared open/abort isolation, exact budgets 유지. lifecycle proof는 현재 문서 계약(형식 검증 후 폐기)과 일치하므로 결함으로 분류하지 않았다. |
| `src/adapters/storage/indexeddb/indexeddb-types.ts` | adapter-local codec/query/schema/dependency contracts | application indexeddb/shared ports. `isOldWriterDrainConfirmed()` boolean은 provider가 전체 window를 보장한다는 문서 전제; lease형으로 강화 권고. |
### 1.5 `storage/opfs`
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
| --- | --- | --- |
| `src/adapters/storage/opfs/browser-opfs-runtime.ts` | OPFS support inspection 및 journal/worker/byte-store composition | OPFS ports, journal, byte-store, policy, worker client. property probe를 real readiness로 주장하지 않음(GAP-03). |
| `src/adapters/storage/opfs/index.ts` | OPFS runtime/journal/policy/protocol/client exports | optional capability barrel. |
| `src/adapters/storage/opfs/indexeddb-opfs-journal.ts` | logical object/journal/budget/chunk refcount의 IDB authority; begin/files-ready/commit/rollback/reconcile pages | OPFS ports, IDB failure, policy. journal+object+budget CAS atomicity가 좋다. STO-01 수정에서 incomplete row를 cleanup 확인 전 삭제하지 않아야 한다. |
| `src/adapters/storage/opfs/opfs-byte-store-adapter.ts` | logical journal과 physical worker를 saga로 조정, put/open/remove, reconcile/policy maintenance | OPFS/shared ports, journal, worker gateway, policy. STO-01의 journal/physical compensation ordering 결함 위치. |
| `src/adapters/storage/opfs/opfs-policy.ts` | root/lock/chunk/object/RPC/reconcile/GC hard limits 및 scope validation | shared/opfs ports. opaque physical path와 absolute caps 유지. |
| `src/adapters/storage/opfs/opfs-worker-client.ts` | request correlation/timeout/abort/transferable chunking, worker gateway, streamed reads | protocol/policy/shared Result. STO-01의 untracked abort cleanup 및 STO-07의 shallow response parse. |
| `src/adapters/storage/opfs/opfs-worker-protocol.ts` | page↔DedicatedWorker request/response union 및 gateway contract | OPFS/shared ports. STO-07; protocol version/kind/effect certainty 추가 필요. |
| `src/adapters/storage/opfs/opfs-worker-runtime.ts` | DedicatedWorker OPFS physical layout, lock lease, immutable chunk, manifest/staging receipt, abort/finalize/remove/GC | protocol/policy/OPFS ports/Web Lock/Crypto. STO-01의 generation-only cleanup과 lease release 순서. sync handle `finally close` 등은 유지. |
### 1.6 직접 연결 경계와 조립
- `src/application/ports/browser-file-storage/{shared,file,indexeddb-port,opfs-ports,cache-storage-ports,storage-durability-port}.ts`와 barrel을 읽었다. native `File/Blob/Cache/IDB*/Response/ReadableStream`을 application으로 노출하지 않는 포트 방향은 올바르다.
- `src/application/ports/storage-port.ts`, `src/contracts/storage-keys.ts`를 대조했다. Web Storage는 registry-owned typed key만 허용한다.
- `src/bootstrap/runtime-adapters.ts:17,260-266`은 Web Storage만 기본 조립한다. file/IndexedDB/OPFS/Cache가 없는 것은 문서의 `AVAILABLE_NOT_COMPOSED`와 일치하며 결함이 아니다.
## 2. 구체적 findings와 구현 방법
### STO-01 — OPFS 보상 cleanup이 journal보다 늦게 완료되거나 실패할 때 후속 generation 삭제 가능
**근거와 실패 연쇄**
1. 새 logical generation은 현재 committed generation+1로 재사용된다: `src/adapters/storage/opfs/opfs-byte-store-adapter.ts:158-195`(특히 183-195).
2. `preparePut` 또는 `markFilesReady` 실패 시 `rollbackBestEffort`를 호출한다: 같은 파일 `222-242`.
3. `rollbackBestEffort``worker.cleanupTransaction(..., callerSignal)``BrowserDataResult`를 검사하지 않고, 곧바로 `journal.rollback`을 호출한다: `833-845`. caller signal이 이미 abort되었으면 cleanup RPC는 시작조차 못 한다.
4. worker client도 prepare 단계 실패/timeout 때 별도의 un-signaled `ABORT_PUT`을 보내지만 timeout/실패를 삼키며 “journal reconciliation이 반복한다”고 가정한다: `src/adapters/storage/opfs/opfs-worker-client.ts:190-198,229-307`. 그런데 3번이 journal row를 삭제한다.
5. physical cleanup은 staging receipt에서 `(scope, objectId, generation)`만 읽어 해당 generation 디렉터리를 삭제한다: `src/adapters/storage/opfs/opfs-worker-runtime.ts:584-607,717-761`. manifest/path에 transaction-unique physical generation identity가 없다.
6. `abortPut`은 mutation lease를 먼저 release한 뒤 generation 삭제를 수행한다: 같은 파일 `501-529`(특히 519-527). `cleanupTransaction` 자체도 mutation lease를 얻지 않는다.
따라서 T1 cleanup RPC가 timeout 뒤 worker에서 계속되거나 T1 `ABORT_PUT`이 늦게 실행되는 동안 coordinator가 T1 journal을 rollback하면 T2가 같은 object의 동일 logical generation을 다시 시작할 수 있다. 늦은 T1 cleanup은 T2의 물리 디렉터리를 삭제할 수 있다. 삭제까지 겹치지 않아도 journal 부재로 stale staging/immutable chunks가 영구 잔존해 quota pressure를 만든다.
**패턴과 수정**
- cross-API ACID를 주장하지 말고 **durable saga + transactional outbox/compensation state**를 유지한다.
- “physical cleanup confirmed” 전에는 PREPARING/FILES_READY journal row와 budget reservation을 rollback하지 않는다. cleanup은 caller signal과 분리한 composition-owned bounded signal을 사용한다.
- worker client 내부에서 fire-and-forget abort를 중복 발행하지 않는다. coordinator가 `abortPreparedPut()` 한 번을 소유하고 결과가 `CLEANED|ALREADY_CLEAN`일 때만 journal rollback한다. timeout/crash는 `EFFECT_UNKNOWN`으로 남겨 reconcile한다.
- 장기적으로 **transaction-unique physical generation/fencing token**을 path, receipt, manifest, journal에 저장한다. stale T1 cleanup은 T1 token 경로만 삭제하고 T2를 건드릴 수 없어야 한다.
- cleanup/abort는 같은 origin mutation Web Lock을 physical 삭제 완료까지 보유한다. lease를 먼저 release하지 않는다.
**권장 새/변경 signature**
```ts
declare const opfsPhysicalGenerationBrand: unique symbol;
export type OpfsPhysicalGenerationId = string & {
readonly [opfsPhysicalGenerationBrand]: "OpfsPhysicalGenerationId";
};
export type OpfsPreparedObjectV2 = Readonly<{
physicalSchemaVersion: 2;
physicalGenerationId: OpfsPhysicalGenerationId;
descriptor: DurableObjectDescriptor; // logical generation은 그대로 유지
chunks: readonly OpfsChunkReference[];
}>;
export type OpfsCleanupEffect =
| Readonly<{ kind: "CLEANED" | "ALREADY_CLEAN" }>
| Readonly<{ kind: "EFFECT_UNKNOWN" }>;
export interface OpfsWorkerGateway {
abortPreparedPut(request: Readonly<{
scope: OpfsStorageScope;
transactionId: string;
physicalGenerationId: OpfsPhysicalGenerationId;
signal?: AbortSignal; // coordinator-owned compensation signal만 전달
}>): Promise<BrowserDataResult<OpfsCleanupEffect>>;
}
```
P0에서는 v1 read를 유지하면서 새 write만 v2/token path로 쓴다. `EFFECT_UNKNOWN`은 성공 Result로 취급하지 말고 journal 유지 + `OBJECT_RECONCILE`를 반환한다.
**기존 테스트와 false-positive 방지**
- `tests/unit/opfs-byte-store.test.ts:504-540`의 “keeps a committed journal row for reconciliation when cleanup fails”는 logical commit 뒤 finalize 실패만 검증한다. PREPARING/FILES_READY 보상 실패를 다루지 않는다.
- `tests/unit/opfs-worker-runtime.test.ts:232-408`은 BEGIN cancel/APPEND-vs-ABORT serialization/authority isolation을 검증하지만, journal rollback 뒤 다른 worker/context가 재사용한 generation에 대한 늦은 cleanup을 만들지 않는다.
- `indexeddb-opfs-journal.ts:997-1008`의 unique `logicalKey` index는 **journal row가 남아 있는 동안** T2를 막는다. 바로 그 row를 조기에 삭제하는 것이 문제이므로 이 index가 반증이 아니다.
### STO-02 — 검증 URL과 실제 download navigation URL의 base가 다름
**근거**
- `safeBrowserManagedTarget``new URL(href, new URL(baseOrigin))`으로 protocol/origin/query/hash를 검증한다: `src/adapters/browser-files/download-delivery-adapter.ts:1248-1269`.
- 성공 후 canonical `URL.href`가 아니라 원문 문자열을 host로 넘긴다: `435-459`.
- 실제 anchor는 `anchor.href = href`라서 document의 current `baseURI`를 기준으로 해석한다: `47-68`.
예: configured `baseOrigin=https://app.example`, capability `href="downloads/report"`, document에 `<base href="https://evil.example/">`가 있으면 검증은 app origin을 통과하지만 실제 anchor는 evil origin으로 향한다. capability receipt의 server binding이 있더라도 adapter의 same-origin 정책 주장이 깨진다.
**패턴과 수정**
- **Parse once / canonicalize then execute** 패턴을 적용한다. validator가 boolean이 아니라 canonical absolute URL을 반환하고 정확히 그 값을 handoff한다.
- cross-origin을 허용하는 별도 policy에서도 username/password/hash/query 규칙을 적용한 canonical string만 실행한다.
```ts
type ResolvedBrowserManagedTarget = Readonly<{ absoluteHref: string }>;
function resolveBrowserManagedTarget(
href: string,
baseOrigin: string,
policy: Readonly<{ allowCrossOrigin: boolean; allowQuery: boolean }>,
): BrowserDataResult<ResolvedBrowserManagedTarget>;
```
`context.options.host.handoff(target.value.absoluteHref, fileName)`로 변경한다. 더 엄격한 선택은 capability resolver가 absolute `https:` URL만 발행하게 하고 상대 URL을 거절하는 것이다.
**기존 테스트 대조**
- `tests/unit/browser-file-download.test.ts:222-255`는 raw 상대 path가 host에 그대로 전달된다고 고정한다. 이 기대값을 canonical `https://app.example/downloads/artifact-1`로 바꿔야 한다.
- `257-285`는 이미 absolute evil/query URL 거절만 검증해 `<base>` 불일치를 잡지 못한다.
### STO-03 — Vary 허용/보존 policy가 모순될 수 있음
**근거**
- policy validation은 vary name이 request allowlist에 포함되는지만 본다: `src/adapters/cache-storage/public-cache-policy.ts:110-141`, 특히 `132-134`. response allowlist에 `vary`가 있는지는 확인하지 않는다.
- network response의 Vary는 exact request headers와 검증한다: `src/adapters/cache-storage/public-response-cache-adapter.ts:1140,1228-1262`.
- 이후 `unknownResponseHeaderAction="STRIP"`이면 response allowlist에 없는 `vary`를 제거하고(`1264-1280`), 제거된 headers로 Cache에 put한다(`472-479`). 동일 URL variant가 충돌한다.
- activation은 모든 entry를 다시 digest/type/Vary 검증하므로 `592-617`에서 fail-closed한다. 따라서 현재 증거로 private-data disclosure를 주장하면 과장이다. 실제 영향은 impossible candidate에 대한 stage 성공, variant loss, activation/rollback availability 저하다.
**수정**
```ts
if (
policy.allowedVaryHeaderNames.length > 0 &&
!policy.allowedResponseHeaderNames.includes("vary")
) throw new TypeError("Vary must be preserved when variants are enabled.");
```
방어를 겹치려면 `sanitizedResponseHeaders`가 검증된 `Vary`를 generic strip과 무관하게 반드시 보존하도록 한다. **Policy cross-field invariant + fail-fast composition** 패턴이다.
`tests/unit/public-response-cache.test.ts:1030-1155`는 default response allowlist가 이미 `vary`를 포함(`public-cache-policy.ts:54-63`)하므로 이 custom-policy 조합을 놓친다.
### STO-04 — existing cache marker만 확인하는 stage idempotence
`src/adapters/cache-storage/public-response-cache-adapter.ts:424-442`는 cache name이 있고 marker의 release ID/digest/count가 맞으면 모든 cached response의 존재/내용을 보지 않고 stage 성공을 반환한다. marker-last는 첫 stage crash에는 강하지만 marker 이후 browser pressure eviction, manual deletion, partial corruption에는 충분하지 않다. activation이 `592-617`에서 재검증하므로 unsafe publish는 막지만, 같은 manifest로 restage해도 손상 candidate를 복구하지 못한다.
**수정:** `verifyReleaseCandidate(cache, normalized, policy, crypto, signal)`를 factor하고 stage fast path와 activation이 공유한다. 기존 candidate가 missing/mismatch면 owned candidate만 삭제하고 network restage한다. verification 중 abort/unknown error면 active pointer는 건드리지 않고 candidate를 유지 또는 정책대로 삭제하되 성공을 반환하지 않는다. 이는 **idempotent repair, marker as claim not evidence** 패턴이다.
### STO-05 — cache mutation availability가 fetcher에 과결합
`mutationAvailability`는 storage+fetcher+lock 모두를 요구한다: `public-response-cache-adapter.ts:1643-1651`. stage 호출 `392-396`에는 맞지만, fetch하지 않는 activate `538-542`와 cleanup `695-699`에도 같은 guard를 쓴다. 이미 검증된 release를 offline에서 활성화/rollback하거나 quota recovery cleanup하는 기능을 차단한다.
**수정:** operation별 capability guard로 분리한다.
```ts
function stageAvailability(d: Dependencies): BrowserFailureResult | null;
// cacheStorage + mutationLock + fetcher
function localMutationAvailability(
d: Dependencies,
operation: "CACHE_ACTIVATE" | "CACHE_DELETE",
): BrowserFailureResult | null;
// cacheStorage + mutationLock
```
**Dependency segregation**을 적용하고 recovery도 `ONLINE_ONLY`가 아니라 실제 operation에 맞는 `RETRY/REHYDRATE`로 유지한다.
### STO-06 — IndexedDB migration commit 중 duration budget 재확인 없음
- port는 async storage operation 사이 cooperative duration budget을 명시한다: `src/application/ports/browser-file-storage/indexeddb-port.ts:81-89`.
- docs도 각 native operation 사이 monotonic deadline 확인을 요구한다: `docs/architecture/browser-file-and-origin-storage.md:611-615`.
- transform phase는 clock을 확인한다: `src/adapters/storage/indexeddb/indexeddb-maintenance.ts:940-966`.
- 그러나 `commitPrepared`의 read/write/budget/sidecar/checkpoint chain은 `969-1233` 동안 clock을 호출하지 않는다. 최대 500 rows의 IDB callbacks가 invocation deadline 이후에도 계속될 수 있다.
**수정:** transaction을 시작하기 전 composition-owned `minimumCommitReserveMs`를 확인하고, prepared row 수를 budget에 맞춰 더 작게 제한한다. transaction을 연 뒤에는 각 record 시작 시 monotonic deadline을 확인하여 아직 어떤 write도 시작하지 않은 다음 record에서 transaction을 정상 종료하고 last-safe checkpoint까지만 commit한다. 이미 시작한 record의 row/sidecar/budget은 원자 완료하거나 tx 전체 abort해야 하며 부분 truth를 반환하면 안 된다. clock failure는 transaction abort + `UNAVAILABLE`다.
`tests/unit/indexeddb-maintenance.test.ts:562-597`은 transform 시작 전 budget exhaustion만 검증하므로 commit callback 중 clock advance 케이스를 추가한다.
### STO-07 — OPFS worker protocol version/strict response correlation 부재
- request/response envelope에 `protocolVersion`과 echoed `kind`가 없다: `src/adapters/storage/opfs/opfs-worker-protocol.ts:15-137`.
- worker는 requestId+known kind만 1차 검사한다: `opfs-worker-runtime.ts:1643-1669`.
- client는 `{requestId:string, ok:boolean}`만 검사한다: `opfs-worker-client.ts:666-675`. 실패 object/failure code/kind를 strict validate하지 않고 `response.failure.code`를 사용(`171-184`)한다.
- VD-15는 real preflight에서 protocol/schema mismatch를 `INCOMPATIBLE`로 닫으라고 한다: `docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md:476-484`.
**수정:** `OPFS_WORKER_PROTOCOL_VERSION = 2 as const`; 모든 request/response에 version과 kind를 넣고 pending request가 expected kind를 보관한다. closed failure-code set과 per-kind value parser를 적용한다. 먼저 `HELLO/CAPABILITIES` handshake에서 supported physical schema와 protocol version을 교환하고 mismatch면 write/read를 금지한다. generic cancel은 초기 correctness 필수가 아니다. PUT은 effect certainty가 필요한 명시적 `ABORT_PUT`; read/verify RPC는 client-side abandon으로 충분하며, 자원 최적화가 필요할 때만 `CANCEL_REQUEST { targetRequestId }`를 추가한다.
### STO-08 — picker function receiver binding은 browser test로 먼저 확정
- open: `src/adapters/browser-files/browser-file-picker.ts:431-436`
- save/open-authorized callbacks: `src/adapters/browser-files/download-delivery-adapter.ts:201-235`
platform `Window.showOpenFilePicker/showSaveFilePicker`를 options object에 bind할 이유가 없고 Web IDL receiver brand check 가능성이 있다. 다만 현재 코드가 host facade 콜백을 의도했을 수도 있어 확정 전 browser matrix가 필요하다. 우선 실제 `window.showOpenFilePicker`를 전달한 capability test를 추가한다. 실패가 재현되면 API를 `SystemPickerHost { open; save? }`로 만들고 composition에서 올바른 owner에 bind한 host만 주입한다. arbitrary callback(`openAuthorizedSource`, integrity factory)은 bind하지 않고 함수 snapshot 그대로 호출한다.
## 3. 명시적 architecture 결정
### Transaction / crash recovery
- IndexedDB 한 domain mutation은 한 native transaction으로 row, retention sidecar, budget, idempotency receipt/checkpoint를 commit한다. request success가 아니라 transaction `complete`가 성공 truth다.
- IDB와 OPFS/Cache 사이에는 atomic transaction이 없다. OPFS는 journal-authoritative durable saga다. phase는 monotonic이고 physical side effect가 불명확하면 incomplete journal을 유지한다.
- compensation은 원 caller abort와 분리된 bounded signal로 실행한다. cleanup success가 확인될 때만 journal/budget rollback; unknown이면 reconcile owner에게 넘긴다.
- committed object를 in-place repair하지 않는다. 새 physical token/generation에 copy/verify 후 logical CAS publish한다.
### Migration / rollback
- 독립 version 축(IDB DDL, record codec, OPFS journal, OPFS physical, Cache control/release)을 합치지 않는다.
- 공통 순서는 expand → old-writer drain lease → bounded migrate/copy → atomic publish → N-1 observe/rollback window → 별도 contract release다.
- schema downgrade, whole DB/root/cache delete, read-time unbounded rewrite는 금지한다.
- IDB `isOldWriterDrainConfirmed()`는 현재 provider가 전체 migration/contract window를 보장한다는 문서 전제라 현 결함은 아니다. 다음 interface로 temporal guarantee를 실행 가능하게 강화한다:
```ts
export interface OldWriterDrainLease {
readonly leaseId: string;
readonly validUntilEpochMs: number;
assertValid(signal?: AbortSignal): Promise<BrowserDataResult<void>>;
release(): Promise<void>;
}
export interface IndexedDbDataMigrationPolicy<WireValue> {
acquireOldWriterDrainLease(input: Readonly<{
migrationId: string;
targetCodecVersion: number;
scope: IndexedDbDatasetScope;
signal?: AbortSignal;
}>): Promise<BrowserDataResult<OldWriterDrainLease>>;
// migrate/measure 기존 계약 유지
}
```
lease는 batch commit 직전 재검증하고, product rollout owner는 migration 완료 후 rollback/contract window까지 global fence를 유지한다.
### Quota / pressure / eviction
- StorageManager estimate는 rough signal일 뿐 free-space reservation이 아니다. 실제 `QuotaExceededError`가 authority다.
- per-dataset hard budget은 그대로 유지하고, origin coordinator는 Web Lock leader 한 개가 hysteresis(`70/85%`, 하향 `65/80%` 2회)를 적용한다.
- GC 순서: incomplete candidate/stale staging → expired reconstructable → grace 지난 unreferenced chunk → inactive public release → confirmed synced copy → 중지. user-authored/unsynced는 자동 삭제 금지.
- 기본 invocation 100 items/5s, 절대 500/30s. cursor는 owner/policy/release epoch에 binding한다.
- quota retry는 실제 quota rollback, 동일 idempotency/revision/digest, external publish 없음, GC가 실제 제거/pressure 하향, 새 admission token 조건을 모두 만족할 때 정확히 1회만 허용한다.
### Lease / destructive authority
- OPFS mutation Web Lock은 physical delete/cleanup 완료까지 보유한다. transaction-unique physical token이 stale cleanup fencing이다.
- object URL은 registry lease로만 만들고 persistence/log/analytics/global cache에 넣지 않는다. release/dispose는 idempotent다.
- IndexedDB lifecycle authority는 현재 문서대로 composition callback의 short-lived proof를 형식 검증 후 즉시 폐기한다. OPFS와 동일한 replay 방지가 제품 threat model에 필요하면 provider+atomic consumer의 one-shot lease로 별도 강화하되 application caller에게 token을 노출하지 않는다.
### Object URL / preview
- 현재 encoded size/signature/media/active-content denylist는 유지한다.
- 제품 untrusted image preview를 선택하기 전 object URL 발급 **앞**에 bounded header parser + native decode probe를 둔다. static JPEG/PNG/WebP/AVIF 등 명시 allowlist만; SVG/PDF/HTML/XML과 animated image는 별도 격리/re-encode capability가 없으면 attachment-only다.
```ts
export interface PreviewSafetyProbe {
inspect(input: Readonly<{
file: File; // adapter-local only
mediaType: string;
maxEncodedBytes: number;
maxPixels: number;
maxDecodedBytes: number;
maxFrames: number;
deadlineMs: number;
signal: AbortSignal;
}>): Promise<BrowserDataResult<Readonly<{
width: number;
height: number;
frameCount: number;
decodedBytes: number;
}>>>;
}
```
parser 산술은 overflow-safe여야 하고 native `createImageBitmap` 결과는 항상 `close()`. timeout/abort/failure면 `createObjectURL`을 호출하지 않는다.
### Stream / cancellation
- application boundary는 `ByteSource.stream(signal): AsyncIterable<BrowserDataResult<Uint8Array>>`를 유지한다. 첫 failure에서 producer/reader/writer를 모두 닫고 raw DOMException/EOF 성공으로 바꾸지 않는다.
- save stream은 backpressure를 따르고 `writer.close()` 완료 truth가 늦은 abort보다 우선한다. partial destination append/resume로 주장하지 않는다.
- Blob/object URL buffer는 hard cap 아래 fallback에서만 허용한다. public cache는 exact length/digest 검증 때문에 bounded buffer를 유지하되 cap을 넘으면 reader cancel.
- pre-start abort는 side effect 0. IDB 중간 abort는 tx abort. irreversible prompt/persist/close가 완료된 뒤에는 platform truth가 이긴다.
- worker mutation timeout은 effect unknown이지 rollback 확인이 아니다. read RPC는 응답을 버릴 수 있지만 mutation은 journal/explicit abort protocol로 종결한다.
### Worker protocol
- versioned handshake, request kind echo, requestId+kind correlation, strict discriminated parser, closed error set을 채택한다.
- wrong version/schema는 `INCOMPATIBLE` health로 write/read 금지. 이를 failure surface에 노출할 필요가 있으면 `BrowserDataFailureCode``INCOMPATIBLE`을 추가하고 모든 exhaustive mapper/fixture를 함께 갱신한다. 단순 `UNAVAILABLE` retry loop로 숨기지 않는다.
- generic `CANCEL_REQUEST`는 read CPU/resource 최적화로 후순위. PUT correctness는 transaction-scoped `ABORT_PUT`과 durable journal이 담당한다.
### Cache security / eviction
- anonymous same-origin public GET, credentials omit, exact query/request headers/Vary/type/length/digest만 cache한다. auth/private/no-store/no-cache/opaque/redirect/206/range는 계속 금지한다.
- verified marker는 모든 entries 이후 마지막에 쓰되 marker만 증거로 믿지 않는다. stage reuse와 activation/lookup에서 response를 재검증한다.
- current+verified previous release를 유지하고 rollback도 동일 activation validation을 다시 통과한다.
- partial eviction/miss는 `STORAGE_EVICTED` 또는 integrity failure로 fail-closed하고 network rehydrate한다. owned prefix 밖 cache나 user data는 절대 삭제하지 않는다.
## 4. 정확한 파일 변경 계획
### Phase 0 — 즉시 correctness/security fix
**수정**
- `src/application/ports/browser-file-storage/opfs-ports.ts`: v1|v2 prepared object read union, `OpfsPhysicalGenerationId`, journal row physical identity.
- `src/adapters/storage/opfs/opfs-worker-protocol.ts`: explicit abort/cleanup effect, protocol v2 envelope/kind correlation.
- `src/adapters/storage/opfs/opfs-worker-client.ts`: fire-and-forget duplicate abort 제거, strict response parser, coordinator-owned confirmed abort.
- `src/adapters/storage/opfs/opfs-worker-runtime.ts`: tokenized physical path/receipt/manifest, cleanup lock 보유, exact token delete.
- `src/adapters/storage/opfs/opfs-byte-store-adapter.ts`: cleanup result 확인 전 journal rollback 금지; independent compensation deadline; unknown effect reconcile.
- `src/adapters/storage/opfs/indexeddb-opfs-journal.ts`: v2 prepared/journal validation, incomplete row 유지 및 migration metadata.
- `tests/unit/opfs-byte-store.test.ts`, `tests/unit/opfs-worker-runtime.test.ts`, `tests/unit/indexeddb-opfs-journal.test.ts`: 아래 race/crash tests.
- `src/adapters/browser-files/download-delivery-adapter.ts`: boolean validator를 canonical resolver로 변경; absolute URL 실행.
- `tests/unit/browser-file-download.test.ts`: canonical URL 및 hostile base regression.
- `src/adapters/cache-storage/public-cache-policy.ts`: Vary preservation cross-field invariant.
- `src/adapters/cache-storage/public-response-cache-adapter.ts`: stage candidate full verify/self-repair, availability 분리.
- `tests/unit/public-response-cache.test.ts`: custom Vary, damaged candidate, no-fetcher activate/cleanup.
### Phase 1 — bounded lifecycle / protocol / preview promotion
**생성**
- `src/application/ports/browser-file-storage/origin-storage-lifecycle-port.ts`
- `src/adapters/storage/origin-storage-lifecycle-coordinator.ts`
- `tests/unit/origin-storage-lifecycle-coordinator.test.ts`
- `src/adapters/browser-files/browser-image-preview-probe.ts`
- `tests/unit/browser-image-preview-probe.test.ts`
- `src/adapters/storage/opfs/opfs-physical-migration.ts`
- `tests/unit/opfs-physical-migration.test.ts`
- `tests/fixtures/origin-storage/opfs-v1-populated.ts`
- `tests/fixtures/origin-storage/cache-v1-populated.ts`
**수정**
- `src/application/ports/browser-file-storage/index.ts`: 새 lifecycle port export.
- `src/application/ports/browser-file-storage/file.ts`: preview safety policy/result를 native-free 형태로 추가하거나 probe를 adapter-internal dependency로 유지.
- `src/application/ports/browser-file-storage/cache-storage-ports.ts`: bounded maintenance page/cursor input.
- `src/application/ports/browser-file-storage/indexeddb-port.ts`, `src/adapters/storage/indexeddb/indexeddb-types.ts`: drain lease contract.
- `src/adapters/storage/indexeddb/indexeddb-maintenance.ts`: commit reserve/deadline checks 및 lease revalidation.
- `src/adapters/browser-files/object-url-lease.ts`, `src/adapters/browser-files/create-browser-file-runtime.ts`: probe success 전 URL 생성 금지.
- `src/adapters/cache-storage/public-response-cache-adapter.ts`: cursor/deadline bounded inspect/cleanup.
- `src/adapters/storage/opfs/browser-opfs-runtime.ts`: real worker/lock/journal/write-read-delete-cleanup preflight 조립 hook.
- `tests/browser-capabilities/{browser-files,opfs-runtime,public-cache-storage,indexeddb-runtime}.spec.ts``opfs-test.worker.ts`: real engine evidence.
- `docs/architecture/browser-file-and-origin-storage.md`, `docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md`, `docs/operations/browser-file-storage-recovery.md`, `docs/operations/client-cache-and-storage-recovery.md`: 상태를 구현 후에만 `AVAILABLE_NOT_COMPOSED`로 승격.
**삭제/이동**: 없음. v1 reader/fixtures와 old cache prefix는 rollback window 종료 전 삭제하지 않는다. barrel 재배치도 불필요하다.
### Cache bounded port signature
```ts
declare const publicCacheCursorBrand: unique symbol;
export type PublicCacheMaintenanceCursor = string & {
readonly [publicCacheCursorBrand]: "PublicCacheMaintenanceCursor";
};
export type PublicCacheMaintenanceInput = Readonly<{
maxCaches?: number; // default 100, absolute 500
maxDurationMs?: number; // default 5_000, absolute 30_000
cursor?: PublicCacheMaintenanceCursor;
signal?: AbortSignal;
}>;
export type PublicCacheMaintenancePage = Readonly<{
inspectedCaches: number;
deletedCaches: number;
retainedCaches: number;
unreadableCaches: number;
nextCursor: PublicCacheMaintenanceCursor | null;
moreAvailable: boolean;
deadlineReached: boolean;
}>;
cleanupOwned(input?: PublicCacheMaintenanceInput):
Promise<BrowserDataResult<PublicCacheMaintenancePage>>;
inspectOwned(input?: PublicCacheMaintenanceInput):
Promise<BrowserDataResult<PublicCacheMaintenancePage>>;
```
cursor는 caller-readable cache name이 아니며 owned prefix, active pointer epoch, policy fingerprint에 서명/opaque binding한다. stale cursor는 `STALE_RESULT`.
## 5. TDD 계획: 이름, 입력, 기대 결과
| 테스트 이름 | 핵심 입력/fixture | 기대 결과 |
| --- | --- | --- |
| `keeps PREPARING journal when compensating cleanup is aborted or unavailable` | `preparePut` failure; caller signal aborted; worker cleanup `ABORTED/UNAVAILABLE` | `journal.rollback` 미호출, reservation/journal 유지, `OBJECT_RECONCILE` recovery; 후속 same object begin conflict |
| `delayed stale cleanup cannot delete a reused logical generation` | T1 generation 1 abort RPC 지연; T2 generation 1 v2 token으로 commit; T1 cleanup resume | T1 token path만 제거; T2 verify/open bytes 성공; T2 manifest/chunks 유지 |
| `holds the OPFS mutation lease until exact physical cleanup completes` | cleanup delete promise를 gate하고 concurrent begin 시도 | delete 완료 전 T2 lease 미획득; release 후 진행 |
| `does not roll back journal after an unknown worker mutation effect` | cleanup RPC timeout 후 worker operation pending | incomplete journal 유지; reconcile가 exact transaction을 종결 |
| `rejects mismatched OPFS worker protocol and response kind` | v1 response 또는 requestId는 같지만 wrong kind/malformed failure | `INCOMPATIBLE`/closed failure; pending request success로 resolve하지 않음; write side effect 0 |
| `hands off the canonical URL validated against baseOrigin` | `href="downloads/a"`, baseOrigin app, document base evil | host receives `https://app.example/downloads/a`; evil URL never assigned |
| `rejects a policy that enables variants but strips Vary` | allowed vary `accept-language`, allowed response headers without `vary`, STRIP | composition `TypeError`, Cache/fetch side effect 0 |
| `preserves Vary for every stored custom variant` | en/ko same URL with exact request header | stage+activate+both exact match succeed; stored response has Vary |
| `restages an evicted entry even when the release marker remains` | successful stage 후 one asset delete, same manifest stage again | missing asset re-fetch; all entries reverify; success only after repair |
| `activates and cleans a prestaged cache without a fetcher` | seeded valid cache/pointer, cacheStorage+lock, no fetcher | activate/cleanup success; no network call |
| `stops codec migration commit at the cooperative deadline` | fake clock advances during IDB record callbacks, prepared N rows | only last atomically safe prefix+checkpoint commit; `MORE`, `budgetExhausted`; no orphan sidecar/budget delta |
| `requires an old-writer drain lease to remain valid before batch commit` | lease valid at acquire, expires before commit | tx write 0/abort; `BLOCKED`; checkpoint unchanged |
| `rejects oversized raster dimensions before object URL creation` | small encoded PNG with huge width/height or overflow dimensions | `LIMIT_EXCEEDED/POLICY_REJECTED`; `createObjectURL` 0 calls |
| `closes a decoded bitmap on preview abort and failure` | probe aborts after native decode begins | bitmap `close` once, URL 0, closed `ABORTED` |
| `rejects animated and truncated preview containers` | animated WebP/GIF, truncated PNG/JPEG | fail before URL, no leaked decoder resource |
| `pages cache cleanup by count deadline and opaque cursor` | 700 owned caches + foreign caches; max 100/5s | <=100 inspected, foreign untouched, `moreAvailable`, bound cursor; repeated pages converge |
| `rejects cache maintenance cursor after active pointer epoch changes` | page1 cursor 후 activation | `STALE_RESULT`, delete 0 |
| `retries quota failure exactly once only after productive GC` | reconstructable write quota fail, GC deleted >0, same idempotency/digest | attempt 2 최대 한 번; second fail no third; user-authored untouched |
| `uses the real Window receiver for enhanced system pickers` | actual browser `window.showOpenFilePicker/showSaveFilePicker` facade (feature-gated) | supported engine에서 illegal invocation 없음; dismissal closed outcome |
### 실행 명령
```bash
# 가장 빠른 red/green loop
corepack pnpm exec vitest run \
tests/unit/opfs-byte-store.test.ts \
tests/unit/opfs-worker-runtime.test.ts \
tests/unit/indexeddb-opfs-journal.test.ts \
tests/unit/browser-file-download.test.ts \
tests/unit/public-response-cache.test.ts \
tests/unit/indexeddb-maintenance.test.ts \
tests/unit/browser-image-preview-probe.test.ts \
tests/unit/origin-storage-lifecycle-coordinator.test.ts
# 정적 경계
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm check:browser-file-storage-boundaries
corepack pnpm lint
# 실제 browser/storage semantics
corepack pnpm exec playwright test --config playwright.capabilities.config.ts \
tests/browser-capabilities/browser-files.spec.ts \
tests/browser-capabilities/indexeddb-runtime.spec.ts \
tests/browser-capabilities/opfs-runtime.spec.ts \
tests/browser-capabilities/public-cache-storage.spec.ts \
tests/browser-capabilities/storage-manager.spec.ts
# 전체 회귀
corepack pnpm test:unit
corepack pnpm test:browser-file-storage-removal
corepack pnpm verify:documentation
```
## 6. 데이터 호환성, migration, deployment, rollback 순서
1. **즉시 containment:** 제품에 OPFS v1 write가 조립돼 있다면 kill switch로 신규 write를 read-only/export-required로 전환한다. read/export와 journal reconcile는 유지한다. file/IDB/OPFS/cache가 template bootstrap 기본 조립이 아니라는 사실은 영향 범위를 줄이지만 product-specific composition을 확인해야 한다.
2. **N expand release:** journal DDL을 additive upgrade하고 v1+v2 `OpfsPreparedObject` reader를 배포한다. worker protocol v2 handshake를 먼저 넣되 v1 data read는 지원한다. v2 physical path는 unique token을 포함하고 새 write만 v2로 쓴다.
3. **old writer drain:** 모든 N-1 page/worker가 write를 중단했다는 release/lease evidence를 확인한다. BroadcastChannel hint만으로 판단하지 않는다. v2 write traffic은 SHADOW/canary부터 연다.
4. **resume/reconcile:** PREPARING/FILES_READY v1 journal을 bounded하게 처리한다. cleanup effect가 불명확하면 row를 삭제하지 않는다. logical committed v1은 authority이며 in-place 수정하지 않는다.
5. **copy-on-write migration:** v1 committed object → v2 staging/token path → bounded chunk read/copy → manifest/tree digest verify → journal generation/fencing CAS publish. publish 전 crash는 v1, publish 후 crash는 v2가 authority다.
6. **Cache migration:** old active verified release를 byte rewrite하지 말고 새 prefix/control schema에 network restage → full verify → explicit activation. current+previous와 old prefix를 rollback/grace window 동안 유지한다.
7. **Web Storage:** current keys는 registry `DISCARD` semantics를 유지한다. adjacent migration이 제품에 필요할 때만 exact owned old physical key를 read-once/validate/write-current/delete-old한다. 전체 localStorage sweep 금지.
8. **Canary observation:** multi-tab/worker timeout, crash between every phase, partial eviction, quota fault, N-1 read-only/online-only fixture를 통과한다. user-authored bytes export/sync path도 확인한다.
9. **Rollback:** traffic admission과 새 writer부터 끈다. schema/database version을 내리지 않는다. compatible N reader 또는 N-1 online-only/read-only bundle로 전환하고, OPFS는 publish authority에 따라 v1/v2 source를 선택한다. Cache는 검증된 previous release로 같은 activate protocol을 실행한다.
10. **Contract release:** 모든 active/rollback clients drain, grace/authority evidence, historical fixtures 후에만 v1 physical generation/old cache prefix를 bounded cursor cleanup한다. DB/root/cache blanket delete는 하지 않는다.
## 7. 유지해야 할 좋은 설계
- closed `BrowserDataResult`, safe recovery vocabulary, observer exception 격리 및 PII/path/name 비노출.
- File policy가 composition-owned immutable identity이고 selection/inspection/preview/download receipt가 exact file/profile에 binding되는 구조.
- native input baseline과 optional enhanced picker 분리, user activation 전에 await하지 않는 규칙, dismissal과 failure 구분.
- 중앙 object URL lease cap, idempotent revoke/dispose, typed Blob, active-content denylist.
- `ByteSource` chunk별 Result/cancellation, download backpressure, close 완료 truth, bounded object URL fallback.
- Web Storage의 typed registry, physical key versioning, strict exact JSON codec, TTL, quota memory overlay와 tombstone.
- IndexedDB의 opaque physical identity/governance binding, additive-only planner, transaction-complete semantics, CAS/idempotency/retention/budget atomicity, versionchange late-close.
- OPFS의 IDB logical authority, phase journal, immutable digest chunks/refcount, hard budget reservation, fail-closed staging GC, no user-readable physical paths.
- Cache의 anonymous public-only same-origin policy, exact query/header/Vary/type/length/digest, marker-last candidate, explicit activation, current+previous retention, owned-prefix-only cleanup, read/activate 재검증.
- optional adapters를 bootstrap에서 자동 조립하지 않고 `AVAILABLE_NOT_COMPOSED`로 남긴 현재 composition posture.
## 8. 기존 테스트·문서 대조와 false-positive 경계
### 실행한 기존 검증
다음 명령을 이 리뷰 중 실행했고 **7 files / 105 tests 전부 통과**했다.
```bash
corepack pnpm exec vitest run \
tests/unit/opfs-byte-store.test.ts \
tests/unit/opfs-worker-runtime.test.ts \
tests/unit/public-response-cache.test.ts \
tests/unit/browser-file-download.test.ts \
tests/unit/indexeddb-maintenance.test.ts \
tests/unit/indexeddb-runtime.test.ts \
tests/unit/storage-registry.test.ts --reporter=default
```
이는 finding이 현재 green suite가 보호하지 않는 interleaving/custom-policy/browser-base case임을 뜻하며, 기존 behavior가 전반적으로 깨졌다는 뜻은 아니다.
### 반증/과장 방지 표
| 의심 항목 | 기존 증거 | 최종 판단 |
| --- | --- | --- |
| OPFS commit 뒤 finalize cleanup 실패 | `opfs-byte-store.test.ts:504-540`가 COMMITTED row 보존 검증 | 보호됨. STO-01은 **commit 전 cleanup 실패/늦은 RPC + generation reuse**로 좁힘. |
| OPFS concurrent operations | `opfs-worker-runtime.test.ts:232-408`가 lock wait cancel, APPEND/ABORT, authority isolation 검증 | 같은 worker의 active put 일부는 보호됨. journal 조기 rollback 후 cross-context late cleanup은 미검증. |
| Cache Vary가 곧 private leak | activation/lookup이 response를 재검증(`public-response-cache-adapter.ts:592-617,341-365`) | 직접 disclosure 주장은 철회. stage success/variant loss/activation availability 결함으로 Medium. |
| Cache 기본 policy Vary | default response allowlist에 `vary` 포함(`public-cache-policy.ts:54-63`), unit `1030-1155` green | 기본은 보호됨. custom policy cross-field invariant만 결함. |
| damaged cache가 active로 publish | activation full reverify | publish는 fail-closed. STO-04는 idempotent stage/self-repair contract. |
| Web Storage schema mismatch | `storage-registry.test.ts:231-244`가 current physical key의 old envelope discard 검증 | 보호됨. old **physical key** sweep/adjacent migration은 문서상 미구현이며 현재 작은 preference의 readiness gap. |
| IndexedDB transaction success/abort | `indexeddb-runtime.test.ts:319-380`가 commit failure rollback과 abort 검증 | 보호됨. STO-06은 migration commit-loop duration budget에 한정. |
| IndexedDB old-writer drain이 전혀 없음 | maintenance test `269-301`, docs `604-609`가 provider confirmation을 전제 | 현 계약상 provider 책임이므로 결함으로 세지 않음. temporal lease는 enforceability 강화. |
| preview decode safety가 몰래 누락 | `browser-file-and-origin-storage.md:360-365`, VD-15 `19-31,574+`, runbook `96-108`가 미구현을 명시 | regression 아님. 제품 preview promotion blocker(GAP-01). |
| Cache unbounded cleanup이 발견되지 않은 bug | VD-15 `486-515`, runbook `382-429`가 정확히 명시 | known `DESIGNED_NOT_IMPLEMENTED` readiness gap(GAP-02). |
| origin pressure/migration coordinator 부재 | VD-15 `19-31,90-103`, `browser-file-storage-recovery.md:10-14` | known gap. 기존 per-store maintenance를 coordinator로 오인하지 않는다. |
| optional adapters가 bootstrap에 없음 | `runtime-adapters.ts:260-266`; docs status `AVAILABLE_NOT_COMPOSED` | 의도된 skeleton posture, 결함 아님. |
| picker receiver | unit tests가 모두 arrow/fake callback을 사용 | 확정 증거 부족. STO-08은 browser test 선행의 낮은 심각도 hypothesis로 격리. |
## 9. 리뷰 범위 밖으로 확장하지 않은 항목
- Service Worker lifecycle, private/range cache, persistent directory/file handles, Range resumable download는 문서상 별도 `NOT_SELECTED`/`DESIGNED_NOT_IMPLEMENTED` capability다. public cache/file adapter에 섞어 고치지 않는다.
- application/product dataset, schema, rollout authority가 없으므로 optional IndexedDB/OPFS/Cache를 현재 default bootstrap에 새로 조립하지 않는다.
- 전체 origin eviction은 모든 IndexedDB/OPFS/Cache metadata가 함께 사라질 수 있어 client-only로 완전 판별할 수 없다. server rehydrate/export UX와 generation/session authority가 필요하다.
---
최종 권고: STO-01은 production composition이 하나라도 있으면 release blocker로 취급한다. STO-02는 작은 canonicalization patch로 즉시 닫을 수 있다. Cache 세 항목은 동일 변경 묶음으로 TDD하고, VD-15 gap들은 상태 문서를 먼저 바꾸지 말고 executable unit+browser evidence와 rollback fixture가 생긴 후에만 승격한다.
@@ -0,0 +1,405 @@
# Adapter Review — Browser Transfer
> 검토 기준: `develop` / `4dc033c` (2026-08-13)
>
> 범위: `src/adapters/browser-transfer/**`, 직접 연결된 application port, unit test, `docs/architecture/presigned-transfer-and-image-cdn.md`
## 결론
브라우저 전송 계열은 URL·header·subscription material을 application/presentation에서 차단하고, identity capability와 strict decoder를 사용하는 방향이 좋다. 특히 presigned single-use vault, multipart checkpoint CAS, image preset registry와 private descriptor 서명 검증은 유지해야 한다.
다만 실제 조합 전에 해결해야 할 P1 항목이 세 개 있다.
1. presigned download는 `open()`에서 이미 fetch와 timeout을 시작하지만 반환된 source에는 `close()`가 없다. 호출자가 stream을 늦게 열거나 열지 않으면 정상 API 사용만으로 body/timeout 자원이 방치된다 (`BT-PRE-01`).
2. IndexedDB checkpoint partition 삭제는 `BLOCKED`를 반환한 뒤에도 native `deleteDatabase()`가 늦게 commit될 수 있다. 반환 결과가 실제 effect certainty를 표현하지 못한다 (`BT-UP-03`).
3. presigned capability wire envelope에는 top-level protocol literal이 없다. 이미 아키텍처 문서가 요구한 `PRESIGNED_TRANSFER_V1`을 실제 request/response decoder가 아직 강제하지 않는다 (`BT-PRE-02`).
파일 크기만을 이유로 나누면 안 되지만, `resumable-upload-runtime.ts` 2,196줄과 `image-cdn-runtime.ts` 1,340줄은 각각 state transition, I/O orchestration, retry, persistence, presentation projection을 동시에 소유한다. characterization test를 먼저 고정한 뒤 State Machine·Saga·Strategy 경계로 분리하는 것이 안전하다.
## 판정 기준
| 표기 | 의미 |
| --- | --- |
| P1 | 조합 또는 배포 전에 수정. 결과 거짓 보고, 보안/정합성, 자원 수명주기 결함 |
| P2 | 다음 리팩터링 묶음에서 수정. 계약 모호성, 실패 격리, 유지보수 위험 |
| P3 | 동작을 고정한 뒤 정리. 테스트 seam, 중복, 가독성 |
| `VERIFIED_DEFECT` | 현재 코드 경로만으로 재현 가능한 결함 |
| `CONTRACT_GAP` | provider/consumer 간 의미가 타입이나 decoder에 충분히 고정되지 않음 |
| `REFACTOR` | 현재 외부 동작은 보존하면서 내부 책임을 재배치 |
| `PLANNED_GAP` | 기존 아키텍처 문서가 이미 미구현으로 선언한 항목. 현재 구현의 회귀로 계산하지 않음 |
| `KEEP` | 의도와 테스트가 일치하므로 변경하지 않음 |
## 전체 파일 판정
| 모듈 | 현재 역할 | 판정 | 후속 항목 |
| --- | --- | --- | --- |
| `browser-transfer/index.ts` | 하위 capability export | KEEP | public export 증가는 각 capability 계획에서만 수행 |
| `presigned/index.ts` | presigned public surface | KEEP | `BT-PRE-04`에서 vault issuer 노출만 축소 검토 |
| `presigned/incremental-sha256.ts` | streaming SHA-256 | KEEP | WebCrypto `digest()`로 바꾸면 전체 buffering이 되므로 교체 금지 |
| `presigned/presigned-capability-http-provider.ts` | BFF capability 발급, strict decode | CONTRACT_GAP | `BT-PRE-02`, `BT-PRE-03`, `BT-X-01` |
| `presigned/presigned-capability-vault.ts` | identity capability 보관/폐기 | REFACTOR | `BT-PRE-04` |
| `presigned/presigned-transfer-executor.ts` | GET stream/PUT part 실행 | VERIFIED_DEFECT | `BT-PRE-01`, `BT-PRE-03`, `BT-X-01` |
| `resumable-upload/checkpoint-schema.ts` | durable schema guard | KEEP | schema V1 golden fixture 유지 |
| `resumable-upload/fetch-json-transport.ts` | bounded JSON control transport | VERIFIED_DEFECT | `BT-UP-01`, `BT-UP-02`, `BT-X-01` |
| `resumable-upload/http-control-plane-adapter.ts` | operation별 wire decoder | KEEP/REFACTOR | runtime 분리 뒤 decoder만 남김 |
| `resumable-upload/index.ts` | resumable public surface | KEEP | facade 호환 유지 |
| `resumable-upload/indexeddb-checkpoint-store.ts` | scope-bound CAS store/admin | VERIFIED_DEFECT | `BT-UP-03` |
| `resumable-upload/presigned-upload-part-executor.ts` | multipart와 presigned bridge | VERIFIED_DEFECT | `BT-UP-04` |
| `resumable-upload/resumable-upload-runtime.ts` | session state/retry/part scheduling/commit | REFACTOR | `BT-UP-05`, `BT-UP-06` |
| `resumable-upload/runtime-policy.ts` | hard bound snapshot | KEEP | 값 변경은 contract migration으로만 수행 |
| `resumable-upload/upload-byte-source.ts` | stream/range source snapshot과 hashing | KEEP/REFACTOR | runtime에서 source preparation Strategy로 주입 |
| `resumable-upload/upload-cancellation-channel.ts` | best-effort cross-context cancel hint | KEEP | backend/CAS가 authority라는 주석과 동작 유지 |
| `resumable-upload/upload-mutation-lock.ts` | Web Lock exclusive mutation | PLANNED_GAP | `BT-UP-07` |
| `image-cdn/README.md` | 안전한 composition 예제 | KEEP | resolve signal 결정 반영 필요 |
| `image-cdn/browser-image-probe.ts` | bounded fetch/header/static decode probe | VERIFIED_DEFECT | `BT-IMG-02` |
| `image-cdn/image-cdn-policy.ts` | origin/preset/hard-limit registry | KEEP | composition-owned identity reference 유지 |
| `image-cdn/image-cdn-runtime.ts` | asset acceptance, signature, URL/projection | REFACTOR | `BT-IMG-01`, `BT-IMG-03` |
| `image-cdn/image-header-metadata.ts` | PNG/JPEG/WebP/AVIF static header parser | KEEP | 별도 fuzz/golden corpus로 보호; 작은 parser로 임의 분해 금지 |
| `image-cdn/p256-image-capability-verifier.ts` | P-256 P1363 verifier | KEEP | key overlap contract 유지 |
| `image-cdn/index.ts` | image public surface | KEEP | descriptor provider가 생길 때만 export 확장 |
직접 연결 경계도 다음과 같이 대조했다: `src/application/ports/browser-transfer/authorized-download.ts`, `src/application/ports/browser-transfer/image-cdn.ts`, `src/application/ports/browser-transfer/presigned-transfer.ts`, `src/application/ports/browser-transfer/resumable-upload.ts`, barrel `src/application/ports/browser-transfer/index.ts`, 그리고 presigned source의 직접 consumer `src/adapters/browser-files/download-delivery-adapter.ts`. native URL/header/File/Response를 application port로 올리지 않는 방향은 유지하며, `BT-PRE-01``close()` migration은 이 consumer까지 포함한다.
## Presigned transfer 상세
### BT-PRE-01 — `open()`이 반환되기 전에 download lease가 시작됨
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: `presigned-transfer-executor.ts:114-192`, `:408-631`
- 현재 동작:
- `openDownload()`이 capability를 claim/consume한 뒤 즉시 `fetch()`를 수행한다.
- timeout scope도 `open()` 안에서 시작한다.
- response body와 scope는 반환된 `PresignedDownloadByteSource.stream()`을 완주하거나 실패해야만 해제된다.
- source port에는 `close()`/`dispose()`가 없다.
- 영향:
- 호출자가 source를 받은 뒤 stream 시작을 늦추면, 실제 consumer deadline이 아니라 `open()` 시점의 timeout으로 실패한다.
- 호출자가 stream을 열지 않으면 body cancellation과 listener/timer cleanup을 명시적으로 수행할 방법이 없다.
- capability는 이미 single-use로 소비되므로 동일 source를 복구할 수도 없다.
결정: **lazy, single-start lease로 변경한다.** `open()`은 policy/vault 검증과 capability consume까지만 수행하고 fetch는 첫 `stream(signal)` 진입 시 시작한다. source에 `close(): void`를 추가해 미사용 lease도 명시적으로 폐기한다. `close()`와 stream의 first-start는 하나의 state machine을 공유한다.
```ts
type PresignedDownloadByteSource = Readonly<{
byteLength: number;
capability: PresignedDownloadCapability;
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION";
stream(signal: AbortSignal): AsyncIterable<BrowserDataResult<Uint8Array>>;
close(): void;
}>;
type DownloadLeaseState = "READY" | "STREAMING" | "CLOSED";
```
구현 규칙:
1. `READY -> STREAMING`만 fetch를 시작한다.
2. `READY -> CLOSED`는 network I/O 없이 끝낸다.
3. `STREAMING -> CLOSED`는 composed signal abort, reader/body cancel, timer/listener release를 한 번만 수행한다.
4. 두 번째 `stream()`은 기존처럼 `CONFLICT / REISSUE_CAPABILITY`다.
5. digest 성공 전 chunk는 현재의 `VERIFIED_ON_SUCCESSFUL_EXHAUSTION` 의미를 유지한다. consumer는 최종 success 전 파일을 commit하면 안 된다.
6. 첫 `stream()` 직전에 capability expiry와 minimum remaining lifetime을 다시 확인하고, `open()` 때 받은 outer signal과 stream signal을 함께 적용한다. 오래 보관되어 만료된 source는 fetch를 시작하지 않는다.
테스트 추가 (`tests/unit/presigned-transfer.test.ts`):
- `does not fetch until the returned download source starts streaming`
- `closes an unused source without issuing a request`
- `starts the transfer deadline at first stream consumption`
- `close during a pending read cancels the reader and releases listeners once`
- `stream after close returns one terminal conflict without fetching`
마이그레이션: port에 `close()`를 추가한 뒤 직접 consumer인 `src/adapters/browser-files/download-delivery-adapter.ts`를 포함한 모든 consumer를 source 획득 직후 `try/finally { source.close(); }`로 감싼다. size reject, `createWritable()`/prompt 실패, object-URL strategy의 stream 전 실패도 `tests/unit/browser-file-download.test.ts`로 고정한다. 그 다음 fetch를 lazy로 옮긴다. rollback은 eager fetch 구현으로 되돌릴 수 있지만 `close()` API는 유지한다.
완료 조건: 위 테스트와 기존 presigned suite가 통과하고, source를 생성만 한 테스트에서 fetch 호출 수와 active timer가 모두 0이다.
### BT-PRE-02 — capability wire envelope의 protocol version 부재
- 우선순위/분류: **P1 / CONTRACT_GAP**, 기존 문서의 미완료 항목
- 근거: `presigned-capability-http-provider.ts:173-210`, `:436-462`; `docs/architecture/presigned-transfer-and-image-cdn.md:93-108`
- 현재 동작: request body와 strict response key set에 top-level transfer protocol이 없다. multipart binding 내부 protocol만으로 전체 capability envelope version을 식별한다.
- 영향: 서버가 필드를 추가/재해석할 때 old/new client가 같은 shape를 서로 다른 의미로 받아들일 수 있다. strict decoder라서 단순 필드 추가도 곧바로 장애가 되지만, 장애가 version mismatch로 분류되지 않는다.
결정:
- request와 response에 `protocol: "PRESIGNED_TRANSFER_V1"`을 필수로 추가한다.
- missing/unknown protocol은 현재 closed taxonomy의 `POLICY_REJECTED`, retryable `false`, recovery `REISSUE_CAPABILITY`로 닫는다. 이 변경에서 새 failure code를 만들지 않는다.
- multipart의 `PRESIGNED_MULTIPART_V1`은 하위 binding protocol로 그대로 유지한다.
- protocol은 `PresignedTransferCapability`, `PresignedCapabilityRegistration/Binding`, vault snapshot, executor common-binding validator까지 전파해 request → registration → consumption exact parity를 보장한다.
- server는 request shape를 협상해 legacy request에는 legacy response, V1 request에는 V1 response를 반환한다. strict legacy decoder를 깨뜨리므로 legacy response에 V1 field를 먼저 emit하거나 한 response에 dual fields를 넣지 않는다.
테스트 추가:
- request body exact-key snapshot과 protocol literal
- missing, V0, V2 protocol response 거절
- V1 download와 V1 multipart capability 수락
- protocol mismatch가 vault `register()` 전에 종료됨
배포 순서: request-shape negotiated provider 배포 → V1 client 배포 → old-client drain 기간 관찰 → provider legacy request/response 제거. rollback 시 provider는 두 request shape를 계속 수락하되 각각 matching exact response를 반환한다.
### BT-PRE-03 — timeout이 non-cooperative fetch를 실제로 bound하지 못함
- 우선순위/분류: **P2 / VERIFIED_DEFECT**
- 근거: capability provider `:186-214`, executor `presigned-transfer-executor.ts:157-190`, 각 파일의 `createAbortScope()`
- 현재 동작: timer는 AbortController만 abort한다. injected fetcher 또는 host가 signal을 무시하면 `await fetcher(...)` 자체는 끝나지 않는다.
- 영향: API가 선언한 timeout이 hard bound가 아니며, teardown도 fetch settlement에 묶인다.
결정: 공통 `AbortableOperationScope``race(task, onLateValue)`를 사용한다 (`BT-X-01`). deadline/caller abort가 먼저 끝나면 즉시 typed failure를 반환하고, 늦게 온 `Response`는 body를 취소한다. timer 생성 실패 시 이미 붙인 external listener를 즉시 제거한다.
테스트 추가:
- signal을 무시하는 fetch Promise가 timeout 뒤에도 pending인 fixture
- timeout 결과가 정시에 반환되고 late response body가 취소되는지 검증
- scheduler `setTimeout`/`clearTimeout` throw 시 listener 누수와 public rejection이 없는지 검증
### BT-PRE-04 — vault가 스스로 registration invariant를 소유하지 않음
- 우선순위/분류: **P2 / REFACTOR**
- 근거: `presigned-capability-vault.ts:112-174`
- 현재 동작: HTTP provider가 URL, header, expiry, byte/digest를 검사하지만 exported vault의 `register()`는 전달받은 registration을 그대로 snapshot한다.
- 영향: 다른 issuer adapter가 추가되거나 테스트/조합 코드가 vault를 직접 사용하면 동일한 capability 타입에 더 약한 invariant가 들어갈 수 있다.
결정: issuer/consumer 권한을 wiring 단계에서 분리하고 공통 invariant validator를 적용한다.
1. `createPresignedCapabilityVault()``{ issuer: PresignedCapabilityIssuer; consumer: PresignedCapabilityConsumer }`를 반환한다. provider option에는 issuer만, executor option에는 consumer만 전달한다. root barrel에는 factory와 consumer-facing type만 export하고 issuer type은 provider의 구조적 parameter로 숨긴다.
2. issuer 등록 직전 공통 `validatePresignedCapabilityRegistration()`으로 method/binding/URL/header/status/bytes/digest/expiry를 다시 검증한다.
3. HTTP decoder는 wire-specific shape를 검사하고, vault validator는 runtime invariant만 검사한다. decoder 로직을 통째로 중복하지 않는다.
테스트: malformed registration을 직접 issuer seam에 넣는 table test와, HTTP provider의 valid 결과가 동일 snapshot으로 등록되는 parity test를 추가한다.
### BT-PRE-05 — encoded path의 provider 해석 차이
- 우선순위/분류: **P2 / SECURITY_HARDENING**
- 근거: `presigned-capability-http-provider.ts:517-533`, `:1078-1097`
- 현재 동작: literal `.`/`..`와 backslash는 거절하지만 `%2f`, `%5c`, `%25...` 같은 encoded separator가 object-store/CDN에서 한 번 더 decode되는지 계약이 없다.
- 결정: raw `URL.pathname`의 각 segment를 strict UTF-8 percent-decode한다. decoded segment에서 `/`, backslash, NUL, `.`/`..`, 그리고 literal `%` 뒤 두 hex digit을 거절한 뒤, 대문자 percent-hex canonical encoder 결과와 raw segment를 비교한다. 이 규칙은 `%252e%252e` double encoding을 닫고 valid opaque UTF-8 segment는 허용한다. CDN/provider conformance fixture가 같은 canonicalizer를 사용한다.
- 테스트: `%2F`, `%5C`, `%252e%252e`, mixed-case encoding, valid UTF-8 opaque segment를 포함한다.
## Resumable upload 상세
### BT-UP-01 — AbortSignal 구조 검증과 cleanup 사용이 불일치
- 우선순위/분류: **P2 / VERIFIED_DEFECT**
- 근거: `fetch-json-transport.ts:548-582`, `:670-677`
- 현재 동작: `isAbortSignal()``aborted``addEventListener`만 검사하지만 `FetchAttempt.release()``removeEventListener()`를 무조건 호출한다.
- 영향: 구조적으로 허용된 signal이 finally에서 throw하여 typed result 대신 Promise rejection을 만든다.
- 수정: native getter 기반 또는 최소한 `removeEventListener`까지 포함한 공통 guard를 사용하고, release cleanup은 terminal result를 덮지 않도록 catch한다.
- 테스트: remove가 없는 structural fake는 입력에서 `INVALID_INPUT`; remove가 cleanup 중 throw하는 hostile facade는 typed terminal result를 보존.
### BT-UP-02 — transport clock/scheduler가 전역에 고정됨
- 우선순위/분류: **P3 / REFACTOR**
- 근거: `fetch-json-transport.ts:571-580`, `:626-636`
- 현재 동작: request timeout은 global timer, HTTP-date `Retry-After``Date.now()`를 직접 사용한다.
- 결정: dependencies에 `clock.now()``scheduler`를 추가하고 snapshot/validate한다. delta-seconds와 HTTP-date parsing은 같은 captured `now`를 사용한다.
- 테스트: fake clock으로 경계값, clock rollback, invalid date, max clamp를 결정론적으로 검증.
### BT-UP-03 — `deleteDatabase()` timeout 뒤 late delete effect
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: `indexeddb-checkpoint-store.ts:375-439`
- 현재 동작: `deletePartition()``onblocked` 후 timer가 끝나면 `BLOCKED`를 반환한다. 그러나 IndexedDB delete request는 취소할 수 없고, 다른 tab이 닫히면 반환 이후 `onsuccess`로 실제 DB가 삭제될 수 있다.
- 영향: caller가 `BLOCKED``NOT_APPLIED`로 해석할 수 있지만 native request는 나중에 성공/실패할 수 있어 반환값과 effect certainty가 모순된다. 다른 realm의 open/delete ordering까지 현재 증거 없이 단정하지 않는다.
결정: delete dispatch 이후에는 failure certainty를 `NOT_APPLIED`로 표현하지 않는다. port outcome을 다음처럼 명시한다.
```ts
type PartitionDeleteOutcome =
| { state: "DELETED"; effect: "APPLIED" }
| { state: "PENDING"; effect: "UNKNOWN"; reason: "BLOCKED_DEADLINE" };
```
- pre-dispatch invalid/aborted/unsupported만 기존 failure다.
- `PENDING`을 받은 runtime은 해당 store instance를 terminal closed로 유지한다. 같은 JS realm에서는 `(IDBFactory identity, databaseName)` pending-deletion registry가 새 factory 생성을 막고 late `onsuccess/onerror`에서 해제한다. 다른 realm은 native IndexedDB blocked ordering과 명시적 recovery UX로 처리하며 client-only global registry를 주장하지 않는다.
- late `onsuccess`/`onerror`는 observer에 기록한다. 다시 확인하려면 별도 `inspectPartitionDeletion()` 또는 새 page generation에서 DB 목록/open 결과를 사용한다.
- 단순히 timer를 제거해 무한 대기시키지는 않는다.
테스트 추가 (`tests/unit/resumable-upload-checkpoint.test.ts`): blocked deadline → PENDING → late success, blocked deadline → late error, PENDING 뒤 store method가 UNAVAILABLE, caller abort before dispatch, concurrent new runtime 금지.
### BT-UP-04 — bridge clock의 non-finite 값이 expiry 검사를 통과함
- 우선순위/분류: **P2 / VERIFIED_DEFECT**
- 근거: `presigned-upload-part-executor.ts:36-70`
- 현재 동작: `now()``NaN` 또는 음수이면 expiry 비교를 우회한다. `+Infinity`는 현재도 expiry 비교에서 거절되지만 dependency failure가 capability policy failure로 잘못 분류된다.
- 수정: `Number.isSafeInteger(nowEpochMs) && nowEpochMs >= 0`을 먼저 검사하고 실패 시 `UNAVAILABLE / RESUME`을 반환한다.
- 테스트: NaN/음수의 현재 bypass, +Infinity의 현재 rejection, 수정 후 모든 non-finite/negative clock의 `UNAVAILABLE / RESUME`, throw, 만료 경계 `expiresAt === now`, 유효 `now + 1`.
### BT-UP-05 — runtime의 상태 전이와 side effect가 한 파일에 결합됨
- 우선순위/분류: **P2 / REFACTOR**
- 근거: `resumable-upload-runtime.ts` 2,196줄; session resolution, retry, hashing, scheduler, CAS, abort saga, validation과 telemetry를 함께 소유
- 외부 facade는 유지하고 다음 내부 경계만 추출한다.
| 새 내부 모듈 | 책임 | 적용 패턴 |
| --- | --- | --- |
| `upload-session-state-machine.ts` | ACTIVE/ABORT_PENDING/completed transition의 순수 함수 | State Machine |
| `upload-session-reconciler.ts` | local checkpoint와 server status 수렴 | Reconciler |
| `upload-part-scheduler.ts` | memory/server/client concurrency와 receipt serialization | Bounded Work Queue |
| `upload-retry-executor.ts` | retry budget, Retry-After, jitter, attempt deadline | Policy + Template Method |
| `upload-abort-saga.ts` | local tombstone → backend abort → checkpoint removal | Saga/Compensation |
| `resumable-upload-runtime.ts` | public facade, lifecycle, mutation lock orchestration만 | Facade |
추출 순서:
1. 기존 `tests/unit/resumable-upload-runtime.test.ts`에 observable call-order characterization를 추가한다.
2. 순수 transition 함수와 table test를 먼저 만든다.
3. retry executor, reconciler, part scheduler, abort saga 순서로 한 모듈씩 이동한다.
4. 각 이동 뒤 기존 suite 전체를 그대로 실행한다. fixture expected 값을 리팩터링에 맞춰 바꾸지 않는다.
변경 금지:
- part idempotency key derivation
- server-authoritative status reconciliation
- accepted receipt의 순차 CAS persistence
- checkpoint에 URL/credential을 저장하지 않는 규칙
- first part failure 뒤 이미 시작한 sibling의 확정 receipt를 기다려 저장하는 현재 정책. 이를 즉시 cancel하면 remote success가 ambiguous해질 수 있으므로 별도 behavior change로 다룬다.
### BT-UP-06 — sync `close()`가 drain 완료를 증명하지 못함
- 우선순위/분류: **P2 / LIFECYCLE_REFACTOR**
- 근거: `resumable-upload-runtime.ts:280-287`
- 현재 동작: lifetime abort 직후 checkpoint store를 닫고 반환한다. native fetch/IDB가 signal에 반응해 정리될 것으로 기대하지만 caller는 active operation의 terminal settlement를 기다릴 수 없다.
- 결정: application `ResumableUploadPort``close(): void`는 admission을 닫고 같은 single-flight drain을 시작하는 호환 facade로 유지한다. adapter runtime lifecycle surface에 향후 composition owner가 `await``dispose(): Promise<void>`를 추가한다. `dispose()`는 이미 시작된 drain promise를 공유하고 active operation registry를 abort한 뒤 bounded `allSettled` 후 store/channel을 닫는다. 현재 production bootstrap consumer가 있다고 가정하지 않는다.
- 테스트: close 중 신규 admission 거절, active fetch/IDB abort, 중복 dispose single-flight, cleanup deadline, late provider success가 checkpoint를 다시 쓰지 못함.
### BT-UP-07 — Web Locks 비지원 정책이 composition 결과로 표현되지 않음
- 우선순위/분류: **P1 before composition / PLANNED_GAP**
- 근거: `upload-mutation-lock.ts:19-58`; 아키텍처 completion ledger의 optional capability decision
- 현재 동작: factory는 LockManager가 없으면 throw한다. multi-tab 안전성을 희생하는 in-memory fallback은 없다.
- 결정: silent fallback은 추가하지 않는다. composition이 Web Locks 미지원 시 resumable upload capability를 `UNSUPPORTED`로 명시하고 일반 foreground upload 또는 재선택 UX로 degrade한다. 실제 지원 browser matrix가 확정되기 전 default composition에는 설치하지 않는다.
## Image CDN 상세
### BT-IMG-01 — resolve signal을 일관되게 필수화할지에 대한 API 단순화
- 우선순위/분류: **P3 / API CONSISTENCY DECISION**, 현재 동작 결함 아님
- 근거: application port `image-cdn.ts:167-172`; runtime `image-cdn-runtime.ts:518-527`
- 현재 동작: optional signal을 허용하고 `PRIMARY_REQUIRED` preset은 signal 부재를 명시적 `UNSUPPORTED`로 표현한다. 문서가 signal 없는 probe 성공을 약속하지 않으므로 defect는 아니다.
- 결정: hidden preset precondition을 줄이기 위해 다음 major contract 정리에서 `resolve()` signal을 필수화한다. 이는 runtime correctness fix가 아니라 API consistency 개선이다.
- 마이그레이션: `tests/fixtures/typecheck/invalid-image-cdn-resolve-signal.ts`와 대응 typecheck script를 먼저 추가하고 모든 caller/README에 lifecycle signal을 전달한 뒤 port와 optional 분기를 바꾼다. P1/P2와 같은 PR에 섞지 않는다.
### BT-IMG-02 — Cache-Control quoted value parser가 malformed 값을 수락
- 우선순위/분류: **P2 / VERIFIED_DEFECT**
- 근거: `browser-image-probe.ts:272-373`
- 현재 동작: `rawValue.replace(/^"|"$/gu, "")`는 한쪽 quote만 있는 `max-age="60` 또는 `max-age=60"`도 숫자 `60`으로 만들 수 있다.
- 영향: probe가 malformed cache policy를 immutable public response로 승인할 수 있다.
- 수정:
- comma split 전에 quote/escape-aware tokenizer를 사용해 quoted extension의 comma를 directive 경계로 취급하지 않는다.
- quoted-string은 시작/종료 quote가 모두 있고 escape/control 문자가 유효할 때만 unquote한다.
- numeric directives는 unquoted digits 또는 완전한 quoted digits만 허용한다.
- private response는 `no-store`가 필수이며 `public`, `private`, `immutable`, `max-age`, `s-maxage`, `no-cache`, `must-revalidate`, `proxy-revalidate`가 함께 있으면 fail-closed한다. 문법상 유효한 unknown extension만 무시한다.
- parser는 중복 directive를 계속 거절한다.
- 테스트: unmatched quote, escaped quote, duplicate, comma-in-quoted extension, contradictory public/private directives, valid quoted max-age.
### BT-IMG-03 — acceptance, verification, URL projection의 응집도 분리
- 우선순위/분류: **P3 / REFACTOR**
- 근거: `image-cdn-runtime.ts` 1,340줄
- facade와 WeakMap capability identity는 유지하고 다음 내부 모듈만 추출한다.
| 새 내부 모듈 | 책임 |
| --- | --- |
| `image-asset-decoder.ts` | public/private exact shape snapshot |
| `image-capability-verification.ts` | canonical payload, digest, key verifier deadline |
| `image-presentation-projector.ts` | candidate URL/srcset/descriptor 생성 |
| `image-cdn-runtime.ts` | issued reference WeakMap, close, facade orchestration |
`image-header-metadata.ts`는 format parser라는 단일 책임을 이미 가진다. LOC만 보고 더 쪼개지 말고 fuzz corpus와 malformed container table을 보강한다.
### BT-IMG-04 — descriptor provider/refresh는 아직 구현 대상
- 우선순위/분류: **P1 before composition / PLANNED_GAP**
- 근거: `docs/architecture/presigned-transfer-and-image-cdn.md:574-610`
- 현재 상태: signature 검증/runtime/probe는 있으나 BFF에서 descriptor를 가져오고 single-flight refresh하는 provider와 `<picture>` renderer는 없다.
- 결정: 현재 runtime을 직접 product composition에 노출하지 않는다. 향후 provider는 `protocol: "IMAGE_CDN_DESCRIPTOR_V1"`, exact authority/request binding, minimum remaining TTL, single-flight refresh, close-generation fence를 필수로 한다. renderer는 descriptor 필드만 투영하고 alt/error/placeholder 정책은 feature 소유로 둔다.
## 공통 개선
### BT-X-01 — abort/deadline/late-result mechanics 통합
- 우선순위: **P2 / REFACTOR**
- 중복 근거: presigned provider/executor, image probe/runtime, resumable runtime, browser files, HTTP, Web Push에 `createAbortScope`, `combineAbortSignals`, `awaitWithAbort`, `readWithSignal` 변형이 반복된다.
- 결정: result taxonomy는 각 adapter에 남기고 **mechanics만** `src/adapters/platform/abortable-operation.ts`로 추출한다.
필수 API:
```ts
type AbortableOperationScope = Readonly<{
signal: AbortSignal;
terminal(): "OPEN" | "CALLER_ABORT" | "DEADLINE" | "CLOSED";
race<T>(
task: Promise<T>,
onLateValue?: (value: T) => void,
): Promise<
| { kind: "VALUE"; value: T }
| { kind: "TERMINAL"; terminal: "CALLER_ABORT" | "DEADLINE" | "CLOSED" }
>;
close(): void;
}>;
```
불변식:
- caller abort와 deadline 중 최초 하나만 terminal authority다.
- `close()`는 idempotent하고 timer/listener cleanup throw를 삼킨다.
- late rejection은 항상 관찰되어 unhandled rejection이 되지 않는다.
- late `Response`/`ImageBitmap`/native handle은 caller가 제공한 compensator로 닫고 값을 버린다. 각 subsystem adapter가 `TERMINAL`을 자기 Result taxonomy로 변환한다.
- 이 utility는 `BrowserDataResult`, `WebPushResult`, HTTP outcome을 import하지 않는다.
적용 순서: 새 utility golden test → presigned → image → resumable transport → 다른 adapter. 한 PR에서 모든 subsystem을 동시에 바꾸지 않는다.
## 유지해야 할 설계
- raw presigned URL/header가 application port를 통과하지 않고 identity capability vault 안에만 존재한다.
- capability는 exact WeakMap identity이며 single-use claim 후 vault에서 제거된다.
- upload byte는 hash/network await 전에 snapshot한다.
- multipart checkpoint에는 URL, credential, capability material을 저장하지 않는다.
- multipart receipt는 revision CAS로 순차 commit하고 server status가 복구 authority다.
- cross-context cancellation은 hint일 뿐 backend idempotency/Web Lock/CAS를 대체하지 않는다.
- public image는 revision rollover, private image는 signed expiry/revocation으로 구분한다.
- private image는 exact signed URL, credential omit, no-store, static container와 decode budget을 확인한다.
- composition hard limit은 adapter implementation ceiling보다 느슨해질 수 없다.
- P-256 key overlap set과 terminal `close()` generation fence를 유지한다.
## 실행 순서와 의존성
1. `BT-UP-03`, `BT-PRE-01`, `BT-PRE-02`를 각각 독립 PR로 해결한다.
2. `BT-X-01` utility golden test를 만들고 `BT-PRE-03`, `BT-UP-01`, `BT-UP-02`를 이관한다.
3. `BT-UP-04`, `BT-IMG-01`, `BT-IMG-02`, `BT-PRE-04/05`를 작은 contract-hardening PR로 처리한다.
4. behavior suite가 모두 green인 뒤 `BT-UP-05/06`, `BT-IMG-03` 구조 분리를 수행한다.
5. 실제 product 선택이 있을 때만 `BT-UP-07`, `BT-IMG-04`를 composition plan으로 연다.
각 PR 공통 gate:
```bash
corepack pnpm exec vitest run tests/unit/presigned-transfer.test.ts \
tests/unit/resumable-upload-checkpoint.test.ts \
tests/unit/resumable-upload-fetch-transport.test.ts \
tests/unit/resumable-upload-http-control-plane.test.ts \
tests/unit/resumable-upload-runtime.test.ts \
tests/unit/image-cdn-runtime.test.ts
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm lint
git diff --check
```
실제 browser gate도 capability promotion 전에 실행한다.
```bash
corepack pnpm test:browser-capabilities -- \
tests/browser-capabilities/presigned-streaming.spec.ts \
tests/browser-capabilities/resumable-upload.spec.ts \
tests/browser-capabilities/image-cdn.spec.ts
```
해당 browser/provider 환경이 없으면 이 gate는 `UNVERIFIED`로 남기며 capability availability를 승격하지 않는다.
## 구현 완료 정의
- 모든 P1 항목에 failing-before/fixed-after test가 있다.
- wire version과 migration 순서가 provider fixture에 반영된다.
- 어떤 timeout 경로도 non-cooperative Promise 때문에 public API를 무한 대기시키지 않는다.
- delete partition 결과가 late native commit 가능성을 숨기지 않는다.
- runtime facade의 public capability identity, failure taxonomy, persisted V1 schema는 명시된 migration 외에는 바뀌지 않는다.
- 기존 문서의 `AVAILABLE_NOT_COMPOSED`/`PLANNED_GAP` 상태를 code defect 완료로 오인하지 않는다.
@@ -0,0 +1,344 @@
# Adapter Review — Service Worker and Web Push
> 검토 기준: `develop` / `4dc033c` (2026-08-13)
>
> 범위: `src/adapters/service-worker/**`, `src/adapters/web-push/**`, `src/contracts/service-worker.ts`, `src/contracts/web-push.ts`, 관련 build input·unit test·architecture 문서
## 결론
서비스 워커는 registration ownership, static asset install의 byte/digest 검증, activation drain handshake, `clients.claim()` 금지와 staged removal이라는 좋은 기반을 갖고 있다. Web Push도 raw endpoint/key를 durable control record에서 분리하고, push/click 전에 association fence를 두 번 확인하며, notification copy/route를 closed registry로 제한한다. 이 경계들은 유지해야 한다.
현재 코드에는 조합 전에 고쳐야 할 P1 항목이 있다.
- generated manifest는 root-relative URL을 가지지만 fetch 분류는 absolute `Request.url`과 비교해 정적 cache path가 사용되지 않을 수 있다 (`SW-URL-01`).
- Cache Storage 전체에서 match하여 현재 release가 아닌 구 cache response를 반환할 수 있다 (`SW-01`).
- reset가 소유권 parser가 아니라 문자열 prefix만 사용해 유사 이름의 타 cache까지 삭제한다 (`SW-02`).
- `unregister()``false`를 성공으로 보고하며 removal mode도 실패/ownership mismatch를 `DISABLED`로 숨긴다 (`SW-03`, `SW-04`).
- build input의 static manifest decoder가 asset row와 set digest를 실제로 검증하지 않는다 (`SW-05`).
- Push fence CAS adapter가 repository의 다음 revision을 확인하지 않고, deadline 뒤 late mutation effect도 표현하지 못한다 (`WP-01`, `WP-02`).
- backend registration response가 request의 전체 authority를 echo/bind하지 않아 client가 잘못 묶인 association을 검출할 수 없다 (`WP-03`).
Web Push는 현재 `AVAILABLE_NOT_COMPOSED`이고 제품 선택도 `NOT_SELECTED`다. service worker entry에 연결되지 않은 사실 자체는 회귀가 아니다. 아래 P1 계약을 해결하고 product-owned registry/provider/consent가 준비되기 전에는 default composition에 추가하지 않는다.
## 파일별 판정
| 파일 | 역할 | 판정 | 후속 |
| --- | --- | --- | --- |
| `service-worker-entry.ts` | 단일 physical worker entry와 event wiring | KEEP/REFACTOR | `SW-06`, `SW-10`; 두 번째 registration 생성 금지 |
| `service-worker-lifecycle.ts` | install/activate/fetch/activation/reset | VERIFIED_DEFECT | `SW-URL-01`, `SW-01`, `SW-02`, `SW-07`, `SW-08` |
| `service-worker-page-controller.ts` | registration/update/activation/reset page facade | VERIFIED_DEFECT | `SW-04`, `SW-06` |
| `service-worker-protocol.ts` | page-worker strict message codec/nonce | CONTRACT_GAP | `SW-06`, `SW-10` |
| `service-worker-removal.ts` | exact registration/cache ownership cleanup | VERIFIED_DEFECT | `SW-03` |
| `service-worker-static-assets.ts` | static manifest/install/cache policy | KEEP/REFACTOR | `SW-01`, `SW-05`, `SW-09` |
| `web-push/index.ts` | public exports | KEEP | product selection 전 surface 확대 금지 |
| `web-push/notification-registry.ts` | closed copy/route registry | KEEP | arbitrary copy/URL 허용 금지 |
| `web-push/push-association-fence-store.ts` | durable authority state machine | VERIFIED_DEFECT | `WP-01`, `WP-02` |
| `web-push/push-codec.ts` | bounded hint/click codec | KEEP | exact keys, expiry, no raw text 유지 |
| `web-push/push-registration-gateway.ts` | fixed backend commands/decoders | CONTRACT_GAP | `WP-03`, `WP-04` |
| `web-push/push-subscription-adapter.ts` | window consent/native/backend/local orchestration | VERIFIED_DEFECT/REFACTOR | `WP-04`, `WP-05`, `WP-06` |
| `web-push/runtime-support.ts` | deadline/link/observation mechanics | CONTRACT_GAP | `WP-02`, `WP-07` |
| `web-push/service-worker-runtime.ts` | push/click/subscriptionchange handler composition | REFACTOR | `WP-06` |
| `web-push/service-worker-scope-host.ts` | native scope facade | KEEP | single worker entry 내부에서만 사용 |
| `web-push/inbound/push-event-adapter.ts` | hint → fence → safe notification | KEEP/CONTRACT_GAP | `WP-07` |
| `web-push/inbound/notification-click-adapter.ts` | click → fence → safe route handoff | KEEP/CONTRACT_GAP | `WP-07` |
직접 경계 inventory도 대조했다: `src/contracts/service-worker.ts`는 protocol/cache ownership identity, `src/contracts/web-push.ts`는 push protocol/selection을 소유한다. `src/bootstrap/register-service-worker.ts`는 page composition, `scripts/lib/service-worker-build-input.ts``scripts/generate-service-worker-assets.ts`는 build decode/generation, `vite.service-worker.config.ts`는 worker bundle entry를 소유한다. 이 파일들은 `SW-05`/`SW-10`의 shared codec과 rollout scope에 포함한다.
## Service Worker 상세
### SW-URL-01 — generated root-relative manifest와 absolute fetch URL의 분류 불일치
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: generator `scripts/generate-service-worker-assets.ts`는 asset URL을 `/assets/...`로 생성하고, `service-worker-lifecycle.ts`는 그 문자열 set을 absolute `Request.url`과 직접 비교한다.
- 영향: generator output을 그대로 사용하면 verified static URL이 manifest member로 분류되지 않아 current cache lookup path에 들어가지 않고 network fallback이 된다. `SW-01`의 cache 선택을 고쳐도 URL identity를 먼저 맞추지 않으면 cache path는 여전히 작동하지 않는다.
- 결정: runtime 생성 시 각 root-relative manifest URL을 `new URL(asset.url, scope.registrationScope).href`로 canonicalize하고 same-origin을 재확인한 frozen absolute URL set을 만든다. install cache key, fetch classification, lookup/delete validation이 이 canonical URL identity를 공유한다. generator의 persisted manifest shape는 root-relative로 유지한다.
- 테스트: generator-shaped `/assets/app.<hash>.js` fixture와 absolute `https://app.example/assets/app.<hash>.js` request를 사용해 `onFetch()`가 current cache로 들어가는지 직접 검증한다. 다른 origin, scope 밖 path, query/hash 변형은 거절한다.
### SW-01 — fetch가 current static cache가 아닌 전역 CacheStorage를 조회
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: `service-worker-lifecycle.ts:166-192`; worker facade `service-worker-entry.ts:38-44`
- 현재 동작: verified static URL에 `scope.caches.match(request.url)`을 호출한다. CacheStorage-wide match는 current, previous 또는 같은 URL을 가진 다른 cache 중 먼저 찾은 response를 반환할 수 있다.
- 영향:
- current release manifest에 URL이 포함되어 있어도 구 cache의 동일 URL response가 반환될 수 있다.
- invalid hit를 발견해도 삭제는 current cache에만 수행하므로 실제로 반환된 stale cache entry는 남는다.
결정: `onFetch()``config.manifest.setDigest`로 계산한 current cache를 `open()`하고 그 cache에서만 `match()`한다. worker scope facade의 CacheStorage-wide `match`는 제거한다.
테스트 추가 (`tests/unit/service-worker-runtime.test.ts`):
- current/previous cache에 같은 URL과 다른 bytes가 있을 때 current만 반환
- previous에만 entry가 있으면 network fallback (`null`)
- current invalid response만 current cache에서 삭제
- unrelated cache의 same URL은 조회/삭제하지 않음
완료 조건: runtime fetch path에 `caches.match` 호출이 0이고 current cache name이 exact digest에서 파생된다.
### SW-02 — cache reset가 exact ownership 대신 prefix를 사용
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: `service-worker-lifecycle.ts:333-365`; exact helper `src/contracts/service-worker.ts:111-116`
- 현재 동작: `name.startsWith("ca-static-v1-")`이면 삭제한다. `isOwnedStaticCacheName()`은 정확히 16자리 lower-hex suffix를 요구하지만 reset path가 이를 사용하지 않는다.
- 영향: `ca-static-v1-not-owned`, suffix가 더 긴 이름 등 같은 prefix를 가진 타 기능/cache가 삭제될 수 있다.
- 수정: import되어 있는 `isOwnedStaticCacheName(name)`만 사용한다. cache name 상수 literal도 lifecycle에서 제거한다.
- 테스트: valid 16-hex 두 개만 삭제하고 short/long/non-hex/upper-hex/unrelated cache를 보존한다.
### SW-03 — `unregister() === false``UNREGISTERED`로 보고
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: `service-worker-removal.ts:89-120`
- 현재 동작: Promise가 resolve하면 boolean을 무시하고 `UNREGISTERED`를 반환한다.
- 수정: `const unregistered = await registration.unregister()``true`만 성공으로 인정한다. `false``{ kind: "FAILED", operation: "UNREGISTER" }`로 닫는다. 새 outcome을 추가할 필요는 없다.
- 테스트: true, false, rejection, absent, ownership mismatch를 각각 고정한다.
### SW-04 — explicit removal mode가 cleanup 실패를 `DISABLED`로 숨김
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: `service-worker-page-controller.ts:78-121`
- 현재 동작:
- `REMOVE_REGISTRATION``PURGE_OWNED_RESOURCES`는 실제 outcome과 무관하게 `DISABLED`를 반환한다.
- `disabledCleanup``OWNERSHIP_MISMATCH``DISABLED`로 반환한다.
- 영향: staged removal이 끝난 것으로 판단해 다음 release에서 worker source/handler를 제거할 수 있지만 실제 registration 또는 cache가 남아 있을 수 있다.
결정 매핑:
| cleanup outcome | page start outcome |
| --- | --- |
| `ABSENT`, `UNREGISTERED`, `PURGED` | `DISABLED` |
| `OWNERSHIP_MISMATCH` | `INCOMPATIBLE` |
| `FAILED` | `FAILED` (`DISABLE_CLEANUP_FAILED`, `REMOVE_FAILED`, `PURGE_FAILED`) |
관찰 이벤트만 남기고 success로 바꾸지 않는다. 테스트는 selection 세 종류와 위 outcome matrix를 모두 table-driven으로 작성한다.
### SW-05 — build input의 manifest row와 set digest 검증 부재
- 우선순위/분류: **P1 / CONTRACT_GAP**
- 근거: `scripts/lib/service-worker-build-input.ts:82-94`; runtime의 부분 검사 `service-worker-static-assets.ts:85-112`; 생성 canonical hash `scripts/generate-service-worker-assets.ts:48-93`
- 현재 동작:
- build input은 manifest top-level shape만 보고 `assets`를 type cast한다.
- runtime validator도 build/release identity, exact row keys, unique/canonical URL, content type type/allowlist, set digest 재계산을 확인하지 않는다.
- 잘못된 `contentType``storeAsset()``.toLowerCase()`에서 typed rejection이 아니라 throw가 될 수 있다.
결정: runtime-neutral shared manifest codec이 exact row keys, content-type/extension allowlist, root-relative canonical URL, length-prefixed canonical byte serialization을 소유한다. generator와 Node build gate는 같은 bytes를 Node SHA-256으로 hash하고 worker는 injected WebCrypto digest로 같은 bytes를 재검증한다. Node `crypto` 구현을 worker에서 import하지 않는다. 이 작업은 기존 2026-08-01 plan Task 5/SW-10의 **선행 build-decoder 단계**로 병합하며 canonical digest를 별도 PR에서 두 번 구현하지 않는다.
Build gate 필수 조건:
- top-level/asset row exact keys
- buildId/releaseId exact match
- sorted unique same-origin root-relative hashed asset URL
- 허용 content type/extension pair
- non-negative safe byte length와 전체 bound
- lower-hex SHA-256
- generator와 같은 length-prefixed canonical algorithm으로 `setDigest` 재계산
테스트 (`tests/unit/service-worker-build-input.test.ts`): 각 row field tamper, duplicate/reorder, cross-origin URL, dot segment, wrong extension/content type, wrong set digest, unknown field. valid generator output을 decoder에 다시 넣는 parity test도 추가한다.
### SW-06 — activation/reset command의 source identity와 single-flight 부재
- 우선순위/분류: **P2 / CONTRACT_HARDENING**
- 근거: `service-worker-page-controller.ts:174-211`, `:231-305`, `:308-369`
- 현재 동작:
- activation 전용 listener는 `event.origin``event.source`를 검증하지 않는다.
- general listener/reset은 origin 일부만 확인하며 expected waiting/controller source와 correlation하지 않는다.
- 동시에 `requestActivation()` 또는 `resetOwnedCaches()`를 여러 번 호출하면 nonce와 listener가 중복 생성된다.
결정:
- activation reply는 request 시 capture한 `registration.waiting``event.source`가 같아야 한다.
- reset reply는 request 시 capture한 `container.controller`와 같아야 한다.
- long-lived `CLIENT_DRAIN_REQUEST` listener도 expected `registration.waiting` source와 correlation한다. nonce가 없더라도 arbitrary same-origin source가 page admission을 닫게 하지 않는다.
- empty origin을 신뢰 근거로 사용하지 않고 source identity + nonce + target identity를 함께 검증한다.
- 각 command를 single-flight Promise로 만들고 concurrent caller는 같은 Promise를 받는다.
- message 수신 직전에 `event.source`, captured source, 현재 `registration.waiting`/`container.controller`가 모두 동일한지 확인한다. 교체되었으면 ignore 후 timeout이 아니라 즉시 `PROTOCOL_MISMATCH`로 종료한다.
테스트: wrong source with correct nonce, source swap, concurrent 10 calls가 postMessage 한 번, stop 중 pending 종료, retry after terminal.
### SW-07 — zero-client drain 의미가 불필요하게 activation을 막음
- 우선순위/분류: **P2 / VERIFIED_BEHAVIOR_CHANGE**
- 근거: `service-worker-lifecycle.ts:268-300`
- 현재 동작: scope 내 client가 0이면 `false`를 반환한다. requester가 request 직후 닫힌 경우 dirty client가 없는데도 waiting worker가 거절된다.
- 결정: empty set은 vacuously drained이므로 `true`다. 단, `clients.matchAll()` 실패는 reject/throw로 유지한다.
- 테스트: zero clients → skipWaiting, one missing ack → timeout/reject, out-of-scope only → zero in-scope로 처리.
### SW-08 — client `postMessage()` 예외가 activation event 전체를 깨뜨림
- 우선순위/분류: **P2 / VERIFIED_DEFECT**
- 근거: `service-worker-lifecycle.ts:225-265`, `:290-299`
- 현재 동작: drain request/accepted/reload notification loop에 per-client 예외 격리가 없다.
- 결정: drain request 전달 실패는 해당 expected client를 failed 처리하고 pending state를 즉시 정리한다. drain 완료 뒤 `skipWaiting()` 호출 성공을 activation admission commit으로 기록한다. 그 다음 `ACTIVATE_ACCEPTED`/reload 알림은 client별 best effort로 보내고 실패를 degraded observation으로 남긴다. 현재 코드의 pre-commit `ACTIVATE_ACCEPTED` 순서는 바꾸거나 protocol V2에서 그 message를 제거한다. `skipWaiting()` 실패는 `REJECTED/FAILED`이고 accepted 성공으로 관찰하지 않는다.
- 테스트: 첫/중간/마지막 client throw, skipWaiting throw, partial delivery, pending map leak 없음.
### SW-09 — install deadline 뒤 late candidate 작업
- 우선순위/분류: **P2 / LIFECYCLE_HARDENING**
- 근거: `service-worker-static-assets.ts:119-153`, `:156-254`, `:259-274`
- 현재 동작: deadline Promise가 먼저 끝나면 candidate cache를 삭제하고 반환하지만, signal을 무시한 fetch/digest/cache put은 뒤늦게 계속될 수 있다. digest rejection도 `storeAsset()`에서 직접 typed outcome으로 변환되지 않는다.
- 결정: public install result는 overall 60초에 닫고 candidate generation fence를 세워 뒤늦은 worker가 새 fetch/digest/put을 시작하지 못하게 한다. late `Response` body는 compensator로 취소한다. 이미 시작한 `cache.put`은 취소할 수 없으므로 background settlement를 관찰한 뒤 candidate cache를 다시 exact-delete하는 second cleanup을 등록한다. cleanup을 public completion에 포함하려면 그 budget을 총 60초 안에 미리 예약하며, 60초 뒤 별도 cleanup deadline을 await해 public bound를 늘리지 않는다. 모든 dependency exception은 closed `FETCH_FAILED`/`INTEGRITY_MISMATCH`로 mapping한다.
- 테스트: non-cooperative late fetch/digest, late cache put, digest rejection, delete rejection, unhandled rejection 없음.
### SW-10 — message protocol을 kind별 discriminated schema와 full identity로 승격
- 우선순위/분류: **P1 before release hardening / 기존 계획 승계**
- 근거: `service-worker-protocol.ts:44-143`; `SERVICE_WORKER_PROTOCOL_VERSION = 1`; 기존 `docs/superpowers/plans/2026-08-01-http-worker-adapter-remediation.md` Task 5
- 현재 동작: 모든 kind가 하나의 optional field bag을 공유하고 page-worker correlation은 주로 buildId에 의존한다. `service-worker-entry.ts:151-166`의 sync message는 codec 대신 V1 literal을 직접 만든다.
결정:
- 기존 계획대로 protocol V2에서 protocol/cache schema/build/release/contract/static set 전체 canonical identity digest를 교환한다.
- kind별 exact required/forbidden field schema를 사용한다. activation/reset kinds에는 nonce와 target identity가 필수다.
- 모든 message, including `SYNC_WAKE_OBSERVED`,는 `createServiceWorkerMessage()`만 사용한다.
- V1/V2 worker가 같은 scope에서 교차 activation하지 않도록 mismatch는 fail-closed하고 강제 skipWaiting 하지 않는다.
이 항목은 기존 계획을 **유지**한다. 정확한 sequence는 `SW-URL-01`, `SW-01`~`SW-04` → 기존 plan Task 4 bounded activation-marker reader → `SW-05` build decoder와 기존 Task 5/`SW-10` 통합 → `SW-06`~`SW-09`다. 같은 canonical digest/codec을 중복 구현하지 않는다.
## Web Push 상세
### WP-01 — CAS success receipt의 expected next revision 미검증
- 우선순위/분류: **P1 / VERIFIED_DEFECT**
- 근거: `push-association-fence-store.ts:475-513`; remove는 `:516-546`에서 next revision을 검사함
- 현재 동작: compareAndSwap success는 key/revision type/replayed만 확인하고 `revision === (expectedRevision ?? 0) + 1`을 확인하지 않는다.
- 영향: repository가 stale/임의 receipt를 반환하면 adapter가 실제로 확인되지 않은 control을 새 revision으로 포장한다. 이후 CAS authority가 틀어진다.
- 수정: write와 remove 모두 exact next revision, expected key, replay semantics를 같은 validator로 검증한다. replayed receipt도 동일 idempotency command의 exact revision이어야 한다.
- 테스트 (`tests/unit/web-push-fence-store.test.ts`): stale/same/skipped/huge revision, wrong key, malformed replay, valid initial/next/replayed receipt.
### WP-02 — deadline 뒤 local fence mutation effect가 UNKNOWN일 수 있음
- 우선순위/분류: **P1 / CONTRACT_GAP**
- 근거: `runtime-support.ts:51-113`; fence store `:411-423`, `:493-503`
- 현재 동작: deadline은 signal을 abort하고 실패를 반환하지만 generic `PushControlRepository`가 signal을 무시하거나 commit 경계 직후 늦게 resolve하면 CAS는 반환 이후 적용될 수 있다.
- 영향: security fence adapter가 `DEADLINE_EXCEEDED`를 반환한 뒤 ACTIVE/REVOKED record가 실제로 바뀔 수 있다.
결정:
1. read deadline wrapper와 mutation wrapper를 분리한다. repository는 commit 전 abort 시 `NOT_APPLIED`, commit 후 success receipt를 반환한다. deadline뿐 아니라 caller abort와 commit/receipt race도 unknown일 수 있다.
2. `WebPushFailureCode``MUTATION_OUTCOME_UNKNOWN`과 recovery reason을 추가한다. lifecycle은 `OPEN | RECONCILIATION_REQUIRED | CLOSED`이며 unknown 뒤 mutation admission을 닫는다.
3. 복구는 새 bounded read로 exact revision/authority/state를 확인한 뒤에만 한다.
4. `withAbortableDeadline`을 mutation의 correctness authority로 사용하지 않는다. deadline은 caller wait bound이며 effect는 repository receipt/read-back이 결정한다.
테스트: timeout-before-commit, timeout-racing-commit, late success, late rejection, recovery read, dispose 중 late ACTIVE 금지.
### WP-03 — backend commit이 전체 request authority에 binding되지 않음
- 우선순위/분류: **P1 / CONTRACT_GAP**
- 근거: request `push-registration-gateway.ts:73-121`; response `:170-218`; activation check `push-subscription-adapter.ts:682-711`
- 현재 동작: request는 `fenceGeneration`, `sessionBindingEpoch`, `releaseEpoch`을 보낸다. response는 `associationEpoch``sessionBindingEpoch`만 반환하고 adapter도 session epoch만 비교한다.
- 영향: provider/server bug 또는 stale response가 다른 fence/release request의 association을 반환해도 local current fence가 unchanged이면 ACTIVE로 commit할 수 있다.
결정: register와 reconcile의 request/response protocol을 V2로 올리고 서로 다른 exact response union을 사용한다.
```ts
type WebPushRegisterCommitV2 = Readonly<{
protocol: "WEB_PUSH_REGISTRATION_RECEIPT_V2";
associationEpoch: string;
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
requestBindingSha256: string;
replacedAssociationEpoch: string | null;
}>;
type WebPushReconciliationV2 =
| Readonly<{
protocol: "WEB_PUSH_RECONCILIATION_V2";
state: "ACTIVE";
associationEpoch: string;
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
requestBindingSha256: string;
}>
| Readonly<{
protocol: "WEB_PUSH_RECONCILIATION_V2";
state: "ABSENT";
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
requestBindingSha256: string;
}>;
```
`WEB_PUSH_PROTOCOLS`가 V2 literal과 length-prefixed field order를 소유한다. register digest에는 operation, authority tuple, subscription fingerprint, idempotency key, expected previous association epoch를 넣는다. reconcile에는 idempotency key가 없으므로 명시적으로 제외한다. decoded fixed-length digest bytes를 비교한 뒤 fence CAS를 수행한다.
배포: server가 V1 request에는 V1 response, V2 request에는 V2 response를 반환하도록 request protocol negotiation 배포 → V2 client → old client drain → V1 제거. exact decoder를 깨뜨리는 response dual-emit은 하지 않는다. authority field mutation과 reconcile `ABSENT` fixture를 추가한다.
### WP-04 — repeated enable의 backend upsert/rotation 의미가 타입에 없음
- 우선순위/분류: **P2 / CONTRACT_GAP**
- 근거: `push-subscription-adapter.ts:162-275`, fence `prepare():175-215`, `activate():219-270`
- 현재 동작: 같은 authority가 이미 ACTIVE여도 `enable()`은 새 idempotency key로 backend register를 다시 수행한다. server atomic installation upsert가 같은 association을 반환하거나 old association을 폐기한다는 문서 요구가 gateway receipt에 표현되지 않는다.
- 결정: public `enable()`이 public `reconcile()`을 호출하지 않는다. permission/prepare 뒤 private `reconcilePrepared()` flow를 공유해 exclusive guard 내부에서 호출한다. ACTIVE + valid native material이면 먼저 reconcile하고 `ABSENT`일 때만 register한다. request에 `expectedPreviousAssociationEpoch: string | null`을 보내고 receipt의 `replacedAssociationEpoch`과 exact match해야 한다. local activate는 old ACTIVE와 다른 epoch를 무조건 덮어쓰지 않는다.
- 테스트: double enable same epoch, reconcile active, server absent then register, replacement receipt, replacement without old epoch rejection, compensation on CAS failure.
### WP-05 — pre-aborted operation이 항상 INSPECT로 기록됨
- 우선순위/분류: **P3 / VERIFIED_DEFECT**
- 근거: `push-subscription-adapter.ts:462-478`
- 수정: `webPushFailure("ABORTED", failureOperation)`을 사용한다.
- 테스트: enable/reconcile/revoke/inspect 각각 pre-aborted operation field.
### WP-06 — bounded truncation을 성공으로 관찰
- 우선순위/분류: **P2 / EVIDENCE_CORRECTNESS**
- 근거: subscriptionchange client handoff `service-worker-runtime.ts:139-172`; notification cleanup `push-subscription-adapter.ts:908-945`
- 현재 정책: client 32개, notification 64개/2초로 bounded best effort이다. architecture 문서는 notification cleanup을 privacy guarantee로 보지 않고 account-neutral copy를 요구하므로 상한 자체는 결함이 아니다.
- 문제: 목록이 상한을 넘었는데도 success로 관찰해 운영자가 일부 처리만 된 사실을 알 수 없다.
- 수정: `WebPushObservation``countBucket: "0" | "1_8" | "9_32" | "33_64" | "GT_64"``truncated: boolean`을 추가한다. subscriptionchange는 32 초과 시 `LIMIT_EXCEEDED/DEGRADED`; notification cleanup은 64 초과 시 revoke authority와 분리된 cleanup observation을 `DEGRADED`로 기록하고 `{ complete: false }`를 반환한다. 무제한 loop나 전체 정리를 주장하지 않는다.
- 테스트: 33 clients, 65 notifications, owned item이 cap 밖에 있는 경우, account-neutral copy/click fence가 계속 안전함.
### WP-07 — user-visible native effect와 deadline result의 certainty
- 우선순위/분류: **P2 / CONTRACT_GAP**
- 근거: `runtime-support.ts:51-113`, `push-event-adapter.ts:144-174`, `notification-click-adapter.ts:144-176`
- 현재 동작: deadline/abort가 먼저 반환된 뒤 `showNotification`, `focus`, `openWindow`가 늦게 성공할 수 있다. 결과는 failure지만 user-visible effect는 발생할 수 있다.
- 결정: native 호출 전 terminal=`NOT_APPLIED`, native Promise pending 중 terminal=`MAYBE_APPLIED`, fulfillment=`CONFIRMED`로 phase를 고정한다. native-effect 전용 observation union에 effect를 두고 wrapper가 `onLateValue/onLateError`로 outer result 종료 뒤에도 safe observation을 한 번 남긴다. 이 observation을 authorization/retry에 사용하지 않는다. account-neutral notification과 click-time fence가 최종 안전 장치다.
## 유지해야 할 설계
- 한 scope에 physical Service Worker registration은 하나만 둔다.
- static install은 immutable hashed asset만 대상으로 하고 byte/digest 검증 후 all-or-nothing으로 공개한다.
- navigation, runtime config, release manifest, API response는 static cache에 넣지 않는다.
- `skipWaiting()`은 page/client drain handshake 이후에만 호출하고 baseline에서 `clients.claim()`은 사용하지 않는다.
- registration과 cache ownership을 exact scope/script/cache parser로 확인한다.
- Web Push endpoint, p256dh, auth, account/user ID, notification content를 durable fence/diagnostics에 저장하지 않는다.
- push와 click 모두 initial/final fence를 확인하고 arbitrary URL 또는 backend raw copy를 사용하지 않는다.
- revoke는 local generation fence를 먼저 commit하고 backend/native cleanup은 bounded best effort로 수행한다.
- notification cleanup 성공을 privacy 보장으로 주장하지 않는다. copy는 항상 account-neutral이어야 한다.
- `WEB_PUSH`가 선택되지 않은 현재 baseline에서 worker import/handler를 억지로 추가하지 않는다.
## 실행 순서
1. `SW-URL-01`, `SW-01`~`SW-04`, `WP-01`~`WP-03`을 독립 P1 PR로 처리한다.
2. 기존 2026-08-01 plan Task 4 bounded activation-marker reader를 완료한다.
3. `SW-05` shared build decoder와 기존 Task 5/`SW-10` protocol V2를 한 sequence로 구현한다.
4. `SW-06`~`SW-09`, `WP-04`~`WP-07`을 protocol/lifecycle PR로 나눈다.
5. 제품이 Web Push를 선택할 때 별도 composition 계획으로 registry/provider/consent/browser evidence를 추가한다.
집중 검증:
```bash
corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts \
tests/unit/service-worker-build-input.test.ts \
tests/unit/web-push-codec.test.ts \
tests/unit/web-push-fence-store.test.ts \
tests/unit/web-push-store-port-compatibility.test.ts \
tests/unit/web-push-runtime-support.test.ts \
tests/unit/web-push-subscription-adapter.test.ts \
tests/unit/web-push-worker-runtime.test.ts
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm lint
git diff --check
```
## 완료 정의
- generated root-relative asset가 canonical absolute request와 일치하고, current cache 외 response가 반환되지 않으며 exact owned cache만 삭제된다.
- unregister/removal 결과가 실제 browser outcome을 숨기지 않는다.
- build gate가 static manifest row와 canonical set digest tamper를 거절한다.
- every command reply는 expected worker source, nonce, target full identity에 묶인다.
- fence mutation receipt가 exact next revision과 effect certainty를 보장한다.
- backend association receipt가 authority 3-tuple과 request digest에 묶인다.
- bounded truncation과 MAYBE_APPLIED native effect가 성공으로 과장되지 않는다.
- Web Push의 미조합 상태를 구현 완료로 오인하지 않는다.
@@ -0,0 +1,111 @@
# Adapter Review — TechLog Asset Multipart Upload
> 검토 기준: `feature/techlog-backend-alignment` (2026-08-18, Task 7)
>
> 범위: `src/features/tech-log/adapters/http/asset-upload-transport.ts` 1개 파일과 그 wiring — `create-tech-log-feature-input.ts`, `installed-feature-adapters.ts`, `bootstrap/runtime-adapters.ts``attachCredentials`/`techLogCsrf`. `src/adapters/**` 전수 리뷰([INVENTORY](./INVENTORY.md))와는 별도 트랙이다: 이 파일은 `src/features/tech-log/adapters/**` 아래에 있고, TechLog는 자체 canonical HTTP 계약을 갖는 product feature이지 템플릿의 범용 adapter 계층이 아니다.
## 결론
TechLog Studio는 canonical 계약상 19개 operation을 갖는다. 그중 18개는 `external-contract-runtime.ts`가 표현할 수 있는 `requestBody: "NONE" | "JSON"` 범위 안에 있고, 플랫폼의 V3 실행기(`http-execution-v3.ts`) · 저수준 client(`client.ts`) · `attachCredentials` credential seam을 그대로 통과한다. 나머지 1개, `uploadStudioAsset`(`POST /api/v1/studio/assets`)만 `multipart/form-data`를 요구한다. 이 요구를 플랫폼이 표현할 수 없으므로, 이 operation 하나만 별도의 좁은 transport(`asset-upload-transport.ts`)로 분리했다.
이 seam은 플랫폼을 대체하지 않는다. CSRF, `Idempotency-Key`, canonical 오류 코드 매핑, timeout, credentials는 동일한 provider·동일한 오류 taxonomy로 다시 구현해 대칭을 유지한다. 포기하는 것은 플랫폼이 대신 강제해 주던 부분 — 계약 실행기의 byte 상한, retry policy, V3 진단 계측 — 뿐이며 이는 아래에 명시적으로 기록한다. 조립 지점은 `createTechLogFeatureInstalledInput`이 무조건(HTTP/MOCK 무관) `createHttpStudioAssetGateway`를 구성하고, 그 안에 이 transport를 주입하는 한 곳뿐이다.
## 우회 대상과 이유
- `src/contracts/external-contract-runtime.ts``requestBody` union은 `"NONE" | "JSON"` 두 값만 갖는다. `multipart/form-data`를 표현할 세 번째 값이 없다.
- `src/adapters/http/client.ts:719` 부근의 저수준 client는 본문이 있는 모든 request를 `JSON.stringify(input)`으로 직렬화해 고정 `content-type: application/json`으로 보낸다. `File`을 이 경로에 태우면 파일 바이트 대신 그 JSON 표현(빈 객체거나 오류)이 전송된다.
- 두 제약 모두 이번 task의 global constraint로 수정 금지 대상이다(`external-contract-runtime.ts`, `client.ts`, `http-execution-v3.ts`, `mutation-intent.ts`, 생성된 계약 산출물, `studio-gateway.ts` 포트). 계약 실행기 자체를 바꾸는 대신, `uploadStudioAsset` 한 operation만 포트 경계 뒤에서 다른 구현으로 우회한다.
## 우회 범위
canonical Studio API 전체는 19개 operation이다. `tech-log-studio-contract-contribution.ts`는 그중 18개만 등록한다 — `uploadStudioAsset`은 계약 실행기가 표현할 수 없으므로 애초에 그 파일에 없다(`studio-contract-contribution.test.ts`의 "declares every canonical operation except the multipart upload"가 18을 고정한다).
| 분류 | operation | 경로 |
| --- | --- | --- |
| JSON, 계약 실행기 경유 | `getStudioSession`, `getStudioDashboard`, `listStudioDocuments`, `createStudioDocument`, `getStudioDocument`, `saveStudioDocument`, `validateStudioDocument`, `getCurrentStudioPreview`, `createStudioPreview`, `publishStudioDocument`, `listStudioPublications`, `unpublishStudioPublication`, `getStudioPublicationSnapshot`, `listStudioCatalog`, `listStudioAssets`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset` (18개) | `contractHttp.execute()``client.ts``attachCredentials` |
| multipart, 플랫폼 우회 | `uploadStudioAsset` (1개) | `asset-upload-transport.ts`의 직접 `fetch()` |
18개는 `contractOperations.execute(operationId, input, { routeId, intent? })`를 통해 나가며, 그중 17개(`getStudioSession` 제외)에 `attachCredentials`가 매 요청 `x-csrf-token`을 싣는다 — `getStudioSession`은 그 토큰을 발급하는 operation 자신이라 CSRF 헤더를 요구하지 않는 `TECH_LOG_STUDIO_BOOTSTRAP` auth profile을 쓴다(자세한 내용은 아래 "유지되는 보증"의 CSRF 행). 1개(`uploadStudioAsset`)만 이 경로를 완전히 벗어나 `createAssetUploadTransport`가 직접 `fetch()`한다. `StudioAssetGateway.uploadAsset()`이 이 transport를 호출하는 유일한 지점이며, 포트 시그니처(`Promise<Asset>`)는 나머지 4개 asset operation과 동일해 호출자는 어느 경로인지 알 필요가 없다.
## 유지되는 보증
플랫폼이 18개 JSON operation에 자동으로 제공하는 것을, 이 transport는 같은 provider·같은 값으로 손으로 다시 만든다.
| 보증 | JSON 경로 | multipart 경로 |
| --- | --- | --- |
| CSRF | `attachCredentials``techLogCsrf.token()`/`headerName()`으로 얻은 값을 그 이름 그대로 요청 헤더에 싣는다 | `StudioAssetGateway.uploadAsset()`**같은** `techLogCsrf` provider에서 `token()`/`headerName()`을 읽어 transport에 넘긴다 — provider가 composition root에 하나뿐이므로 세션당 토큰도 하나다. `getStudioSession` 자신은 이 provider를 거치지 않는 `TECH_LOG_STUDIO_BOOTSTRAP` auth profile을 쓴다: 그 provider가 토큰을 얻으려고 호출하는 operation이 같은 provider의 토큰을 요구하면 순환이 되기 때문이다(`docs`가 아니라 코드로 고정: `studio-csrf-composition.test.ts`) |
| Idempotency-Key | 실행 intent(`mutationIntent()`)에서 나와 client가 헤더로 싣는다 | 호출자(`uploadAsset` options)의 `idempotencyKey`를 gateway가 그대로 헤더로 전달한다 |
| canonical 오류 코드 매핑 | `toStudioGatewayError()``STUDIO_ERROR_CODES`에 있는 `problem.code``StudioGatewayError`로 승격하고, 계약 밖 코드는 `STUDIO_UNAVAILABLE`로 접는다 | transport가 동일한 `STUDIO_ERROR_CODES` 집합을 재사용해 같은 규칙으로 매핑한다. 서버가 `PAYLOAD_TOO_LARGE`/`UNSUPPORTED_MEDIA_TYPE`처럼 이 목록에 있는 코드를 보내면 그대로 `StudioGatewayError`가 되고, 계약에 없는 코드나 파싱 불가능한 본문은 도메인 코드를 지어내지 않고 `STUDIO_UNAVAILABLE`로 접는다 |
| timeout | 계약의 `requestDeadlineCeilingMs` | `AbortSignal.timeout(deps.timeoutMs)`를 호출자 signal과 `AbortSignal.any()`로 합성한다 |
| credentials | `TECH_LOG_STUDIO_SESSION` 프로필의 `credentials: "include"` | `fetch()` 호출에 동일하게 `credentials: "include"`를 명시한다 |
## 포기하는 보증
- **byte 상한**: 계약 실행기의 bounded body reader/writer가 응답 크기를 강제하는 것과 달리, 이 transport의 요청 body(`FormData`)와 응답 JSON 파싱에는 별도 상한이 없다. 서버가 `413 PAYLOAD_TOO_LARGE`로 거절하는 것에 의존한다.
- **retry policy**: 계약 실행기의 `retry-policy.ts`는 이 operation에 적용되지 않는다. `uploadStudioAsset`은 애초에 계약에서 `retrySemantics`를 선언할 수 없는 경로 밖에 있으므로, 재시도는 호출자(향후 Task 11의 Asset Library UI)가 명시적으로 다시 `uploadAsset()`을 호출하는 형태로만 존재한다.
- **V3 진단 계측**: `createHttpObservationProjector`가 만드는 `api.request.*` diagnostics/telemetry 이벤트는 `contractHttp.execute()` 내부에서만 발생한다. 이 transport는 그 관찰 경계 밖에서 직접 `fetch()`하므로 업로드 성공/실패는 diagnostics 스트림에 나타나지 않는다. `routeId: "TECH_LOG_STUDIO_ASSETS"`는 나머지 4개 asset JSON operation에는 여전히 붙지만, `uploadStudioAsset` 자체에는 대응하는 diagnostics 레코드가 없다.
이 세 항목 모두 이번 task 범위에서 새로 만들지 않는다 — 다시 만들려면 플랫폼과 동일한 bounded reader/retry/observation을 복제해야 하고, 그것은 계약 실행기를 다시 짓는 것과 다르지 않다. 대신 아래 교체 계획으로 닫는다.
## `ROUTE_ID` 검토 (Task 6 리뷰 인계 항목)
`http-studio-asset-gateway.ts``const ROUTE_ID = "TECH_LOG_STUDIO_ASSETS"`는 diagnostics/telemetry 버킷을 나누는 low-cardinality routing 메타데이터이지, 등록된 route 경로가 아니다. 이제 gateway가 실제로 배선되어 나머지 4개 asset JSON operation(`listAssets`, `getAsset`, `updateAssetMetadata`, `deleteAsset`)이 이 값으로 나가는 시점에서 다시 확인한 결과, **그대로 유지한다.** 문서/편집 operation의 `TECH_LOG_STUDIO`와 별개 값을 쓰는 것은 두 가지를 갖는다.
1. Asset 수명주기(업로드·목록·삭제)는 문서 편집과 실패 특성이 다르다 — 예를 들어 `PAYLOAD_TOO_LARGE`/`UNSUPPORTED_MEDIA_TYPE`/`ASSET_QUARANTINED`는 asset 쪽에만 있다. 별도 routeId는 이 실패를 diagnostics에서 문서 편집 트래픽과 섞지 않는다.
2. `uploadStudioAsset` 자체는 계약 실행기를 우회해 이 routeId를 진단에 보고하지 않지만, 같은 이름을 4개 JSON operation에 유지해 두면 향후 업로드가 presigned/resumable로 옮겨가거나 플랫폼에 MULTIPART 모드가 생겨 계약 경로로 복귀할 때, 같은 routeId 아래 asset 트래픽 전체가 이미 일관되게 모여 있다.
값을 바꿀 이유(예: 기존 registry 충돌, 명명 규칙 위반)는 없었다.
## 교체 계획
1. **presigned/resumable 업로드로 이전.** `src/adapters/browser-transfer/`에 이미 presigned capability와 resumable checkpoint 인프라가 있다(별도 리뷰: [04 — Browser transfer](./04-browser-transfer.md)). Studio asset 업로드가 그쪽으로 옮겨가면, 이 transport는 presigned URL 발급을 위한 작은 JSON operation(계약 실행기 경유 가능)과 실제 바이트 전송을 위한 presigned executor 호출로 나뉜다. `POST /api/v1/studio/assets`의 multipart 자체가 없어진다.
2. **플랫폼에 `requestBody: "MULTIPART"` 모드가 생기는 경우.** `external-contract-runtime.ts``client.ts``FormData` 본문을 표현할 수 있게 확장되면, `uploadStudioAsset`을 이미 등록된 다른 18개 operation과 함께 `tech-log-studio-contract-contribution.ts`에 등록하고 `createHttpStudioAssetGateway``upload` 의존성을 제거한다. `StudioAssetGateway` 포트 시그니처(`uploadAsset(form, options): Promise<Asset>`)는 바뀌지 않는다 — 교체는 이 파일과 `create-tech-log-feature-input.ts`의 배선 한 줄에서 끝난다.
두 경로 모두 `StudioAssetUploadTransport`/`StudioAssetGateway` 포트 경계 뒤에서 일어나므로, presentation 계층(Task 11의 Asset Library UI)은 재작성하지 않는다.
## MOCK 의존성 리비전은 계약의 요구가 아니라 mock의 구현이다
`createMockStudioGateway`의 기본 `dependencyRevision.current()`가 무엇을 관찰하는지 — 그리고 그것이 **계약이 요구하는 계산이 아니라는 점** — 을 여기에 남긴다. 나중에 mock의 구현을 계약의 요구로 오독하지 않기 위해서다.
### 실제 백엔드
`DependencyRevision`(`studio-api.openapi.yaml` / `generated.ts`)은 값의 **형식**만 계약이다: 불투명한 문자열. 계약이 요구하는 것은 값이 아니라 규칙 하나뿐이다 — *검증에 사용한 dependency set을 publish 시 다시 계산해 값이 다르면 `VALIDATION_STALE`로 거절한다.* 무엇을 dependency set에 넣을지(Topic/Project 존재, relation target 상태, Asset READY/QUARANTINED 상태, slug/route ownership, catalog revision, 필요 시 renderer/content-format version), 그리고 그것을 어떻게 정규화·hash할지는 **서버가 스스로 정한다.** 프론트엔드는 이 값을 생성하지도, 해석하지도, 비교하지도 않는다. `ValidationReport.dependencyRevision`을 받아 그대로 되돌려 보내고, 서버가 내린 `VALIDATION_STALE` 판정을 표시할 뿐이다. HTTP gateway(`http-studio-gateway.ts`)에는 리비전을 계산하는 코드가 없다 — 있어서도 안 된다.
### MOCK
MOCK `studioSource`에는 그 서버가 없으므로, mock이 같은 규칙을 스스로 만족시켜야 한다. 기본 리비전은 `dependency-revision.ts``mockDependencyRevision`이 계산한다.
- **catalog 성분**: `MOCK_CATALOG_REVISION`(`"catalog-2026-08-14"`) 상수. `createMockStudioState`가 고정 fixture catalog 하나를 싣고 변경하지 않으므로 catalog의 기여는 실제로 상수다. `fixtures.ts`의 seed validation/preview도 같은 정의를 import해 쓴다 — 두 값이 갈라지면 seed된 문서가 전부 조용히 stale이 된다.
- **asset 성분**: Asset store를 정규화해 만든 128비트 digest. 각 Asset을 **투영(projection)** 으로 줄이고(`id`, `assetKey`, `managementStatus`, `publicPath`, `updatedAt`, `decorative`, `altText`, `mediaType`, `width`, `height`), `stableStringify`로 정규 문자열을 만든 뒤 정렬해 접는다. 따라서 `Map` 삽입 순서와 무관하게 같은 논리적 Asset 집합은 항상 같은 리비전을 낸다 — 이 mock의 재현성은 저장소 전체 테스트가 의존하는 성질이다.
- 빈 store는 성분을 더하지 않아 `MOCK_CATALOG_REVISION` 그대로다. seed fixture가 Asset이 없는 세계에서 만들어졌고 그 문자열을 그대로 싣기 때문이다.
레코드 전체가 아니라 투영을 hash하는 이유: `usageCount`는 그 Asset을 참조하는 문서 수라 실제 백엔드였다면 **아무 문서나 publish할 때마다** 다른 저자의 진행 중인 검증이 전부 무효가 된다 — 검증기도 렌더러도 읽지 않는 필드인데도. `version`은 이 mock에서 `updatedAt`과 함께 움직여 신호를 더하지 않고, `kind`·`originalFilename`·`byteSize`·`createdAt`은 검증에도 render model에도 도달하지 않는다.
### 이 기본값이 닫는 구멍
기본 리비전이 리터럴 상수였을 때, `createStudioPreview`/`publishStudioDocument`의 staleness guard는 Asset store를 전혀 관찰하지 못했다. 그래서 **validate와 preview 사이의 Asset 변경이 guard에게 보이지 않았다.** 구체적으로: 어떤 evidence key의 Asset이 `decorative: true`뿐이면 `alt=""`인 directive는 정당하게 VALID다(장식용 이미지는 대체 텍스트가 없어도 된다). 그 사이에 같은 key에 `decorative: false`인 더 새로운 Asset이 도착하면, `createStudioPreview`는 성공하고 figure는 `decorative: false, alt: ""`로 해석되며 `publishDocument`가 그 render model을 그대로 snapshot한다. **의미 있는 이미지가 접근 가능한 이름 없이, 검증은 깨끗한 채로, 아무도 오류를 보고하지 않은 채 공개된다.** 이제 그 변경이 리비전을 움직여 guard가 `VALIDATION_STALE`을 내고, 저자가 재검증하면 `EVIDENCE_ALT_REQUIRED`로 진짜 문제를 듣는다.
수정은 `findResolvableAsset`이 아니라 리비전에 있다. `findResolvableAsset`*한 시점의* 술어이고 그 자체로는 옳다 — 두 시점 사이의 변화를 보는 것은 리비전의 일이다.
**주의**: 위 필드 목록은 이 mock이 스스로 무엇을 읽는지에 대한 서술이지, 서버가 무엇을 dependency set에 넣어야 하는지에 대한 요구가 아니다. 서버는 프론트엔드가 볼 수 없는 것(예: relation target의 게시 상태, route ownership)까지 포함할 수 있고 그래야 한다. 이 mock을 계약의 참조 구현으로 삼지 말 것.
고정 테스트: `tests/features/tech-log/mock-dependency-revision.test.ts`(보고된 시나리오 end-to-end, 순서 무관 결정성, 무변경 authoring loop 안정성).
## 검증
```
corepack pnpm exec vitest run tests/features/tech-log/asset-upload-transport.test.ts
corepack pnpm exec vitest run tests/features/tech-log/studio-asset-gateway.test.ts
corepack pnpm exec vitest run tests/features/tech-log/runtime-composition.test.ts
corepack pnpm exec vitest run tests/features/tech-log/studio-csrf-composition.test.ts
corepack pnpm exec vitest run tests/features/tech-log/studio-session-csrf.test.ts
corepack pnpm exec vitest run tests/features/tech-log/mock-dependency-revision.test.ts
corepack pnpm check:types
corepack pnpm test:tech-log
```
`studio-csrf-composition.test.ts`는 fix round 1에서 추가됐다 — 실 `createContractHttpExecutor` · `createCsrfTokenProvider` · `attachStudioSessionCredentials`를 composition root와 같은 방식으로 조립해 `getStudioSession`이 정확히 한 번만 나가고 그 토큰이 JSON operation과 업로드 양쪽에 모두 실리는지 검증한다. `studio-session-csrf.test.ts`는 provider의 재진입 가드를 단독으로 고정한다.
fix round 2에서 같은 파일에 "JSON operation의 403이 캐시된 토큰을 무효화해 다음 operation이 세션을 다시 가져온다"는 테스트를 더했다 — `invalidateTechLogCsrfOnOutcome`(`studio-session-credentials.ts`)를 composition root와 동일하게 호출한다. `asset-upload-transport.test.ts`에는 업로드 transport가 계약 밖 상태 코드의 실제 HTTP status를 그대로 통과시키는지, 그리고 계약 밖 401 본문도 게이트웨이의 토큰 무효화를 실제로 촉발하는지 검증하는 테스트를 더했다. `studio-contract-contribution.test.ts`에는 `getStudioSession`이 bootstrap profile의 유일한 사용자인지와 `assertExactlyOneTechLogStudioBootstrapOperation`이 0개·2개 위반을 거절하는지 고정하는 테스트를 더했다.
정확한 실행 결과는 `.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-7-report.md`에 있다.
+139
View File
@@ -0,0 +1,139 @@
# Adapter 파일 전수 inventory
> 검토 기준: `develop` (2026-08-14 재검토 반영)
>
> GOV-01. 이 표는 손으로 센 숫자가 아니라 `corepack pnpm check:adapter-inventory``git ls-files src/adapters`와 정확히 대조하는 목록이다.
>
> `rg --files src/adapters | sort` 결과 119개를 하나씩 고정한 coverage ledger다. 책임·의존성·finding·유지/변경 판정은 연결된 상세 리뷰의 파일별 표를 따른다.
| # | full path | 상세 리뷰 |
| ---: | --- | --- |
| 1 | `src/adapters/auth/external-session-adapter.ts` | [Network/state](./01-network-and-state.md) |
| 2 | `src/adapters/browser-file-storage/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 3 | `src/adapters/browser-file-storage/result.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 4 | `src/adapters/browser-file-storage/storage-manager-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 5 | `src/adapters/browser-files/browser-file-picker.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 6 | `src/adapters/browser-files/browser-file-policy-registry.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 7 | `src/adapters/browser-files/browser-file-vault.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 8 | `src/adapters/browser-files/create-browser-file-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 9 | `src/adapters/browser-files/download-delivery-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 10 | `src/adapters/browser-files/file-observer.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 11 | `src/adapters/browser-files/file-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 12 | `src/adapters/browser-files/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 13 | `src/adapters/browser-files/object-url-lease.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 14 | `src/adapters/browser-rpc/browser-rpc-runtime.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 15 | `src/adapters/browser-rpc/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 16 | `src/adapters/browser-rpc/transport.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 17 | `src/adapters/browser-rpc/unavailable-browser-rpc-transport.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 18 | `src/adapters/browser-transfer/image-cdn/README.md` | [Browser transfer](./04-browser-transfer.md) |
| 19 | `src/adapters/browser-transfer/image-cdn/browser-image-probe.ts` | [Browser transfer](./04-browser-transfer.md) |
| 20 | `src/adapters/browser-transfer/image-cdn/image-cdn-policy.ts` | [Browser transfer](./04-browser-transfer.md) |
| 21 | `src/adapters/browser-transfer/image-cdn/image-cdn-runtime.ts` | [Browser transfer](./04-browser-transfer.md) |
| 22 | `src/adapters/browser-transfer/image-cdn/image-header-metadata.ts` | [Browser transfer](./04-browser-transfer.md) |
| 23 | `src/adapters/browser-transfer/image-cdn/index.ts` | [Browser transfer](./04-browser-transfer.md) |
| 24 | `src/adapters/browser-transfer/image-cdn/p256-image-capability-verifier.ts` | [Browser transfer](./04-browser-transfer.md) |
| 25 | `src/adapters/browser-transfer/index.ts` | [Browser transfer](./04-browser-transfer.md) |
| 26 | `src/adapters/browser-transfer/presigned/incremental-sha256.ts` | [Browser transfer](./04-browser-transfer.md) |
| 27 | `src/adapters/browser-transfer/presigned/index.ts` | [Browser transfer](./04-browser-transfer.md) |
| 28 | `src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts` | [Browser transfer](./04-browser-transfer.md) |
| 29 | `src/adapters/browser-transfer/presigned/presigned-capability-vault.ts` | [Browser transfer](./04-browser-transfer.md) |
| 30 | `src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts` | [Browser transfer](./04-browser-transfer.md) |
| 31 | `src/adapters/browser-transfer/resumable-upload/checkpoint-schema.ts` | [Browser transfer](./04-browser-transfer.md) |
| 32 | `src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts` | [Browser transfer](./04-browser-transfer.md) |
| 33 | `src/adapters/browser-transfer/resumable-upload/http-control-plane-adapter.ts` | [Browser transfer](./04-browser-transfer.md) |
| 34 | `src/adapters/browser-transfer/resumable-upload/index.ts` | [Browser transfer](./04-browser-transfer.md) |
| 35 | `src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts` | [Browser transfer](./04-browser-transfer.md) |
| 36 | `src/adapters/browser-transfer/resumable-upload/presigned-upload-part-executor.ts` | [Browser transfer](./04-browser-transfer.md) |
| 37 | `src/adapters/browser-transfer/resumable-upload/resumable-upload-runtime.ts` | [Browser transfer](./04-browser-transfer.md) |
| 38 | `src/adapters/browser-transfer/resumable-upload/runtime-policy.ts` | [Browser transfer](./04-browser-transfer.md) |
| 39 | `src/adapters/browser-transfer/resumable-upload/upload-byte-source.ts` | [Browser transfer](./04-browser-transfer.md) |
| 40 | `src/adapters/browser-transfer/resumable-upload/upload-cancellation-channel.ts` | [Browser transfer](./04-browser-transfer.md) |
| 41 | `src/adapters/browser-transfer/resumable-upload/upload-mutation-lock.ts` | [Browser transfer](./04-browser-transfer.md) |
| 42 | `src/adapters/cache-storage/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 43 | `src/adapters/cache-storage/public-cache-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 44 | `src/adapters/cache-storage/public-response-cache-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 45 | `src/adapters/cross-context-invalidation/browser-cross-context-host.ts` | [Network/state](./01-network-and-state.md) |
| 46 | `src/adapters/cross-context-invalidation/browser-cross-context-invalidation.ts` | [Network/state](./01-network-and-state.md) |
| 47 | `src/adapters/cross-context-invalidation/index.ts` | [Network/state](./01-network-and-state.md) |
| 48 | `src/adapters/diagnostics/bounded-diagnostics.ts` | [Network/state](./01-network-and-state.md) |
| 49 | `src/adapters/http/bounded-body-reader.ts` | [Network/state](./01-network-and-state.md) |
| 50 | `src/adapters/http/bounded-json.ts` | [Network/state](./01-network-and-state.md) |
| 51 | `src/adapters/http/client.ts` | [Network/state](./01-network-and-state.md) |
| 52 | `src/adapters/http/http-contract-bridge.ts` | [Network/state](./01-network-and-state.md) |
| 53 | `src/adapters/http/http-effect-certainty.ts` | [Network/state](./01-network-and-state.md) |
| 54 | `src/adapters/http/http-execution-v3.ts` | [Network/state](./01-network-and-state.md) |
| 55 | `src/adapters/http/request-builder.ts` | [Network/state](./01-network-and-state.md) |
| 56 | `src/adapters/http/resource-mapper.ts` | [Network/state](./01-network-and-state.md) |
| 57 | `src/adapters/http/retry-policy.ts` | [Network/state](./01-network-and-state.md) |
| 58 | `src/adapters/http/schema-registry.ts` | [Network/state](./01-network-and-state.md) |
| 59 | `src/adapters/platform/abortable-operation.ts` | [Network/state](./01-network-and-state.md) |
| 60 | `src/adapters/platform/browser-lifecycle.ts` | [Network/state](./01-network-and-state.md) |
| 61 | `src/adapters/platform/browser-mutation-intent-factory.ts` | [Network/state](./01-network-and-state.md) |
| 62 | `src/adapters/platform/bounded-capacity.ts` | [Network/state](./01-network-and-state.md) |
| 63 | `src/adapters/platform/system-clock.ts` | [Network/state](./01-network-and-state.md) |
| 64 | `src/adapters/query-cache/conditional-validator-store.ts` | [Network/state](./01-network-and-state.md) |
| 65 | `src/adapters/query-cache/cursor-pagination-runtime.ts` | [Network/state](./01-network-and-state.md) |
| 66 | `src/adapters/query-cache/server-state-scope-runtime.ts` | [Network/state](./01-network-and-state.md) |
| 67 | `src/adapters/query-cache/tanstack-cache-coordinator.ts` | [Network/state](./01-network-and-state.md) |
| 68 | `src/adapters/query-cache/tanstack-query-cache.ts` | [Network/state](./01-network-and-state.md) |
| 69 | `src/adapters/realtime/event-codec.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 70 | `src/adapters/realtime/event-consumer.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 71 | `src/adapters/realtime/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 72 | `src/adapters/realtime/json-member-scanner.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 73 | `src/adapters/realtime/live-poll-handoff-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 74 | `src/adapters/realtime/polling/bounded-poll-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 75 | `src/adapters/realtime/polling/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 76 | `src/adapters/realtime/reconnect-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 77 | `src/adapters/realtime/reconnect-policy.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 78 | `src/adapters/realtime/result.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 79 | `src/adapters/realtime/sse/fetch-sse-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 80 | `src/adapters/realtime/sse/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 81 | `src/adapters/realtime/sse/sse-parser.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 82 | `src/adapters/realtime/stream-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 83 | `src/adapters/realtime/websocket/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 84 | `src/adapters/realtime/websocket/websocket-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 85 | `src/adapters/realtime/websocket/websocket-protocol.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
| 86 | `src/adapters/service-worker/service-worker-entry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 87 | `src/adapters/service-worker/service-worker-lifecycle.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 88 | `src/adapters/service-worker/service-worker-page-controller.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 89 | `src/adapters/service-worker/service-worker-protocol.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 90 | `src/adapters/service-worker/service-worker-removal.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 91 | `src/adapters/service-worker/service-worker-static-assets.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 92 | `src/adapters/storage/browser-storage-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 93 | `src/adapters/storage/browser-storage-codec.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 94 | `src/adapters/storage/indexeddb/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 95 | `src/adapters/storage/indexeddb/indexeddb-failure.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 96 | `src/adapters/storage/indexeddb/indexeddb-governance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 97 | `src/adapters/storage/indexeddb/indexeddb-maintenance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 98 | `src/adapters/storage/indexeddb/indexeddb-migrations.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 99 | `src/adapters/storage/indexeddb/indexeddb-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 100 | `src/adapters/storage/indexeddb/indexeddb-types.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 101 | `src/adapters/storage/opfs/browser-opfs-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 102 | `src/adapters/storage/opfs/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 103 | `src/adapters/storage/opfs/indexeddb-opfs-journal.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 104 | `src/adapters/storage/opfs/opfs-byte-store-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 105 | `src/adapters/storage/opfs/opfs-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 106 | `src/adapters/storage/opfs/opfs-worker-client.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 107 | `src/adapters/storage/opfs/opfs-worker-protocol.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 108 | `src/adapters/storage/opfs/opfs-worker-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
| 109 | `src/adapters/telemetry/best-effort-telemetry.ts` | [Network/state](./01-network-and-state.md) |
| 110 | `src/adapters/web-push/inbound/notification-click-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 111 | `src/adapters/web-push/inbound/push-event-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 112 | `src/adapters/web-push/index.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 113 | `src/adapters/web-push/notification-registry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 114 | `src/adapters/web-push/push-association-fence-store.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 115 | `src/adapters/web-push/push-codec.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 116 | `src/adapters/web-push/push-registration-gateway.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 117 | `src/adapters/web-push/push-subscription-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 118 | `src/adapters/web-push/runtime-support.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 119 | `src/adapters/web-push/service-worker-runtime.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
| 120 | `src/adapters/web-push/service-worker-scope-host.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
합계: **120/120**. 새 adapter 파일이 추가되면 이 ledger와 해당 상세 리뷰 inventory를 같은 변경에서 갱신한다.
## Feature-scoped adapter 리뷰 (`src/adapters/**` 밖)
이 표는 `corepack pnpm check:adapter-inventory``git ls-files src/adapters`와 대조하는 목록이라 `src/features/**/adapters/**` 파일은 포함하지 않는다. TechLog는 자체 canonical HTTP 계약을 갖는 product feature이며 그 adapter는 별도 트랙으로 검토한다.
- `src/features/tech-log/adapters/http/asset-upload-transport.ts` — [06 — TechLog asset multipart upload](./06-tech-log-asset-upload.md)
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
# Adapter 전수 리뷰 — 통합 인덱스와 확정 결정
> 검토 기준: `develop` / `4dc033cf33a5b6173bbf960d5eb464a406dc4c92` (2026-08-13)
>
> 검토 범위: `src/adapters/**`의 117개 TypeScript 파일과 1개 README, 총 53,475 TypeScript LOC. 직접 연결된 contracts, application ports, bootstrap composition, feature gateway, unit/integration test, ADR와 운영 문서를 함께 대조했다.
## 결론
adapter 계층의 큰 방향은 유지할 가치가 있다. native 객체와 raw provider material을 application 경계 밖에 두고, strict decoder·immutable capability·generation fence·bounded queue·typed failure를 사용하며, 선택되지 않은 capability를 조용히 fallback하지 않는 구조는 일관적이다. 정적 architecture gate도 현재 계층 위반을 찾지 않았다.
반면 lifecycle과 effect certainty에는 반복되는 공백이 있다. 가장 높은 위험은 OPFS 보상 정리의 journal 순서이며, 현재 조립 경로에서는 V3 HTTP 관찰 전체 유실, auth profile 미강제, retry 중 command effect 하향, telemetry의 dispose 이후 동작이 우선 수정 대상이다. 선택되지 않은 realtime, Browser RPC, Web Push, image/transfer capability의 결함은 현재 production incident로 과장하지 않되, 해당 capability를 조립하기 전 필수 promotion gate로 둔다.
이 문서와 하위 리뷰는 구현자가 추가 제품 결정을 요청하지 않도록 다음을 고정한다.
- 현재 코드로 재현되는 결함, contract gap, 구조 리팩터링, 문서화된 미구현을 분리한다.
- 각 finding마다 적용 패턴, 수정할 파일/API, 테스트 이름과 기대 결과, migration·deployment·rollback을 지정한다.
- 기존 public facade와 persisted/wire V1 호환을 언제 유지하고 언제 version-up할지 명시한다.
- default bootstrap에 optional capability를 새로 조립하지 않는다. 구현과 browser/provider evidence가 준비된 뒤 별도 product selection으로 승격한다.
## 보고서 구성과 범위
| 문서 | 구현 범위 | 파일 수 | 핵심 주제 |
| --- | --- | ---: | --- |
| [01 — Network and state](./01-network-and-state.md) | `http`, `auth`, `query-cache`, `cross-context-invalidation`, `platform`, `diagnostics`, `telemetry` | 24 | HTTP authority/effect, diagnostics·telemetry, ETag key, cancellation |
| [02 — Realtime and Browser RPC](./02-realtime-and-browser-rpc.md) | `realtime`, `browser-rpc` | 21 | stream lease, DRAINING, handoff writer, immutable binding, backpressure |
| [03 — Storage and browser files](./03-storage-and-browser-files.md) | `storage`, `browser-files`, `browser-file-storage`, `cache-storage` | 32 | OPFS saga, IndexedDB maintenance, file URL, public cache, quota/migration |
| [04 — Browser transfer](./04-browser-transfer.md) | `browser-transfer` | 24 | presigned capability, resumable upload, image CDN |
| [05 — Service Worker and Web Push](./05-service-worker-and-web-push.md) | `service-worker`, `web-push` | 17 | cache ownership, activation/removal, worker protocol, push authority |
합계는 118/118 파일이다. [전수 inventory](./INVENTORY.md)가 full path와 상세 리뷰를 일대일로 연결하고, 각 하위 문서의 파일 표가 책임, 직접 dependency/downstream, 판정을 기록한다.
## 최우선 finding
| 순서 | ID | 상태/심각도 | 확정 영향 | 구현 결정 |
| ---: | --- | --- | --- | --- |
| 1 | `STO-01` | 확정 / Critical | OPFS pre-commit cleanup 실패·취소 뒤 journal을 지워 복구 근거를 잃고, 늦은 generation-only cleanup이 후속 write를 삭제할 수 있다. | cleanup 확인 전 journal/budget rollback 금지, compensation signal 분리, transaction-unique physical generation token, cleanup 종료까지 mutation lease 유지 |
| 2 | `N-01` | 확정 / High / 현재 V3 | HTTP V3 observation의 미허용 context key 때문에 모든 request diagnostic이 drop되고 terminal failure telemetry도 없다. | typed observation을 closed diagnostic/telemetry bucket으로 투영하고 route ID를 executor context에 보존 |
| 3 | `N-02` | 확정 / High / 현재 V3 | `authProfileId`가 조립·강제되지 않아 bearer 필수 header와 transport-owned credentials/header invariant를 증명하지 못한다. | immutable auth Profile/Strategy registry, credential owner는 허용된 proof header만 제공, missing/extra는 fetch 전 fail-close |
| 4 | `N-03` | 확정 / High | 이미 dispatch된 command가 retry-time scope fence에서 `MAYBE_APPLIED`에서 `NOT_STARTED`로 하향될 수 있다. | logical execution 전체에 monotonic effect-certainty join 적용 |
| 5 | `N-04` | 확정 / High | telemetry가 dispose 뒤 scheduled/new/in-flight delivery를 계속하고 composition teardown이 dispose를 호출하지 않는다. | `ACTIVE/DISPOSED`, joined flush, in-flight abort, infrastructure teardown 연결 |
| 6 | `STO-02` | 확정 / High | download URL은 `baseOrigin`으로 검증하지만 원문 상대 URL은 `document.baseURI`로 실행된다. | parse-once canonical absolute URL만 handoff |
| 7 | `SW-URL-01`, `SW-01`~`SW-05` | 확정/gap / P1 | generated URL 분류 불일치, stale static response 선택, 과도한 prefix delete, 거짓 unregister/removal success, manifest 검증 부재 | canonical absolute runtime URL set, current-cache-only lookup, exact ownership parser, truthful cleanup result, shared strict manifest codec |
| 8 | `WP-01`~`WP-03` | 확정/gap / P1 / 미조립 | fence revision·mutation effect·backend authority receipt가 충분히 묶이지 않는다. | exact next revision, unknown effect recovery, V2 full authority/request binding receipt |
| 9 | `R-01`~`R-04` | 확정 / High / 미조립 | non-cooperative stream/effect가 무한 대기하거나 active writer가 유실되고 Browser RPC binding이 TOCTOU다. | explicit stream lease, retained DRAINING registry, retired writer set, immutable parse/validate/install |
| 10 | `BT-PRE-01`, `BT-PRE-02`, `BT-UP-03` | 확정/gap / P1 / 미조립 | eager download 자원 누수, wire envelope version 부재, late IndexedDB delete effect 오보고 | lazy closeable lease, protocol literal, `PENDING/effect UNKNOWN` outcome |
하위 문서의 나머지 Medium/P2/P3 항목도 생략 대상이 아니다. 위 표는 release·promotion을 막는 순서만 압축한 것이다.
## 공통 설계 결정
### D-01 — effect certainty는 단조 증가한다
한 번 native/network mutation을 dispatch한 뒤에는 새 retry가 아직 시작되지 않았다는 이유로 전체 logical operation을 `NOT_STARTED`로 되돌리지 않는다. 결과는 `NOT_STARTED → NOT_APPLIED/MAYBE_APPLIED → APPLIED_CONFIRMED`의 보수적 lattice로 join한다. IndexedDB/OPFS/Web Push처럼 deadline 뒤 native commit 가능성을 취소할 수 없는 API는 `UNKNOWN`을 명시하고 bounded read-back/reconcile만 허용한다.
### D-02 — commit fence와 resource settlement를 분리한다
abort/deadline 시 late commit capability는 즉시 폐기하지만, non-cooperative Promise·stream·writer reference는 실제 settlement까지 버리지 않는다. public wait은 bounded하게 끝내되 내부 lifecycle은 `DRAINING`으로 남고 같은 physical owner의 신규 admission을 막는다. `close()`가 성공했다면 tracked task가 실제로 quiescent여야 한다.
### D-03 — 외부/조립 입력은 parse → validate → install한다
TypeScript `Readonly`나 한 번의 boolean validator를 runtime immutability로 취급하지 않는다. registry, contract binding, provider response는 exact own-data descriptor와 closed key set을 검사한 immutable snapshot으로 설치하고 이후 원본을 다시 읽지 않는다. getter, extra/symbol key, revoked proxy는 composition/decoder 경계에서 fail-close한다.
### D-04 — 검증한 값을 그대로 실행한다
URL·path·header·manifest는 parse-once canonical form을 반환하고 network/navigation/cache operation은 그 canonical 값을 사용한다. boolean 검증 후 원문을 다른 base/decoder로 다시 해석하지 않는다. provider별 double-decode 가능성이 있는 encoded separator는 계약 fixture로 닫는다.
### D-05 — marker와 hint는 권위가 아니다
cache release marker는 “작성 완료 주장”일 뿐 모든 entry의 존재·digest 증거가 아니다. BroadcastChannel/storage event와 realtime cancellation은 hint이며 server/CAS/generation authority를 대신하지 않는다. 재사용·activation·복구 경로는 exact identity와 content를 다시 검증한다.
### D-06 — operation별 최소 dependency만 요구한다
stage에는 fetch가 필요하지만 local activate/cleanup에는 필요하지 않다. capability availability를 편의상 하나의 공통 guard로 묶지 않고 operation별로 분리한다. offline rollback/cleanup을 네트워크 부재 때문에 차단하지 않는다.
### D-07 — state machine과 Saga 경계로만 큰 runtime을 나눈다
파일 길이만으로 분해하지 않는다. 먼저 facade의 success/failure/cancel/call-order characterization을 고정한 뒤 순수 transition, retry policy, bounded scheduler, persistence reconciler, compensation saga를 추출한다. public capability identity, failure taxonomy, persisted schema, wire semantics는 별도 versioned migration 없이는 바꾸지 않는다.
### D-08 — abort/deadline mechanics만 공유한다
listener/timer 정리, first-terminal-owner, late rejection 관찰, late native handle compensation은 platform utility로 통합할 수 있다. HTTP, browser data, Web Push, realtime의 result taxonomy와 recovery vocabulary는 각 adapter에 남긴다. 범용 middleware/interceptor나 하나의 generic repository로 합치지 않는다.
### D-09 — optional capability의 미조립 상태를 유지한다
`AVAILABLE_NOT_COMPOSED`, `DESIGNED_NOT_IMPLEMENTED`, `NOT_SELECTED`는 defect status가 아니다. realtime, Browser RPC, Web Push, resumable upload, image provider, storage coordinator를 이번 remediation만으로 default bootstrap에 설치하지 않는다. 관련 P1/P2 closure, actual browser/provider/load evidence, product-owned policy·consent·registry가 모두 준비되어야 별도 selection change를 연다.
## 구현 순서
서로 다른 subsystem을 한 PR에 섞지 않는다. 각 항목은 failing characterization → 최소 수정 → focused green → type/architecture/lint → commit 순서다.
1. **Containment:** product-specific composition에서 OPFS v1 writer 사용 여부를 확인하고, 사용 중이면 신규 write admission을 read-only/export-required로 닫는다. template 기본 bootstrap은 OPFS를 조립하지 않는다.
2. **현재 실행 경로:** `STO-01`, `N-01`~`N-04`, `STO-02`를 독립 PR로 수정한다.
3. **기존 rollback/sidecar:** `N-05`~`N-11`과 legacy HTTP V2 hardening을 처리한다. V2를 지우는 일은 zero-caller와 rollback-window 종료 뒤 별도 PR이다.
4. **선택 capability correctness:** `SW-URL-01`, `SW-01`~`SW-09`, `WP-01`~`WP-07`, `R-01`~`R-06`, browser-transfer P1/P2를 subsystem별 PR로 닫는다.
5. **기존 version/migration 계획:** Service Worker V2(`SW-10`), OPFS physical/protocol V2, presigned/Web Push receipt V2를 expand → dual-read/emit → old-writer drain → contract 순서로 배포한다.
6. **구조 리팩터링:** behavior가 모두 green인 상태에서 resumable upload, image CDN, OPFS worker, public cache, download strategy를 characterization-preserving extraction으로 나눈다.
7. **Promotion gaps:** preview decode, bounded origin/cache maintenance, Browser RPC concrete transport, image descriptor provider 등 명시된 gap을 실제 browser/provider conformance와 함께 구현한다. 완료 전 availability state를 올리지 않는다.
질문 없는 세부 실행 절차는 [Adapter Remediation Implementation Plan](../../superpowers/plans/2026-08-13-adapter-remediation.md)에 있으며, finding별 exact API·test·migration은 각 하위 리뷰가 source of truth다.
## 기존 계획과의 우선권
| 기존 계획 | 유지할 내용 | 이번 리뷰가 추가하는 선행 조건 |
| --- | --- | --- |
| [2026-08-01 HTTP/worker remediation](../../superpowers/plans/2026-08-01-http-worker-adapter-remediation.md) Tasks 13 | installed HTTP contract 단일 권위, provider-neutral outcome, bound-only query API | `N-01`~`N-03` auth/observation/effect 결함을 같은 V3 migration에 먼저 포함 |
| 같은 계획 Task 4 | bounded Service Worker marker reader | 그대로 유지; `SW-01`~`SW-09`의 cache/lifecycle truth를 함께 닫은 뒤 V2로 이동 |
| 같은 계획 Task 5 | full identity Service Worker protocol V2 | `SW-10`으로 승계. 새 protocol을 두 번 설계하지 않는다. |
| 같은 계획 Task 6 | shared IndexedDB persisted-row schema | 그대로 유지하되 `STO-06` deadline/drain lease test를 extraction 전 추가 |
| 같은 계획 Task 7 | OPFS/cache/download cohesive decomposition | `STO-01`~`STO-05` correctness fix와 characterization이 먼저다. |
| [2026-08-01 runtime correctness](../../superpowers/plans/2026-08-01-runtime-correctness-remediation.md) Tasks 15 | query key/invalidation, application mutation intent, keyed command preflight, effect-aware settlement | 새 plan이 대체하지 않는다. `N-03`, `N-05`, `N-06`을 동일 certainty/key authority에 병합한다. |
충돌 시 우선순위는 **현재 재현 결함의 fail-close 수정 → 기존 plan의 계약 통합 → 구조 추출 → optional capability 조립**이다. 두 기존 plan을 완료로 표시하거나 삭제하지 않는다.
## 검증 기준선
- `corepack pnpm check:types`: 통과.
- `corepack pnpm lint`: 통과.
- `corepack pnpm check:architecture`: sandbox child-process 제약에서는 실패했으나 동일 명령을 허용된 실행 환경에서 다시 수행해 286 modules / 854 dependencies, 12 fixture, TS-only/allowed/forbidden gate가 모두 통과했다.
- 영역별 focused baseline:
- network/state: 21 files / 144 tests 통과, `check:diagnostics` 통과.
- realtime/Browser RPC: 16 files / 185 tests 통과, source boundary gate 통과.
- storage/files/cache: 7 files / 105 tests 통과.
- browser transfer: 6 files / 93 tests 통과; Service Worker/Web Push: 8 files / 52 tests 통과(독립 재감사 실행).
- 전체 `test:unit`은 이 sandbox에서 child `spawnSync ... EPERM`이 발생한 세 CI/evidence test file 때문에 108 files 통과, 3 files 실패(1465 tests 통과, 50 실패)였다. adapter focused suite의 실패가 아니며 전체 green으로 주장하지 않는다.
최종 산출물 검증은 118/118 inventory 포함, placeholder/깨진 local path 검사, Markdown diff 검사, focused adapter tests, type/architecture/lint를 다시 실행한다.
## 명시적으로 하지 않는 변경
- 이 리뷰에서는 production source를 수정하거나 optional adapter를 bootstrap에 조립하지 않는다.
- private/range cache, persistent browser handles, resumable range download, arbitrary Web Push copy/URL 같은 별도 미선택 capability를 기존 adapter에 섞지 않는다.
- timeout을 이유로 irreversible native mutation이 적용되지 않았다고 추정하지 않는다.
- cleanup 실패를 observation만 남기고 success로 바꾸지 않는다.
- schema/database version을 downgrade하거나 broad prefix/root/database 전체 삭제를 rollback으로 사용하지 않는다.
- SSE↔WebSocket, Connect↔gRPC-Web↔REST를 장애 중 자동 전환하지 않는다.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,812 @@
# TechLog Public and Studio UI Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Move every Public and Studio screen from `/home/donghyeon/workspace/techlog-studio-frontend` into this Vite frontend while preserving the source DOM, text, assets, CSS, responsive behavior, accessibility semantics, and user-visible interactions exactly.
**Architecture:** Keep the target repository's Vite, React Router, application API, diagnostics, route lifecycle, and adapter boundaries. Install one `tech-log` feature whose presentation is split into nested `PUBLIC` and `STUDIO` layout groups; expose immutable Public queries and a factory-scoped `StudioGateway` through application feature inputs; use source-equivalent static Public data and the deterministic mock Studio adapter. The Public renderer is shared with Studio previews so one content-format implementation produces both views.
**Tech Stack:** Node 24, pnpm 11, TypeScript 7, React 19, React Router 7, Vite 8, Vitest 4, Testing Library, Playwright, Axe, Tailwind 4, Pretendard 1.3.9, IBM Plex Mono 5.3.0, Unified 11.0.5, Remark Parse 11.0.0, Remark GFM 4.0.1, Remark Directive 4.0.0.
## Source of truth and delivery branch
- Approved design: [`docs/superpowers/specs/2026-08-15-techlog-ui-migration-design.md`](../specs/2026-08-15-techlog-ui-migration-design.md).
- Visual/behavior source: `/home/donghyeon/workspace/techlog-studio-frontend` at the locally inspected revision used when Task 1 records the baseline.
- Delivery flow is already initialized with `main` as production and `develop` as integration. Execute every task and commit on `feature/techlog-ui-migration`; finish through `git flow feature finish techlog-ui-migration` only after Task 14 is green and the user authorizes integration.
- Do not push, merge, finish the feature, or delete branches as part of an individual task.
## Non-negotiable constraints
- Visual parity means no redesign: preserve source element order, nesting, class names, visible copy, labels, ARIA, SVGs, typography, spacing, color values, borders, shadows, animation, and responsive rules.
- Copy `app/globals.css`, `app/studio.css`, `app/studio-editor.css`, `components/studio/workflow.module.css`, and `components/studio/publication-flow.module.css` without value or selector changes. Remove only the source `@import "tailwindcss"` because target `theme.css` already owns that import.
- Preserve the source breakpoints, including 1179, 1050, 1024, 980, 900, 767, and 420 pixels. Do not substitute the target generic design-system components where doing so changes source markup or styles.
- Preserve `public/favicon.svg` and `public/media/fetch-strategy-boundary.svg` byte-for-byte. Add only assets demonstrably referenced by a migrated screen.
- The only permitted framework translations are: `next/link` to React Router `Link` with `href` renamed to `to`; the Studio `공개 사이트 보기` boundary remains a plain `<a href="/">` so it performs the source-intended full reload/session reset; `usePathname`/Next navigation to `useLocation`/`useNavigate`; `next/image` to an `img` that preserves classes, dimensions, alt text, loading intent, and wrapper structure; server page inputs to validated route params/search plus injected feature queries.
- No Next.js, Vinext, Cloudflare, server actions, or direct presentation-to-adapter imports enter the target.
- Public behavior stays deterministic and static. Studio behavior stays session-scoped and deterministic through one provider-owned mock gateway instance. Reload resets Studio state; remounting child routes does not.
- Studio authentication is deliberately deferred. Studio routes use `access: "public"` in this migration so the current shell is reachable, while `layoutGroup: "STUDIO"` and the application feature input remain the future auth seam. Do not add fake sign-in UI.
- Unknown public content renders the Public not-found experience; unknown `/studio/*` and unknown document/publication IDs render Studio not-found inside the Studio shell. Param codecs accept non-empty strings and do not reject unknown IDs before gateway lookup.
- Every production change follows red → green → focused regression → commit. Never update a test merely to legitimize a visual or behavioral difference.
- The pre-existing `tests/unit/ci-artifact-contract.test.ts` child-process failures caused by the restricted environment are baseline infrastructure evidence, not permission to add failures. Record exact commands/counts; focused TechLog tests and static gates must pass.
## Fixed contracts
### Route grouping
Add this field to every route definition:
```ts
export type RouteLayoutGroup = "PUBLIC" | "STUDIO";
export type RouteDefinition = Readonly<{
routeId: string;
path: string;
layoutGroup: RouteLayoutGroup;
paramsSchema: string | null;
searchSchema: string | null;
access: "public" | "session-required";
loadingSurface: string;
errorSurface: string;
chunkId: string;
title: string;
navigationLabel: string | null;
navigationOrder: number | null;
}>;
```
The installed TechLog route IDs and paths are fixed:
| Layout | Route ID | Path |
| --- | --- | --- |
| PUBLIC | `TECH_LOG_HOME` | `/` |
| PUBLIC | `TECH_LOG_EXPLORE` | `/explore` |
| PUBLIC | `TECH_LOG_EXPLORE_KIND` | `/explore/:kind` |
| PUBLIC | `TECH_LOG_CASE` | `/cases/:slug` |
| PUBLIC | `TECH_LOG_REFERENCE` | `/references/:slug` |
| PUBLIC | `TECH_LOG_QUESTION` | `/questions/:slug` |
| PUBLIC | `TECH_LOG_TOPIC` | `/topics/:slug` |
| PUBLIC | `TECH_LOG_PROJECTS` | `/projects` |
| PUBLIC | `TECH_LOG_PROJECT` | `/projects/:slug` |
| PUBLIC | `TECH_LOG_PROJECT_RECORDS` | `/projects/:slug/records` |
| PUBLIC | `TECH_LOG_PROJECT_DECISIONS` | `/projects/:slug/decisions` |
| PUBLIC | `TECH_LOG_PROJECT_ACTIVITY` | `/projects/:slug/activity` |
| PUBLIC | `TECH_LOG_RELEASES` | `/releases` |
| PUBLIC | `TECH_LOG_RELEASE` | `/releases/:version` |
| PUBLIC | `TECH_LOG_PROFILE` | `/profile` |
| PUBLIC | `TECH_LOG_SEARCH` | `/search` |
| STUDIO | `TECH_LOG_STUDIO_HOME` | `/studio` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENTS` | `/studio/documents` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_NEW` | `/studio/documents/new` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_EDIT` | `/studio/documents/:id/edit` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_VALIDATION` | `/studio/documents/:id/validation` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PREVIEW` | `/studio/documents/:id/preview` |
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PUBLISH` | `/studio/documents/:id/publish` |
| STUDIO | `TECH_LOG_STUDIO_PUBLICATIONS` | `/studio/publications` |
| STUDIO | `TECH_LOG_STUDIO_PUBLICATION_PREVIEW` | `/studio/publications/:publicationEventId/preview` |
| STUDIO | `TECH_LOG_STUDIO_NOT_FOUND` | `/studio/*` |
| PUBLIC | `NOT_FOUND` | `*` |
Route schema IDs are also fixed. Parameter codecs are `TechLogExploreKindParams` (`kind` non-empty; page-level lookup recognizes only `cases | references | questions`), `TechLogSlugParams` (`slug` non-empty), `TechLogVersionParams` (`version` non-empty), `TechLogDocumentIdParams` (`id` non-empty), `TechLogPublicationEventIdParams` (`publicationEventId` non-empty), `TechLogStudioSplat`, and the existing `NotFoundSplat`. Search codecs are `TechLogHomeSearch` (`focus`, `state`), `TechLogExploreSearch` (`type`, `topic`, `project`), `TechLogExploreKindSearch` (`topic`, `project`), `TechLogSearchQuery` (`q`), and `TechLogCaseStateSearch` (`state`); all fields are optional single strings and canonicalization keeps the first repeated value, trims selections where the source does, and drops unknown fields. Other routes use `none`.
Each TechLog route uses a unique kebab-cased chunk/module identity derived from its ID: lower-case the route ID, replace underscores with hyphens, and prefix `route-` (for example, `TECH_LOG_STUDIO_DOCUMENT_EDIT``route-tech-log-studio-document-edit`). The global `NOT_FOUND` keeps `route-not-found`. This same identity must appear in the runtime contract, lazy import map, release manifest, diagnostics, and chunk-recovery tests.
### Feature input and gateway
```ts
export const TECH_LOG_FEATURE_ID = "tech-log" as const;
export type TechLogFeatureInput = Readonly<{
publicContent: PublicContentQueries;
createStudioGateway(): StudioGateway;
}>;
declare module "../../../application/ports/in/application-api.ts" {
interface ApplicationFeatureInputs {
"tech-log": TechLogFeatureInput;
}
}
export interface StudioGateway {
getDashboard(options?: RequestOptions): Promise<StudioDashboard>;
listDocuments(query: ListDocumentsQuery, options?: RequestOptions): Promise<DocumentPage>;
createDocument(input: CreateDocumentInput, options: IdempotentOptions): Promise<WorkingCopy>;
getDocument(documentId: string, options?: RequestOptions): Promise<WorkingCopyDetail>;
saveDocument(documentId: string, command: SaveDocumentCommand, options: IdempotentOptions): Promise<WorkingCopyDetail>;
validateDocument(documentId: string, command: ValidateDocumentCommand, options: IdempotentOptions): Promise<ValidationReport>;
createPreview(documentId: string, command: CreatePreviewCommand, options: IdempotentOptions): Promise<PublicPreview>;
getCurrentPreview(documentId: string, options?: RequestOptions): Promise<PreviewDetail>;
publishDocument(documentId: string, command: PublishDocumentCommand, options: IdempotentOptions): Promise<PublishResult>;
unpublishPublication(publicationId: string, command: UnpublishCommand, options: IdempotentOptions): Promise<PublishResult>;
listPublications(query: ListPublicationsQuery, options?: RequestOptions): Promise<PublicationPage>;
getPublicationSnapshot(publicationEventId: string, options?: RequestOptions): Promise<PublicationSnapshot>;
getCatalog(query: CatalogQuery, options?: RequestOptions): Promise<CatalogPage>;
}
```
Use the source generated contract names and exact payload fields from `lib/studio/api/generated.ts`. `StudioGatewayError` retains RFC 9457-like problem details, HTTP status, stable code, and retryability. Abort remains distinguishable from not-found/conflict/validation failures.
`PublicContentQueries` is the immutable boundary for the source functions `listRecords`, `getRecord`, `getProject`, `getRelease`, `getProjectRecords`, `getProjectDecisions`, `getProjectActivity`, `getHomeFocusItems`, and `searchPublicContent`, with the source argument and return types unchanged.
### State derivation
Port the source pure functions and values exactly:
```ts
deriveValidationState(input: StateInput): ValidationState;
derivePreviewState(input: StateInput): PreviewState;
deriveNextAction(input: StateInput): NextAction;
deriveDocumentState(input: StateInput): StudioDocumentState;
```
Editor state is `CLEAN | DIRTY | SAVING | CONFLICT`; validation freshness is `NONE | CURRENT | STALE`; publication state is `NEVER_PUBLISHED | PUBLISHED | UNPUBLISHED`. The saved revision/version token owns optimistic concurrency.
## Deterministic source-to-target map
| Source | Target |
| --- | --- |
| `lib/content-format/*`, `lib/public-render-content.ts` | `src/features/tech-log/domain/content-format/*`, `src/features/tech-log/domain/public-render-content.ts` |
| `lib/content.ts`, `lib/evidence-assets.ts`, `lib/public-content.ts`, `lib/public-query.ts` | `src/features/tech-log/adapters/static/*` behind `PublicContentQueries` |
| `lib/studio/api/*`, `contracts/studio-api.openapi.yaml` | `src/features/tech-log/contracts/studio/*` and `src/features/tech-log/application/ports/studio-gateway.ts` |
| `lib/studio/document-state.ts`, `lib/studio/local-id.ts` | `src/features/tech-log/domain/studio/*` |
| `lib/studio/mock/*` | `src/features/tech-log/adapters/mock/*` |
| public `components/*.tsx` | `src/features/tech-log/presentation/public/components/*` |
| Studio `components/studio/*.tsx` | `src/features/tech-log/presentation/studio/components/*` |
| public `app/**/page.tsx` | `src/features/tech-log/presentation/public/pages/*` |
| Studio `app/studio/**` | `src/features/tech-log/presentation/studio/pages/*` |
| source CSS | `src/features/tech-log/presentation/styles/*` |
| `public/favicon.svg`, `public/media/fetch-strategy-boundary.svg` | same target-relative paths |
Component and stylesheet ports must retain source file boundaries where practical. Rename a file only for Vite/React Router clarity; do not combine components in a way that obscures parity review.
---
### Task 1: Freeze the migration baseline and dependency/assets contract
**Files:**
- Create: `docs/operations/techlog-ui-migration-baseline.md`
- Modify: `package.json`
- Modify: `pnpm-lock.yaml`
- Create: `public/favicon.svg`
- Create: `public/media/fetch-strategy-boundary.svg`
- Test: `tests/features/tech-log/migration-baseline.test.ts`
- [ ] **Step 1: Write the red asset-integrity test.** Assert each target SVG's SHA-256 against a hand-recorded expected source hash (never an external source-path read in CI) and exercise the evidence asset lookup so an altered path/hash breaks a consumer-visible contract. Dependency pins are verified by the package manager's frozen-lockfile command; the human baseline document and its route/CSS inventory are reviewed rather than tested as source text. Include the source commit SHA or, if the source worktree has uncommitted changes, its HEAD SHA plus `git status --short` in the baseline document.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/migration-baseline.test.ts
```
Expected: the migrated assets and evidence lookup are absent.
- [ ] **Step 3: Add exact dependencies and assets.** Run `corepack pnpm add pretendard@1.3.9 @fontsource/ibm-plex-mono@5.3.0 unified@11.0.5 remark-parse@11.0.0 remark-gfm@4.0.1 remark-directive@4.0.0`, copy only the two approved assets, and record checksums, source state, the 27 expected routes, and the five CSS files in the baseline document. Do not add Next/Vinext/Cloudflare packages.
- [ ] **Step 4: Run green and lockfile verification.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/migration-baseline.test.ts
corepack pnpm verify:lockfile
git diff --check
```
- [ ] **Step 5: Commit.**
```bash
git add package.json pnpm-lock.yaml public/favicon.svg public/media/fetch-strategy-boundary.svg docs/operations/techlog-ui-migration-baseline.md tests/features/tech-log/migration-baseline.test.ts
git commit -m "chore: establish TechLog migration baseline"
```
### Task 2: Port Studio API contracts and the application-facing feature seam
**Files:**
- Create: `src/features/tech-log/contracts/studio/studio-api.openapi.yaml`
- Create: `src/features/tech-log/contracts/studio/generated.ts`
- Create: `src/features/tech-log/contracts/studio/contract.ts`
- Create: `src/features/tech-log/application/ports/studio-gateway.ts`
- Create: `src/features/tech-log/application/ports/studio-gateway-error.ts`
- Create: `src/features/tech-log/application/ports/public-content-queries.ts`
- Create: `src/features/tech-log/application/tech-log-feature-input.ts`
- Test: `tests/features/tech-log/studio-contract.test.ts`
- Test: `tests/features/tech-log/feature-input.test.ts`
- [ ] **Step 1: Write red contract tests.** Port the source shape assertions and add compile/runtime assertions that the exact gateway method set above is exposed, feature ID is `tech-log`, and `ApplicationFeatureInputs["tech-log"]` accepts queries plus a gateway factory but no concrete adapter.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-contract.test.ts tests/features/tech-log/feature-input.test.ts
```
Expected: TechLog contracts and feature input do not exist.
- [ ] **Step 3: Port contracts without reshaping payloads.** Copy the OpenAPI and generated types, replace source-local aliases only, implement the port/error, define `PublicContentQueries` from the source query return shapes, and add the module augmentation shown above.
- [ ] **Step 4: Run green and inward-boundary checks.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-contract.test.ts tests/features/tech-log/feature-input.test.ts
corepack pnpm check:types:app
corepack pnpm check:architecture
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/contracts src/features/tech-log/application tests/features/tech-log/studio-contract.test.ts tests/features/tech-log/feature-input.test.ts
git commit -m "feat: add TechLog feature contracts"
```
### Task 3: Port Content Format v1 and the shared public render model
**Files:**
- Create: `src/features/tech-log/domain/content-format/heading-id.ts`
- Create: `src/features/tech-log/domain/content-format/inline-plain-text.ts`
- Create: `src/features/tech-log/domain/content-format/parse-case-content.ts`
- Create: `src/features/tech-log/domain/content-format/serialize-case-content.ts`
- Create: `src/features/tech-log/domain/content-format/project-public-render-model.ts`
- Create: `src/features/tech-log/domain/public-render-content.ts`
- Create: `src/features/tech-log/presentation/shared/public-render/*`
- Test: `tests/features/tech-log/content-format.test.ts`
- Test: `tests/features/tech-log/public-render.test.tsx`
- [ ] **Step 1: Port the source parser/serializer/renderer tests first.** Keep headings, inline text, GFM tables, directives, evidence figures, code blocks, callouts, malformed-input fallback, unsafe HTML/script/`javascript:`/unknown-asset rejection, and parse→serialize round-trip fixtures byte-equivalent. Include code-copy success/failure/live-region reset and evidence zoom open/backdrop-close/button-close/trigger-focus restoration.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
```
Expected: shared content functions/components are absent.
- [ ] **Step 3: Port the pure format code and renderer components.** Map `components/public-render/*`, `code-block.tsx`, `document-toc.tsx`, and both evidence-figure implementations into `presentation/shared`. Preserve emitted tags/classes/ARIA and sanitize/escape behavior; do not use raw HTML insertion.
- [ ] **Step 4: Run green and security/architecture gates.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
corepack pnpm check:browser-security
corepack pnpm check:architecture
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/domain src/features/tech-log/presentation/shared tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
git commit -m "feat: port TechLog content format and renderer"
```
### Task 4: Compose deterministic Public content and the mock Studio gateway
**Files:**
- Create: `src/features/tech-log/adapters/static/content.ts`
- Create: `src/features/tech-log/adapters/static/evidence-assets.ts`
- Create: `src/features/tech-log/adapters/static/public-content.ts`
- Create: `src/features/tech-log/adapters/static/public-query.ts`
- Create: `src/features/tech-log/domain/studio/document-state.ts`
- Create: `src/features/tech-log/domain/studio/local-id.ts`
- Create: `src/features/tech-log/adapters/mock/*`
- Create: `src/features/tech-log/adapters/create-tech-log-feature-input.ts`
- Modify: `src/features/installed-feature-adapters.ts`
- Test: `tests/features/tech-log/public-query.test.ts`
- Test: `tests/features/tech-log/studio-document-state.test.ts`
- Test: `tests/features/tech-log/mock-studio-gateway.test.ts`
- Test: `tests/features/tech-log/runtime-composition.test.ts`
- [ ] **Step 1: Port red source tests.** Cover Public type/kind/status/search filtering, stable ordering, source fixtures, state derivation, deterministic IDs/cursors, create/save conflict, validation, preview freshness/expiry, publish/unpublish, snapshots, idempotency, abort, and gateway error shapes.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-query.test.ts tests/features/tech-log/studio-document-state.test.ts tests/features/tech-log/mock-studio-gateway.test.ts tests/features/tech-log/runtime-composition.test.ts
```
Expected: data adapters and installed `tech-log` input are absent.
- [ ] **Step 3: Port data/state/mock code exactly.** Preserve fixture IDs, timestamps, copy, pagination cursors, validation issue order, error codes, stable stringify rules, preview content, and publication history. Add the TechLog input beside the temporarily retained reference-feature input in `createInstalledFeatureInputs`; one call to `createStudioGateway` creates one isolated mutable session.
- [ ] **Step 4: Run green and boundary checks.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-query.test.ts tests/features/tech-log/studio-document-state.test.ts tests/features/tech-log/mock-studio-gateway.test.ts tests/features/tech-log/runtime-composition.test.ts
corepack pnpm check:types:app
corepack pnpm check:architecture
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/adapters src/features/tech-log/domain/studio src/features/installed-feature-adapters.ts tests/features/tech-log/public-query.test.ts tests/features/tech-log/studio-document-state.test.ts tests/features/tech-log/mock-studio-gateway.test.ts tests/features/tech-log/runtime-composition.test.ts
git commit -m "feat: compose TechLog static and mock adapters"
```
### Task 5: Add the nested route-group capability and freeze TechLog route contracts
**Files:**
- Modify: `src/contracts/routes.ts`
- Modify: `src/contracts/route-runtime-contract.ts`
- Modify: `src/features/installed-feature-contracts.ts`
- Modify: `src/features/installed-feature-runtimes.tsx`
- Modify: `src/features/reference-feature/contracts/reference-feature-contract.ts`
- Modify: `config/contracts/registry-governance.json`
- Modify: `src/presentation/routes/app-router.tsx`
- Modify: `src/presentation/routes/route-codecs.ts`
- Modify: `src/presentation/routes/platform-route-codecs.ts`
- Create: `src/features/tech-log/contracts/tech-log-route-contract.ts`
- Create: `src/features/tech-log/contracts/tech-log-message-catalog.ts`
- Create: `src/features/tech-log/presentation/tech-log-route-codecs.ts`
- Test: `tests/features/tech-log/route-contract.test.ts`
- Modify: `tests/component/router.test.tsx`
- [ ] **Step 1: Add red route tests.** Assert the exact standalone 27-entry TechLog contract table, `layoutGroup`, Studio routes currently public, non-empty string codecs, generic Public/Studio parent assembly, Studio catch-all precedence over global catch-all, and canonical URL creation. The installed starter registry remains intact through this task so no route points to an unfinished screen.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/route-contract.test.ts tests/component/router.test.tsx
```
Expected: layout grouping and the standalone TechLog route contract are missing.
- [ ] **Step 3: Implement grouped route assembly without incomplete runtime entries.** Build a pure `createGroupedRouteObjects` helper that accepts a registry, matching runtime, and layout elements, then creates Public/Studio parent `RouteObject`s while retaining `RouteLifecycle`, `RouteInputProvider`, `ProtectedRoute`, Suspense, render boundary, and chunk recovery around each registered leaf. Add `layoutGroup: "PUBLIC"` to currently installed platform/reference definitions and keep the existing `AppShell` as the installed Public layout until Task 13 atomically installs the complete TechLog runtime. Extend `FE-REG-ROUTE` governance with required string field `layoutGroup`, allowed values `PUBLIC | STUDIO`, and the TechLog route/search schema IDs; add `layoutGroup` to breaking fields because layout lifetime changes navigation behavior.
- [ ] **Step 4: Run green and registry gates.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/route-contract.test.ts tests/component/router.test.tsx
corepack pnpm check:registries:structure
corepack pnpm check:architecture
```
- [ ] **Step 5: Commit.**
```bash
git add src/contracts/routes.ts src/contracts/route-runtime-contract.ts src/features/installed-feature-contracts.ts src/features/installed-feature-runtimes.tsx src/features/reference-feature/contracts/reference-feature-contract.ts src/features/tech-log/contracts/tech-log-route-contract.ts src/features/tech-log/contracts/tech-log-message-catalog.ts src/features/tech-log/presentation/tech-log-route-codecs.ts src/presentation/routes/app-router.tsx src/presentation/routes/route-codecs.ts src/presentation/routes/platform-route-codecs.ts config/contracts/registry-governance.json tests/features/tech-log/route-contract.test.ts tests/component/router.test.tsx
git commit -m "feat: add grouped TechLog route contracts"
```
### Task 6: Port exact styles, fonts, Public shell, header, and search dialog
**Files:**
- Create: `src/features/tech-log/presentation/styles/globals.css`
- Create: `src/features/tech-log/presentation/styles/studio.css`
- Create: `src/features/tech-log/presentation/styles/studio-editor.css`
- Create: `src/features/tech-log/presentation/styles/workflow.module.css`
- Create: `src/features/tech-log/presentation/styles/publication-flow.module.css`
- Modify: `src/main.tsx`
- Create: `src/features/tech-log/presentation/public/components/site-header.tsx`
- Create: `src/features/tech-log/presentation/public/components/search-dialog.tsx`
- Create: `src/features/tech-log/presentation/public/components/fatal-error-state.tsx`
- Create: `src/features/tech-log/presentation/public/public-shell.tsx`
- Test: `tests/features/tech-log/style-contract.test.ts`
- Test: `tests/features/tech-log/public-shell.test.tsx`
- [ ] **Step 1: Add red style and interaction contracts.** Render the real shell and assert its DOM/class/ARIA relationships plus consumer-visible computed typography, color, width, spacing, focus, and minimum target behavior where the test browser supports it; media-query transitions and full computed-style/pixel equality remain Task 14 browser assertions. Assert header links/labels match, `/` brand navigation works, search opens by click and keyboard, Escape/focus restoration work, and dialog results navigate to canonical routes. Do not grep CSS source text or assert private CSS-module key inventories.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/style-contract.test.ts tests/features/tech-log/public-shell.test.tsx
```
Expected: source styles/shell are absent.
- [ ] **Step 3: Copy styles and port shell components.** Import fonts and TechLog CSS after target `theme.css`; translate navigation APIs only. Preserve source header DOM and mobile behavior. Ensure Public pages render inside source-equivalent `<main>` without the old `AppShell` chrome.
- [ ] **Step 4: Run green and static style checks.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/style-contract.test.ts tests/features/tech-log/public-shell.test.tsx
corepack pnpm lint
corepack pnpm check:types:app
```
- [ ] **Step 5: Commit.**
```bash
git add src/main.tsx src/features/tech-log/presentation/styles src/features/tech-log/presentation/public tests/features/tech-log/style-contract.test.ts tests/features/tech-log/public-shell.test.tsx
git commit -m "feat: port TechLog shells and styles"
```
### Task 7: Port Public home, explore, and search screens
**Files:**
- Create: `src/features/tech-log/presentation/public/components/home-focus.tsx`
- Create: `src/features/tech-log/presentation/public/components/latest-index.tsx`
- Create: `src/features/tech-log/presentation/public/components/explore-filter-form.tsx`
- Create: `src/features/tech-log/presentation/public/components/public-record-list.tsx`
- Create: `src/features/tech-log/presentation/public/pages/home-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/explore-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/explore-kind-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/search-page.tsx`
- Create: `src/features/tech-log/domain/public/focus-state.ts`
- Test: `tests/features/tech-log/public-discovery-screens.test.tsx`
- [ ] **Step 1: Add red component cases.** Port source expectations for headings, introductory copy, counts, latest records, focus-tab URL normalization and Arrow/Home/End keyboard movement, explore kind/status/query filters, empty results, URL search synchronization, result ordering, keyboard submit, and result navigation.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-discovery-screens.test.tsx
```
Expected: discovery pages are missing.
- [ ] **Step 3: Port exact source JSX and bind queries.** Replace async Next server inputs with `useRouteInput` plus `application.features.get("tech-log").publicContent`; preserve DOM/classes/copy and query semantics. Use `Link`/`useNavigate` translations only.
- [ ] **Step 4: Run green and typecheck.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-discovery-screens.test.tsx
corepack pnpm check:types:app
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/domain/public/focus-state.ts src/features/tech-log/presentation/public tests/features/tech-log/public-discovery-screens.test.tsx
git commit -m "feat: port TechLog discovery screens"
```
### Task 8: Port Public cases, references, questions, and topics
**Files:**
- Create: `src/features/tech-log/presentation/public/components/public-document-header.tsx`
- Create: `src/features/tech-log/presentation/public/components/public-document-relations.tsx`
- Create: `src/features/tech-log/presentation/public/components/case-document-page.tsx`
- Create: `src/features/tech-log/presentation/public/components/reference-document-page.tsx`
- Create: `src/features/tech-log/presentation/public/components/question-document-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/case-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/reference-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/question-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/topic-page.tsx`
- Test: `tests/features/tech-log/public-document-screens.test.tsx`
- [ ] **Step 1: Add red cases from source rendered-HTML and interaction tests.** Assert each known slug's exact title, metadata, relation sections, table of contents, rendered blocks, evidence media, anchors, back links, and topic aggregation. Assert unknown slugs use Public not-found rather than a generic runtime error.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-document-screens.test.tsx
```
Expected: document route pages are missing.
- [ ] **Step 3: Port page/component JSX and connect shared renderer.** Preserve all source record ordering and classes. Keep specialized hard-coded source case pages represented by the same exact output at their canonical slugs.
- [ ] **Step 4: Run green plus accessibility component checks.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-document-screens.test.tsx tests/features/tech-log/public-render.test.tsx
corepack pnpm check:types:app
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/presentation/public tests/features/tech-log/public-document-screens.test.tsx
git commit -m "feat: port TechLog document screens"
```
### Task 9: Port projects, releases, profile, and Public fallbacks
**Files:**
- Create: `src/features/tech-log/presentation/public/components/project-navigation.tsx`
- Create: `src/features/tech-log/presentation/public/components/project-page-header.tsx`
- Create: `src/features/tech-log/presentation/public/pages/projects-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/project-overview-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/project-records-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/project-decisions-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/project-activity-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/releases-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/release-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/profile-page.tsx`
- Create: `src/features/tech-log/presentation/public/pages/public-not-found-page.tsx`
- Test: `tests/features/tech-log/public-index-screens.test.tsx`
- [ ] **Step 1: Add red cases.** Assert exact project tabs/active states, record/decision/activity filtering and order, release index/detail text, profile content, cross-links, and Public unknown-route/unknown-record copy.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-index-screens.test.tsx
```
Expected: remaining Public screens are missing.
- [ ] **Step 3: Port exact source markup and route wiring.** Translate Next links only; derive active tab from React Router location without changing element structure.
- [ ] **Step 4: Run complete Public green suite.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/public-*.test.tsx tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-query.test.ts
corepack pnpm check:types:app
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/presentation/public tests/features/tech-log/public-index-screens.test.tsx
git commit -m "feat: complete TechLog public screens"
```
### Task 10: Port Studio provider, runtime boundary, shell, dashboard, list, and creation
**Files:**
- Create: `src/features/tech-log/presentation/studio/studio-provider.tsx`
- Create: `src/features/tech-log/presentation/studio/use-studio.ts`
- Create: `src/features/tech-log/presentation/studio/studio-runtime-boundary.tsx`
- Create: `src/features/tech-log/presentation/studio/components/studio-header.tsx`
- Create: `src/features/tech-log/presentation/studio/components/studio-dashboard.tsx`
- Create: `src/features/tech-log/presentation/studio/components/document-list.tsx`
- Create: `src/features/tech-log/presentation/studio/components/new-document-form.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/studio-home-page.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/documents-page.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/new-document-page.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/studio-not-found-page.tsx`
- Create: `src/features/tech-log/presentation/studio/studio-shell.tsx`
- Test: `tests/features/tech-log/studio-shell-smoke.test.tsx`
- Test: `tests/features/tech-log/studio-screens-smoke.test.tsx`
- [ ] **Step 1: Port red shell/screen tests.** Assert one gateway creation per Studio shell session, a new gateway plus cleared requests/dialogs on `pageshow` with `persisted === true`, source header/nav/labels, dashboard totals/status links, list filters/pagination/empty/error/retry states, new-document type selection and redirect, direct route access without auth UI, and in-shell Studio not-found behavior.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx
```
Expected: Studio provider and real pages are absent.
- [ ] **Step 3: Port provider/shell/screens.** Resolve `createStudioGateway` through the application feature input once via lazy state/ref, preserve gateway state across child navigation, cancel obsolete requests, and preserve source loading/error/not-found markup. Recreate the gateway and provider generation when a persisted bfcache page is shown; use provider generation keys so all child state and dialogs reset with it.
- [ ] **Step 4: Run green and composition regression.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx tests/features/tech-log/runtime-composition.test.ts
corepack pnpm check:architecture
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/presentation/studio tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx
git commit -m "feat: port TechLog Studio shell and indexes"
```
### Task 11: Port document editors and instant preview
**Files:**
- Create: `src/features/tech-log/presentation/studio/components/common-document-fields.tsx`
- Create: `src/features/tech-log/presentation/studio/components/case-fields.tsx`
- Create: `src/features/tech-log/presentation/studio/components/reference-fields.tsx`
- Create: `src/features/tech-log/presentation/studio/components/question-fields.tsx`
- Create: `src/features/tech-log/presentation/studio/components/ordered-text-list.tsx`
- Create: `src/features/tech-log/presentation/studio/components/relation-editor.tsx`
- Create: `src/features/tech-log/presentation/studio/components/document-editor.tsx`
- Create: `src/features/tech-log/presentation/studio/components/document-editor-screen.tsx`
- Create: `src/features/tech-log/presentation/studio/components/instant-preview.tsx`
- Create: `src/features/tech-log/presentation/studio/components/document-status-rail.tsx`
- Create: `src/features/tech-log/presentation/studio/components/publication-flow-classes.ts`
- Create: `src/features/tech-log/presentation/studio/pages/document-edit-page.tsx`
- Test: `tests/features/tech-log/studio-editor-smoke.test.tsx`
- [ ] **Step 1: Port red editor tests.** Assert exact controls/order/labels for case/reference/question, loaded working-copy values, add/remove/reorder relations and ordered lists, dirty state, status rail, instant preview updates, focus behavior, and source accessibility names.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-editor-smoke.test.tsx
```
Expected: the edit route screen is missing or lacks source controls.
- [ ] **Step 3: Port editor components exactly.** Keep local working-copy state presentation-owned, reuse Content Format v1/shared Public renderer for preview, and preserve CSS module class-name composition through a typed `publication-flow-classes.ts` equivalent.
- [ ] **Step 4: Run green and renderer regression.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-editor-smoke.test.tsx tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
corepack pnpm check:types:app
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/presentation/studio tests/features/tech-log/studio-editor-smoke.test.tsx
git commit -m "feat: port TechLog Studio editors"
```
### Task 12: Implement save, conflict, dirty-leave, validation, and preview workflows
**Files:**
- Create: `src/features/tech-log/presentation/studio/components/guarded-studio-link.tsx`
- Create: `src/features/tech-log/presentation/studio/components/unsaved-leave-dialog.tsx`
- Create: `src/features/tech-log/presentation/studio/components/use-before-unload.ts`
- Create: `src/features/tech-log/presentation/studio/components/validation-report.tsx`
- Create: `src/features/tech-log/presentation/studio/components/validation-screen.tsx`
- Create: `src/features/tech-log/presentation/studio/components/public-preview-screen.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/document-validation-page.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/document-preview-page.tsx`
- Modify: `src/features/tech-log/presentation/studio/components/document-editor-screen.tsx`
- Test: `tests/features/tech-log/studio-save-navigation.test.tsx`
- Test: `tests/features/tech-log/studio-validation-preview.test.tsx`
- [ ] **Step 1: Add red workflow cases.** Cover save pending/success, revision conflict without data loss, gateway error/retry, internal link leave dialog, stay/discard/save-then-navigate choices, trigger focus restoration, browser `beforeunload`, validation issue anchors, validation state/freshness, preview create/current/stale/expired states, and exact next-action labels.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx
```
Expected: saves/guards and validation/preview pages are absent.
- [ ] **Step 3: Port workflow behavior.** Generate a new idempotency key per user command and reuse it only for that command's safe retry. Keep source dirty/conflict semantics, dialog DOM/focus trap/return focus, and derived state copy. Abort route-obsolete reads without converting aborts into visible errors.
- [ ] **Step 4: Run green and relevant browser smoke.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx tests/features/tech-log/mock-studio-gateway.test.ts
corepack pnpm check:browser-security
```
- [ ] **Step 5: Commit.**
```bash
git add src/features/tech-log/presentation/studio tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx
git commit -m "feat: port TechLog Studio validation workflow"
```
### Task 13: Port publish, unpublish, publication history, and immutable snapshots
**Files:**
- Create: `src/features/tech-log/presentation/studio/components/warning-acknowledgements.tsx`
- Create: `src/features/tech-log/presentation/studio/components/publish-screen.tsx`
- Create: `src/features/tech-log/presentation/studio/components/unpublish-dialog.tsx`
- Create: `src/features/tech-log/presentation/studio/components/publication-list.tsx`
- Create: `src/features/tech-log/presentation/studio/components/publication-event-preview-screen.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/document-publish-page.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/publications-page.tsx`
- Create: `src/features/tech-log/presentation/studio/pages/publication-preview-page.tsx`
- Create: `src/features/tech-log/presentation/tech-log-route-runtime.tsx`
- Modify: `src/contracts/routes.ts`
- Modify: `src/contracts/route-runtime-contract.ts`
- Modify: `src/features/installed-feature-contracts.ts`
- Modify: `src/features/installed-feature-runtimes.tsx`
- Modify: `src/features/installed-feature-adapters.ts`
- Modify: `src/features/installed-feature-messages.ts`
- Modify: `src/presentation/routes/route-runtime.tsx`
- Modify: `src/presentation/routes/platform-route-codecs.ts`
- Modify: `public/release-manifest.json`
- Modify: `scripts/test-performance.ts`
- Modify: `tests/component/router.test.tsx`
- Modify: `tests/component/runtime-application.test.tsx`
- Modify: `tests/features/reference-feature/reference-contract.test.ts`
- Remove: `src/features/reference-feature/presentation/**`
- Remove: `src/presentation/examples/auth-example-page.tsx`
- Remove: `src/presentation/examples/platform-overview-page.tsx`
- Remove: `src/presentation/examples/state-gallery-page.tsx`
- Remove: `src/presentation/examples/ui-gallery-page.tsx`
- Remove: `src/presentation/pages/home-page.tsx`
- Remove: `src/presentation/pages/not-found-page.tsx`
- Remove: `tests/component/platform-overview-page.test.tsx`
- Remove: `tests/features/reference-feature/reference-page.test.tsx`
- Remove: `tests/features/reference-feature/reference-production-vertical.test.tsx`
- Remove: `tests/e2e/platform-overview.spec.ts`
- Remove: `tests/e2e/reference-form.spec.ts`
- Remove: `tests/e2e/reference-route.spec.ts`
- Remove: `tests/e2e/ui-gallery.spec.ts`
- Test: `tests/features/tech-log/studio-publication-flow.test.tsx`
- E2E: `tests/e2e/tech-log-studio-workflow.spec.ts`
- [ ] **Step 1: Port red publication tests.** Assert blocked publish for invalid/stale preview, warning acknowledgement requirements, pending/error/retry behavior, successful event/URL/state, publication filtering, unpublish reason/confirmation, immutable historical snapshot rendering after later edits, and unknown publication Studio not-found.
- [ ] **Step 2: Run red.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-publication-flow.test.tsx
```
Expected: publication screens are missing.
- [ ] **Step 3: Port publication flow exactly.** Preserve source DOM/classes/copy and gateway command ordering. Render event snapshots through the shared Public renderer; never substitute current working-copy content.
- [ ] **Step 4: Atomically install the complete feature.** Make `ROUTE_REGISTRY`, `ROUTE_RUNTIME_CONTRACT`, codecs, runtime imports, and release-manifest chunk entries expose only the complete TechLog routes; configure `PublicShell` and `StudioShell` as their layout elements. Keep the non-UI reference contracts/adapters and their HTTP/platform tests installed as template contract fixtures, but remove their product routes, runtime pages, page-level tests, and every `/examples/*` screen. Update the reference contract test so it continues to prove API/schema/invalidation behavior without asserting product route installation. Retain platform boot, diagnostics, providers, lifecycle boundaries, service worker, and generic design-system infrastructure.
- [ ] **Step 5: Update route consumers.** Rewrite router/runtime-application expectations for the TechLog home, point the performance probe at `TECH_LOG_HOME`, and write all 27 derived chunk IDs to `public/release-manifest.json`. Search the active source/tests/config for old product route imports and prove only deliberately retained platform test-fixture IDs remain.
- [ ] **Step 6: Run green, removal, and end-to-end workflow.**
```bash
corepack pnpm exec vitest run tests/features/tech-log/studio-publication-flow.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx tests/features/tech-log/mock-studio-gateway.test.ts
corepack pnpm exec vitest run tests/component/router.test.tsx tests/component/runtime-application.test.tsx tests/features/reference-feature/reference-contract.test.ts
corepack pnpm test:sample-removal
corepack pnpm check:registries:structure
corepack pnpm exec playwright test tests/e2e/tech-log-studio-workflow.spec.ts --project=chromium
```
- [ ] **Step 7: Commit.**
```bash
git add -A -- src/contracts src/features src/presentation/examples src/presentation/pages src/presentation/routes public/release-manifest.json scripts/test-performance.ts tests/features tests/component tests/e2e/platform-overview.spec.ts tests/e2e/reference-form.spec.ts tests/e2e/reference-route.spec.ts tests/e2e/ui-gallery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts
git commit -m "feat: complete TechLog Studio publication flow"
```
### Task 14: Prove visual, responsive, accessibility, architecture, and release parity
**Files:**
- Create: `tests/visual/tech-log.visual.spec.ts`
- Create: `tests/e2e/tech-log-public-discovery.spec.ts`
- Create: `tests/e2e/tech-log-accessibility.spec.ts`
- Create: `tests/e2e/tech-log-responsive.spec.ts`
- Modify: `tests/e2e/app-shell.spec.ts`
- Modify: `tests/e2e/accessibility.spec.ts`
- Modify: `tests/e2e/responsive.spec.ts`
- Remove: `tests/e2e/compact-smoke.spec.ts`
- Remove: `tests/e2e/design-system-interactions.spec.ts`
- Remove: `tests/e2e/i18n.spec.ts`
- Remove: `tests/e2e/theme.spec.ts`
- Remove: `tests/visual/platform.visual.spec.ts`
- Remove: `tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/**`
- Modify: `config/contracts/registry-change-evidence.json`
- Modify: `config/contracts/registry-baseline.json`
- Modify: `config/contracts/registry-baseline.approval.json`
- Modify: `docs/operations/techlog-ui-migration-baseline.md`
- Modify: `README.md`
- [ ] **Step 1: Add parity suites before accepting snapshots.** Cover every canonical route definition, all known Public fixture slugs/versions, and every Studio screen/state. Capture full parity at 360 and 1440 pixels, breakpoint transitions at 1179/1180, 1050, 1024, 980, 900, 767/768, 420, 390, 820, and a compact 375-pixel viewport, with fixed timezone, fonts-ready wait, animation disabled, deterministic clock/data, and no masks.
- [ ] **Step 2: Establish source references.** Run the source app and target app under the same Chromium viewport/device scale/color scheme, capture both into temporary artifact directories, and use pixel diff plus DOM/class/text/ARIA assertions. Require zero pixel difference after deterministic controls; if browser rasterization still differs, document the exact pixels/cause and obtain user approval before accepting a target baseline. Source reference images are not copied into target snapshots as a shortcut, and committed/CI tests read only target fixtures and snapshots rather than the external source path.
- [ ] **Step 3: Run visual/responsive/a11y red.**
```bash
corepack pnpm exec playwright test tests/visual/tech-log.visual.spec.ts --config=playwright.visual.config.ts --project=chromium
corepack pnpm exec playwright test tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
```
Expected before final corrections: any remaining framework-port drift is reported with a route/viewport-specific diff.
- [ ] **Step 4: Correct only parity defects.** Fix DOM/CSS/import ordering/router lifecycle differences without redesign. Confirm zero unexpected console errors, no horizontal overflow, keyboard-accessible dialogs/navigation, valid heading/landmark order, restored focus, and Axe results matching or improving on source without changing appearance.
- [ ] **Step 5: Retire starter-only browser evidence.** Rewrite `app-shell`, registry-wide accessibility, and responsive suites against TechLog. Delete the example-gallery/theme/locale E2E cases because those controls intentionally leave the product UI, while retaining direct component/design-system tests for the underlying platform capabilities. Replace old platform screenshots with reviewed TechLog screenshots; do not leave stale snapshots unreferenced.
- [ ] **Step 6: Record and accept the governed route/schema migration.** Run `corepack pnpm check:registries` once to generate `artifacts/quality/registries.json` and list the exact breaking change IDs. Add one complete evidence row per reported breaking change to `registry-change-evidence.json`, covering the TechLog route migration/version, atomic release-manifest/runtime update, same-release compatibility window, rollback to the prior feature commit, and owner `tech-log-frontend`. Rerun until evidence passes, then execute:
```bash
REGISTRY_BASELINE_OWNER=tech-log-frontend REGISTRY_BASELINE_REASON="Install approved TechLog Public and Studio route contract" node scripts/update-registry-baseline.ts artifacts/quality/registries.json
corepack pnpm check:registries
```
Expected: approval digest matches the newly committed snapshot and compatibility impact is `none` with no unacknowledged change.
- [ ] **Step 7: Run focused full TechLog gates.**
```bash
corepack pnpm exec vitest run tests/features/tech-log
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
corepack pnpm test:visual
```
- [ ] **Step 8: Run repository gates and classify baseline-only failures.**
```bash
corepack pnpm check:types
corepack pnpm lint
corepack pnpm check:architecture
corepack pnpm check:design-system
corepack pnpm check:i18n
corepack pnpm check:registries
corepack pnpm check:browser-security
corepack pnpm test:all
corepack pnpm build
git diff --check
git status --short
```
All gates must pass except an exactly reproduced, documented environment-only baseline. For any baseline exception, rerun its test in isolation, record command/output/count in the baseline document, and prove no TechLog test is among the failures.
- [ ] **Step 9: Use the verification and review skills.** Invoke `superpowers:verification-before-completion`, then `superpowers:requesting-code-review`. Resolve findings with focused red/green tests and rerun affected gates.
- [ ] **Step 10: Commit final parity evidence.**
```bash
git add -A -- tests/visual tests/e2e config/contracts/registry-change-evidence.json config/contracts/registry-baseline.json config/contracts/registry-baseline.approval.json README.md docs/operations/techlog-ui-migration-baseline.md
git commit -m "test: prove TechLog UI migration parity"
```
- [ ] **Step 11: Stop before integration.** Report the feature branch commit range, exact green commands, baseline-only exceptions, visual-diff result, and changed-route inventory. Wait for explicit user approval before `git flow feature finish techlog-ui-migration`, merging into `develop`, pushing, or deleting the feature branch.
## Definition of done
- All 27 canonical route definitions resolve under the correct nested shell, with source-equivalent unknown-content behavior.
- Public content/search/filter/navigation output matches the source data and UI.
- Studio create/edit/save/conflict/validate/preview/publish/unpublish/history flows match the source and persist for one shell session.
- No migrated presentation module imports an adapter, Next.js, Vinext, or Cloudflare module.
- Source assets and all non-Tailwind CSS rules are preserved exactly; every specified viewport has reviewed visual evidence with no unexplained pixel drift.
- Focus, keyboard, dialog, landmarks, labels, and Axe coverage pass.
- Focused tests, typecheck, lint, architecture, registry, browser-security, build, and applicable repository suites pass, with any infrastructure-only baseline reproduced and documented.
- The work remains on `feature/techlog-ui-migration` until the user explicitly authorizes GitFlow feature completion into `develop`.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,328 @@
# TechLog 전체 UI 이식 설계
## 상태
- 승인일: 2026-08-15
- 원본: `/home/donghyeon/workspace/techlog-studio-frontend`
- 대상: `/home/donghyeon/workspace/desktop-server-git/tech-log-frontend`
- 결정: 원본 UI를 보존하고 현재 Vite·React Router·포트/어댑터 구조로 내부 경계만 치환한다.
## 목적
원본 TechLog의 Public과 Studio 전체 화면, URL 구조, 표시 콘텐츠, 상호작용과 세션 기반 Mock 동작을 대상 프로젝트로 이식한다. 대상 프로젝트의 부트스트랩, 오류 경계, 진단, 서비스 워커, route registry와 clean architecture 경계는 유지한다.
이 작업은 새 디자인을 만드는 작업이 아니다. 원본의 DOM 구조, CSS 계산값, 폰트, 자산, 문구, 반응형 동작과 접근성 구조를 기준본으로 삼는다. 구현 편의를 위한 시각적 재해석이나 대상 디자인 시스템에 맞춘 재디자인을 허용하지 않는다.
## 범위
### Public
- 홈과 현재 집중 항목
- 통합 탐색과 유형별 탐색
- Case, Reference, Question 공개 문서
- Topic별 기록
- Project 목록, 개요, 기록, 결정, 활동
- Release 목록과 상세
- Profile
- 헤더 검색 dialog와 검색 결과
- Public 오류, 빈 상태와 404
- 공통 header, footer, document renderer, TOC, 관계, 코드, 표, callout, evidence figure
### Studio
- Studio dashboard
- 작업본 검색·필터·정렬 목록
- Case, Reference, Question 생성
- 편집, 즉시 preview와 저장 상태
- 검증과 issue 위치 이동
- Public preview
- 게시·재게시·게시 취소
- 게시 이벤트 목록과 불변 snapshot
- 충돌, 만료, 요청 실패, 세션 전용 not-found
- 미저장 변경의 내부 이동 dialog와 native `beforeunload`
### 동작 경계
- 원본의 정적 Public 콘텐츠와 query 동작을 유지한다.
- 원본의 `StudioGateway` 계약과 `MockStudioGateway` 상태 전이를 유지한다.
- Studio 세션의 게시 결과는 Public 정적 콘텐츠와 검색 결과를 변경하지 않는다.
- `localStorage`, 실제 서버 저장, 파일 업로드와 실제 배포 게시를 추가하지 않는다.
- Studio 인증은 후속 작업으로 유보한다. 이번 이식에서는 Studio URL에 직접 접근할 수 있고 가짜 로그인 UI를 추가하지 않는다.
## 선택한 접근
### 원본 UI 보존형 이식
원본의 React 마크업, class 이름, CSS, 폰트, 자산과 문구를 유지하고 Next/Vinext 전용 경계만 대상 런타임으로 치환한다.
- `next/link`는 React Router `Link` 또는 의도된 전체 문서 이동 `<a>`로 바꾼다.
- `usePathname`, App Router params와 search params는 React Router route input으로 바꾼다.
- Next layout 수명은 React Router 중첩 layout route로 재현한다.
- 서버 컴포넌트의 정적 조회는 순수 application query와 동기 projection으로 바꾼다.
- Studio의 비동기 요청과 오류는 주입된 `StudioGateway` port를 통해 유지한다.
별도 legacy SPA 삽입은 라우터·상태·오류 경계를 이중화하므로 사용하지 않는다. 대상을 Next/Vinext로 전환하는 방식은 현재 템플릿과 운영 계약을 폐기하므로 사용하지 않는다.
## 대상 아키텍처
TechLog 기능은 하나의 feature boundary 안에서 Public과 Studio 하위 영역을 공유한다. Public renderer를 Studio preview가 함께 사용해야 하므로 두 영역을 서로 독립된 feature로 분리하지 않는다.
```text
src/features/tech-log/
├── contracts/ route, content format, gateway DTO와 schema
├── domain/ Public content와 Studio document/publication 모델
├── application/ Public query, projection, Studio state 계산과 port
├── adapters/
│ ├── static/ 원본 Public 콘텐츠 catalog
│ └── mock/ 세션 수명의 MockStudioGateway
└── presentation/
├── public/ Public shell, pages와 renderer
├── studio/ Studio shell, pages, provider와 editor
├── shared/ 양쪽이 공유하는 안전한 render component
└── styles/ 원본 CSS와 CSS Module
```
공통 플랫폼은 feature의 route contract, route runtime과 adapter factory만 조립한다. Presentation이 adapter 구현을 직접 import하지 않으며 composition root가 gateway와 Public catalog를 주입한다.
대상 템플릿의 기존 example navigation, sidebar, theme·locale selector와 인증 예제 UI는 TechLog 제품 화면에서 제거한다. 더 이상 route registry에서 참조되지 않는 sample presentation은 대상의 sample-removal 정책에 따라 제거한다. 부트 오류와 플랫폼 진단 경계는 유지한다.
## 라우팅과 layout 수명
공통 router는 플랫폼 provider와 오류 경계를 유지하고 두 개의 시각 layout group을 만든다.
```text
공통 부트스트랩·플랫폼 오류 경계
├── PUBLIC layout
│ ├── PublicShell
│ ├── Public routes
│ └── Public 404
└── STUDIO layout
├── StudioRuntimeBoundary
├── StudioProvider
├── StudioShell
├── Studio routes
└── Studio 전용 상태
```
### Public route pattern
| 경로 | 책임 |
| --- | --- |
| `/` | 홈 |
| `/explore` | 통합 탐색 |
| `/explore/:kind` | 유형별 탐색 |
| `/cases/:slug` | Case 문서 |
| `/references/:slug` | Reference 문서 |
| `/questions/:slug` | Question 문서 |
| `/topics/:slug` | Topic별 기록 |
| `/projects` | Project 목록 |
| `/projects/:slug` | Project 개요 |
| `/projects/:slug/records` | Project 기록 |
| `/projects/:slug/decisions` | Project 결정 |
| `/projects/:slug/activity` | Project 활동 |
| `/releases` | Release 목록 |
| `/releases/:version` | Release 상세 |
| `/profile` | Profile |
| `/search` | 검색 결과 |
| `*` | Public 404 |
### Studio route pattern
| 경로 | 책임 |
| --- | --- |
| `/studio` | dashboard |
| `/studio/documents` | 작업본 목록 |
| `/studio/documents/new` | 새 문서 생성 |
| `/studio/documents/:id/edit` | 편집과 즉시 preview |
| `/studio/documents/:id/validation` | 검증 보고서 |
| `/studio/documents/:id/preview` | 저장·검증된 Public preview |
| `/studio/documents/:id/publish` | 게시·재게시 |
| `/studio/publications` | 게시 이벤트 목록 |
| `/studio/publications/:publicationEventId/preview` | 불변 snapshot |
| 정의되지 않은 `/studio/*` | 실제 route 404 |
알 수 없는 동적 document ID와 publication event ID는 route에는 일치하지만 Studio shell 안의 전용 찾을 수 없음 상태를 렌더링한다. 선행 검증이나 preview가 부족한 직접 진입은 redirect하지 않고 원본과 같은 차단 이유와 다음 행동을 보여 준다.
Studio layout의 provider는 Studio 내부 client navigation 동안 유지된다. Studio에서 Public으로 가는 `공개 사이트 보기`는 일반 `<a>`를 사용해 전체 문서 이동, `beforeunload`와 세션 초기화를 보존한다.
라우트 정의에는 `PUBLIC` 또는 `STUDIO` layout group을 명시한다. Studio 인증을 구현할 때는 `STUDIO` group의 access policy만 `session-required`로 전환할 수 있어야 하며 화면 컴포넌트나 URL을 다시 설계하지 않는다.
## 컴포넌트 이식 규칙
- 원본 HTML tag, class 이름, 표시 문구, 요소 순서와 ARIA 관계를 유지한다.
- 원본 컴포넌트 경계를 가능한 한 유지하되 Next layout과 router hook에만 필요한 변경을 한다.
- 대상 generic design-system primitive로 화면을 다시 그리지 않는다.
- Public과 Studio가 공유하는 Public renderer는 하나만 유지한다.
- 문자열 HTML과 `dangerouslySetInnerHTML`을 도입하지 않는다.
- source의 semantic heading, landmark, tab, dialog, live region과 focus restoration을 유지한다.
- 검색 dialog는 하나만 렌더링하고 원본처럼 viewport 중앙에 둔다.
- Studio editor 탭 전환은 draft를 잃지 않으며 즉시 preview는 gateway 상태를 바꾸지 않는다.
## 스타일·폰트·자산 보존
원본의 다음 파일을 시각 기준으로 사용한다.
- `app/globals.css`
- `app/studio.css`
- `app/studio-editor.css`
- `components/studio/workflow.module.css`
- `components/studio/publication-flow.module.css`
- `pretendard/dist/web/variable/pretendardvariable.css`
- `@fontsource/ibm-plex-mono/400.css`
- `@fontsource/ibm-plex-mono/500.css`
- `public/favicon.svg`
- `public/media/fetch-strategy-boundary.svg`
대상 `theme.css`의 Tailwind import와 플랫폼 token은 부트 오류 같은 플랫폼 표면을 위해 유지한다. TechLog 전역 스타일은 그 뒤에 unlayered CSS로 한 번만 로드한다. 원본 `globals.css`의 중복 `@import "tailwindcss"`만 제외하며 그 뒤 rule 순서와 선언값은 유지한다.
다음 값은 재해석하거나 대상 token 값으로 치환하지 않는다.
- `--canvas`, `--paper`, `--ink`, `--muted`, `--faint`, `--signal` 등 원본 color token
- `--shell: 1180px`, `--body-copy: 42rem`
- font size, weight, line-height와 letter-spacing
- border, radius, shadow와 transition
- `1179`, `1050`, `1024`, `980`, `900`, `767`, `420px` breakpoint
- reduced-motion 동작과 최소 `44px` interaction target
Public과 Studio의 최종 computed style은 원본이 기준이다. 자산은 내용 변경 없이 복사하고 build가 제공하는 동일-origin URL을 사용한다.
## Public 데이터와 query
Public 콘텐츠는 immutable static catalog adapter가 소유한다. Application query는 catalog port만 사용해 다음 결과를 파생한다.
- 홈 focus와 latest index
- 유형·topic·project filter
- 제목·요약·topic·project 검색
- Project별 record, decision, activity
- 문서 relation과 related content
- Release와 Profile
페이지 컴포넌트는 URL params와 query를 route codec으로 검증한 뒤 application query를 호출한다. 잘못된 filter 값은 원본의 canonical 상태로 정규화하고 검색 query는 URL에 보존한다. 존재하지 않는 slug와 version은 Public 404로 보낸다.
## Content Format과 공유 renderer
Case 본문의 Content Format v1 parser, serializer와 `PublicRenderModel` 판별 union을 유지한다. 지원 block은 heading, paragraph, blockquote, ordered/unordered list, code block, data table, callout와 evidence figure다. 지원 inline은 text, emphasis, strong, inline code, link와 status다.
Public 문서와 Studio 즉시 preview, 검증 preview, publication snapshot은 같은 typed renderer를 사용한다. raw HTML, script, `javascript:` URL과 임의 asset URL은 계속 거절한다.
## Studio port, adapter와 상태
Presentation은 `StudioGateway` port만 사용한다. 모든 method는 `Promise`를 반환하고 선택적인 `AbortSignal`을 받는다. 예상 가능한 실패는 RFC 9457 본문, status, code와 retryable 정보를 가진 `StudioGatewayError`로 정규화한다. `AbortError`만 조용히 무시하고 프로그래밍 오류는 runtime boundary로 전달한다.
Mock adapter는 원본 seed data, cursor, conflict fixture, preview expiry, validation, publication aggregate와 idempotency 동작을 보존한다.
Studio 상태 축은 다음 값을 유지한다.
- Editor: `CLEAN`, `DIRTY`, `SAVING`, `CONFLICT`
- Validation result: `NOT_RUN`, `INVALID`, `WARNINGS`, `VALID`
- Validation freshness: `NONE`, `CURRENT`, `STALE`
- Preview: `NONE`, `CURRENT`, `STALE`, `EXPIRED`
- Publication: `NEVER_PUBLISHED`, `PUBLISHED`, `UNPUBLISHED`
저장은 불완전 draft를 허용하고 version을 증가시킨다. 저장 뒤 validation은 `NOT_RUN``NONE`, 기존 preview는 `STALE`이 된다. 게시 준비 검증과 다음 행동 우선순위, warning acknowledgement, publication idempotency와 unpublish 규칙은 원본 계약을 유지한다.
Studio 세션은 layout이 유지되는 동안만 살아 있다. 새로고침, Public 전체 이동과 `pageshow.persisted === true`에서 새 Mock adapter를 만들고 draft, request와 dialog를 초기화한다. 고정 fixture ID는 seed 상태로 돌아가고 세션 생성 ID는 Studio 전용 찾을 수 없음 상태가 된다.
## 오류와 빈 상태
- Boot failure는 기존 `BootErrorShell`이 담당한다.
- Public query·render 실패는 Public shell 안의 원본 fatal error surface를 사용한다.
- Public empty, no-result와 not-found는 서로 다른 원본 상태를 유지한다.
- Studio request failure는 `StudioGatewayError`의 code와 retryable을 기준으로 원본 메시지와 action을 표시한다.
- Studio render failure는 `StudioRuntimeBoundary`가 담당한다.
- save conflict는 현재 입력을 보존하고 gateway 최신본과 field path를 비교한다.
- auto merge와 auto overwrite를 추가하지 않는다.
- dirty 내부 이동은 머무르기, 변경 버리기와 저장 후 이동을 제공하고 trigger focus를 복원한다.
## 인증 유보
Studio는 인증이 필요한 제품 영역이지만 이번 이식에서는 인증 구현을 범위 밖으로 둔다.
- Studio route group을 별도로 유지한다.
- access policy 전환 지점을 route contract에 둔다.
- 현재는 직접 URL 접근을 허용한다.
- 가짜 로그인, 임시 계정과 인증된 것처럼 보이는 UI를 추가하지 않는다.
- 후속 인증 작업은 Studio 화면 DOM, URL과 gateway contract를 변경하지 않고 route guard와 session adapter를 연결하는 방식으로 수행한다.
## GitFlow와 전달 브랜치
저장소는 `main`을 production 브랜치, `develop`을 integration 브랜치로 사용하는 GitFlow로 초기화한다. 이 설계 문서와 선행 template 동기화가 포함된 현재 `main`에서 `develop`을 만든다.
- feature prefix는 `feature/`를 사용한다.
- 화면 이식 작업은 `develop`에서 시작한 `feature/techlog-ui-migration`에서만 수행한다.
- 구현 계획, 테스트, source port, 자산과 검증 문서는 같은 feature 브랜치에 커밋한다.
- `main``develop`에는 화면 이식 production code를 직접 커밋하지 않는다.
- feature 통합은 구현·검증 완료 뒤 사용자가 선택한 방식으로 수행한다.
- 원격 push, remote branch 생성과 GitFlow feature finish는 별도 사용자 요청 전에는 수행하지 않는다.
## 테스트 전략
모든 production 동작 변경은 TDD로 진행한다. 각 slice는 기대 동작을 표현하는 실패 테스트를 먼저 추가하고 예상한 이유로 실패하는 것을 확인한 다음 최소 구현을 추가한다.
### 구조·계약 테스트
- 모든 Public·Studio route ID, path, layout group과 runtime module mapping
- Public/Studio layout 수명과 Studio gateway 단일 instance
- 원본 heading, landmark, class, element 순서와 ARIA 관계
- source CSS color, typography, width, breakpoint와 touch target 계약
- Public content graph의 유효한 내부 링크와 404 경계
- Content Format parser·serializer round trip과 unsafe input 거절
### 상호작용·상태 테스트
- 홈 focus tab과 URL 정규화
- 탐색 filter, reset, empty와 no-result
- 검색 dialog open/close, focus trap·restore와 query 보존
- document TOC, code copy와 evidence dialog
- Studio document 생성, 편집, 저장, validation, preview, publish와 unpublish
- conflict, stale·expired preview, warning acknowledgement와 idempotency
- dirty navigation dialog와 native `beforeunload`
- 새로고침·Public 이동·bfcache 복원 뒤 세션 reset
### 접근성·반응형·시각 검증
동일한 Chromium, font와 reduced-motion 조건에서 원본과 대상을 캡처한다.
- 고정 fixture로 접근 가능한 모든 canonical Public·Studio route: `360px`, `1440px`
- breakpoint 대표 화면: `390`, `768`, `820`, `1024`, `1180px`
- 열린 검색 dialog와 mobile menu
- Studio editor, 즉시 preview, warning과 dirty-leave dialog
- viewport 전체의 예상하지 않은 가로 overflow
- axe, keyboard navigation, focus visibility, single H1과 `aria-current`
동적 timestamp, caret와 animation을 고정한 뒤 pixel difference는 원칙적으로 `0`을 요구한다. 차이는 개선 여부가 아니라 원본과 동일한지로 판정한다. 브라우저 rasterization처럼 통제할 수 없는 차이가 발견되면 원인을 기록하고 사용자의 별도 승인을 받기 전에는 baseline을 갱신하지 않는다.
CI는 커밋된 target snapshot과 계약 테스트를 사용하며 외부 원본 경로에 의존하지 않는다. 구현 중 로컬 one-time source/target 비교로 baseline을 만들고 이후 target regression test로 고정한다.
## 검증 명령과 완료 기준
구현 완료 전에 다음 범주를 모두 실행한다.
- TechLog route·component·integration test
- TechLog Studio gateway·state·publication test
- Playwright visual·accessibility test
- TypeScript 전체 project 검사
- ESLint
- architecture, route registry, design-system과 i18n contract 검사
- production build
- repository full test suite
현재 저장소에 이미 기록된 RLIMIT, EMFILE, umask와 `/tmp` 관련 19개 환경 의존 실패는 known baseline으로 분리한다. 그 외 새 실패를 허용하지 않으며 TechLog 이식으로 추가된 테스트는 모두 통과해야 한다. 병합 전에는 known baseline과 새 실패를 구분한 결과를 사용자에게 보고한다.
완료 조건은 다음과 같다.
1. 원본 Public과 Studio canonical URL이 대상에서 모두 열리고 정의되지 않은 경로가 올바른 404를 반환한다.
2. 원본의 표시 콘텐츠, DOM·ARIA 구조, CSS 계산값, font와 asset이 유지된다.
3. Public 검색·탐색·문서 연결과 Studio 전체 Mock workflow가 원본과 동일하게 동작한다.
4. Studio 내부 이동 동안 상태가 유지되고 전체 문서 이동·새로고침·bfcache에서 초기화된다.
5. 대상의 clean architecture, route registry, 부트·진단·서비스 워커 계약이 유지된다.
6. 승인되지 않은 시각 diff와 새 test failure가 없다.
## 범위 밖
- Studio 인증과 권한
- 실제 HTTP Studio adapter와 백엔드 연결
- PostgreSQL, Cloudflare D1·R2와 파일 업로드
- Public 콘텐츠 CMS화
- 디자인 개선, 문구 수정과 정보 구조 재해석
- 원본에 없는 화면이나 기능 추가
@@ -0,0 +1,402 @@
# TechLog Backend 정합 설계
## 상태
- 승인일: 2026-08-17
- 기준선: `tech-log-frontend` `main` (UI 이식 완료 상태)
- 원본 요구: `/home/donghyeon/workspace/tech-log-alignment-design/01-tech-log-frontend-alignment-design.md`
- Canonical 계약: `/home/donghyeon/workspace/tech-log-design-package/contracts/openapi/studio-v1.yaml`
- Specification Version `2.0.0`
- **digest·revision의 단일 기록처는 `src/features/tech-log/contracts/studio/canonical-source.json`이다.**
이 문서는 그 값을 복제하지 않는다. 승인 시점에 여기 적혀 있던
`sha256:85a65004…` / revision `0ec5582`은 구현 중 canonical yaml이 갱신되면서
무효가 됐고, 실제로 vendor·고정된 값은 `canonical-source.json`이 기록한
`sha256:99f54f56…` / revision `ce2e748`이다(Task 12 표 3행의 `ce2e748`과 동일).
- 계약 기여(`tech-log-studio-contract-contribution.ts`)는 그 파일을 import해서
`EXTERNAL_PACKAGE` provenance를 채우고, `check:tech-log-contract`가 vendor
사본·`generated.ts`와의 일치를 검증한다. 두 곳이 다시 갈라질 수 있는 지점은
없다.
- 결정: 현재 Public/Studio UI 기준선을 고정하고, Studio 계약·전송 경계와 Asset capability를 canonical 계약에 정합시킨다. Public 조회의 HTTP 전환은 이 사이클에서 제외한다.
### Task 12 완료 상태 (2026-08-18)
12개 Task 전부 `feature/techlog-backend-alignment`에 커밋됐다. 아래는 §완료 조건의 12개 항목을 Task 12 게이트 실행(전체 로그는
`.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-12-report.md`)과 Task 1–11이 기록한 구현 상태를 근거로 판정한 결과다.
| # | 완료 조건 | 판정 | 근거 |
|---|---|---|---|
| 1 | 현재 Public UI·라우트가 변경되지 않는다 | 충족 | Public 화면 테스트(`public-document-screens.test.tsx` 등) 무변경 통과; `case-body-renderer.tsx`/`evidence-figure.tsx``9e5fbd1..HEAD` 사이 diff가 비어 있어 렌더러 코드에 변경이 없다(`check:architecture`/`check:registries`는 import 그래프·레지스트리 정합만 보고 Public 렌더 출력을 관찰하지 않으므로 이 판정의 근거가 아니다). `test:visual`의 Public 스냅샷 실패는 이 브랜치가 아니라 `main``79e9aa8`에서 물려받은 것이다(아래 §Task 12 참고) |
| 2 | 현재 Studio 작업 흐름이 변경되지 않는다 | 충족 | 기본 `MOCK`에서 `test:tech-log`(36 files/303 tests) 전부 PASS; `tech-log-studio-workflow.spec.ts` chromium 2/2 PASS |
| 3 | Studio 계약이 canonical에서 생성되고 digest 고정·drift 게이트 동작 | 충족 | `check:tech-log-contract`: "in sync: @tech-log/studio-contract@2.0.0 (ce2e748), 19 operations". 최종 fix wave에서 이 명령을 `config/ci/gates.json`의 FE-GATE-010과 `test:all`에 연결했다 — 그전까지는 손으로 칠 때만 실행돼 drift 게이트가 실질적으로 비어 있었다 |
| 4 | `StudioGateway` 전체 operation이 HTTP 어댑터로 구현·MSW 검증 | 충족 | `test:unit`/`test:integration`의 HTTP·MSW 계약 스위트 PASS (환경 요인 실패 1건 제외, 아래 참고) |
| 5 | WorkingCopy 저장이 Public Projection을 변경하지 않는다 | 충족 | `studio-publication-flow.test.tsx`, `public-document-screens.test.tsx` PASS |
| 6 | Validation/Preview/Publish가 version·dependency revision으로 묶인다 | 충족 | `studio-validation-preview.test.tsx` PASS |
| 7 | Publication Event/Snapshot 조회 가능, 과거 Snapshot 불변 | 충족 | `studio-publication-flow.test.tsx` PASS |
| 8 | Image/SVG 업로드 + Asset 기반 evidence 삽입 | 충족 | Task 10/11 Asset Picker·업로드 다이얼로그·Asset Library; `test:tech-log` 내 asset 관련 스위트 PASS |
| 9 | `READY` Asset만 Preview/Publish에 사용, `QUARANTINED` 미노출 | 충족 | Task 6/9가 구현; 관련 렌더러·게이트 테스트 PASS |
| 10 | 23개 오류 코드 + idempotency/version 충돌 구분 | 충족 | `error-classification.test.ts` 등 PASS |
| 11 | 프론트가 Backend 도메인 Aggregate를 복제하지 않는다 | 충족 | `check:architecture` PASS (415 modules, 전 import 해석, 12개 회귀 fixture PASS) |
| 12 | 런타임 스위치 `MOCK`/`HTTP` 전환, 기본 `MOCK`에서 기존 parity 스위트 전부 통과 | 충족 | 위 1·2 근거 + 수동 확인: `TECH_LOG_STUDIO_SOURCE=HTTP`에서 Backend 부재 시 Studio 쉘은 정상 렌더되고 패널은 "작업 흐름을 불러오지 못했습니다" 인라인 오류로 우아하게 저하됨(백지·미처리 예외 없음) |
명시적 비완료 항목 — 계획대로 이번 사이클에 포함되지 않는다:
- **실행 중 Backend와의 실응답 대조**: 이 환경에 Backend가 없다. Task 12 수동 확인은 "Backend 부재 시 우아한 오류 상태"까지만 검증했고, 실제 Backend 응답과의 대조는 Backend Studio 구현 완료 후 별도로 수행한다.
- **Public 조회의 HTTP 전환**: 범위에서 명시적으로 제외됐다(§범위 "제외 — Public 조회의 HTTP 전환"). `adapters/static/public-query.ts`는 이번 사이클에서 손대지 않았고, `public-v1.yaml` 기준 별도 spec/plan 사이클로 수행한다.
- **성공 payload의 런타임 계약 검증**: `tech-log-studio-contract-contribution.ts`의 18개 operation은 전부 `passthrough`(`z.unknown()`) `inputValidator`/`outputValidator`를 쓴다. 의도된 선택이고 코드에도 주석으로 남아 있다 — canonical 계약이 payload를 소유하고 `generated.ts`가 컴파일 시점 계약이며, 런타임 재검증은 계약 갱신 때마다 두 곳을 고치게 만든다. `problemValidator`는 그대로 엄격하다(23개 코드 enum + 필드 제약). **결과적으로 성공 응답의 shape 불일치로는 `CONTRACT_VIOLATION`이 발생할 수 없다.** 이 한계는 바로 위 "실행 중 Backend와의 실응답 대조 없음"과 같은 종류의 위험이다: Backend가 없으므로 MSW는 테스트 작성자가 적은 것을 그대로 돌려주고, 그것을 canonical 스키마와 대조하는 주체가 없다. 즉 shape 회귀를 잡을 수 있는 층이 지금은 컴파일 타임 한 겹뿐이다. Backend 대조 사이클에서 (a) 실서버 응답 대조로 대체할지 (b) canonical에서 생성한 런타임 스키마로 `outputValidator`를 채울지 함께 판단한다.
완료 조건 자체는 아니지만, 게이트 실행 중 확인된 사전 존재(pre-existing) 또는 환경적(environmental) 이슈:
- `tests/unit/ci-artifact-contract.test.ts` 16개 테스트가 `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted` 샌드박스 제약으로 실패한다. 브랜치 분기점 `9e5fbd1`에서도 동일하게 재현되는 환경 문제이며 이번 작업과 무관하다.
- `test:coverage`는 위 환경 실패 때문에 vitest가 non-zero로 종료해 `check-risk-coverage.ts`까지 도달하지 못한다(vitest 기본값 `coverage.reportOnFailure: false`). 그 파일만 제외한 진단 실행에서는 `src/application/policies/compatibility.ts`(re-export전용, 계측 가능한 statement 0개)와 `reference-http-gateway.ts`(statements 86.95%/branches 85%, 임계값 90%) 2건이 걸리는데, 둘 다 병합 지점(`9e5fbd1`) 이후 이 브랜치가 건드리지 않은 파일이다.
- `test:visual`(chromium): **최종 fix wave에서 130개 중 13개 실패로 정리했고, 남은 13개는 전부 merge-base `9e5fbd1`에서 동일하게 실패한다** — 이 브랜치가 만든 시각 회귀는 0건이다.
- **판정 근거는 추론이 아니라 실측이다.** merge-base `9e5fbd1`을 detached checkout해 `test:visual`을 그대로 실행했고(13 failed / 117 passed), 두 실행이 남긴 `-actual.png`를 SHA-256으로 대조했다. 13개는 merge-base와 HEAD의 실제 렌더 결과가 **바이트 동일**했다 — 즉 이 브랜치의 코드와 무관하다.
- **물려받은 13개(갱신하지 않음)**: `TECH_LOG_CASE` 1440, `known Public fixture /cases/collection-fetch-join-pagination` 1440, `TECH_LOG_STUDIO_DOCUMENT_PREVIEW` 1440, `TECH_LOG_STUDIO_PUBLICATION_PREVIEW` 1440, `Studio current-preview` 1440, `Studio publication-snapshot` 1440, `Studio immediate preview` 1440(이상 7개는 높이가 **줄었다**), `TECH_LOG_STUDIO_DOCUMENTS` 360/1440, `Studio document-list` 1440, `TECH_LOG_STUDIO_DOCUMENT_NEW` 1440, `Studio new-document` 1440(이상 5개는 크기 변화 없는 픽셀 차이), `TECH_LOG_STUDIO_DOCUMENT_NEW` 360(360×1000 → 360×1130). 높이가 줄어든 7개의 원인은 `main`에 이미 있고 merge-base `9e5fbd1`의 조상인 `79e9aa8`("fix: align TechLog article content widths")로, `.evidence-figure`의 CSS 폭을 `min(61rem, calc(100% + 15rem))`(≈912px)에서 `min(var(--body-copy), 100%)`(672px, 비율 73.6%)로 바꿨다. golden PNG는 `79e9aa8`보다 앞선 `3a7c5de`에서 마지막으로 기록됐다. 갱신은 이 브랜치가 아니라 `main``79e9aa8`에 대해 기록해야 한다.
- **이전 기록의 정정 2건**: (1) "나머지 5개는 크기 변화 없는 픽셀 차이(추가 조사하지 않음)"로 남겨뒀던 항목은 조사 결과 **전부 물려받은 것**이다. (2) `TECH_LOG_STUDIO_DOCUMENT_NEW` 360(360×1000 → 360×1130)을 Asset Picker 때문이라고 원인 B로 분류했었는데, merge-base에서 **동일한 픽셀 수(28,413)로 동일하게 실패**한다 — 물려받은 것이다. 따라서 이 브랜치가 만든 실패는 6개가 아니라 5개다.
- **이 브랜치가 만들어 갱신한 5개**: `TECH_LOG_STUDIO_DOCUMENT_EDIT` 360(360×3313 → 360×3627), `TECH_LOG_STUDIO_DOCUMENT_EDIT` 1440·`Studio case-editor` 1440·`Studio conflict-editor` 1440·`Studio dirty-leave dialog` 1440(전부 1440×2706 → 1440×2999). 전부 Case 편집기 화면이고, 늘어난 293px 영역은 Task 10이 추가한 "EVIDENCE / 본문에 Asset 삽입" 패널(업로드 종류 select + `Asset 업로드` 버튼 + Picker 빈 상태)임을 갱신본에서 직접 확인했다. `--update-snapshots`는 이 5개 테스트에만 `--grep`으로 한정해 실행했다 — 일괄 갱신은 물려받은 13개까지 조용히 흡수해 이 구분을 없애기 때문이다.
- `test:e2e`/`test:a11y`는 chromium에서 전부 통과하고, firefox/webkit 실패는 이 환경의 브라우저 의존성 문제다(firefox: Pretendard 폰트의 "name records not sorted" 경고를 strict 콘솔 검사가 실패로 잡음; webkit: 호스트에 필요한 시스템 라이브러리 없음 — `playwright install-deps` 필요).
전체 명령·원문 출력은 `.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-12-report.md`에 기록했다.
## 목적
현재 TechLog 프론트엔드는 Studio를 세션 수명 `MockStudioGateway`로, Public을 정적 동기 catalog로 구동한다. 이 설계는 다음을 달성한다.
1. Studio HTTP 계약을 canonical `studio-v1.yaml` 단일 출처에서 생성하고, 두 계약이 다시 갈라지지 못하게 빌드로 막는다.
2. `StudioGateway`의 모든 operation을 canonical 계약 기준 HTTP 어댑터로 구현한다.
3. Backend에 이미 존재하는 Asset/Image/SVG capability를 프론트 편집 흐름에 연결한다.
4. 위 전부를 실행 중인 Backend 없이 완료하고, Backend가 완성되면 런타임 스위치만으로 대조할 수 있게 한다.
현재 Public UI 라우트·화면 구성과 Studio의 `작업본 → 편집 → 저장 → 검증 → Public Preview → 게시/재게시 → 게시 기록/Snapshot` 흐름은 변경하지 않는다.
## Source of Truth 우선순위
| 영역 | Source of Truth |
|---|---|
| Public 화면·라우트·사용 흐름 | 현재 `tech-log-frontend` |
| Studio 화면 구조·사용 흐름 | 현재 `tech-log-frontend` |
| Studio HTTP 계약 | `studio-v1.yaml` (단일 canonical) |
| Asset lifecycle·불변 조건 | `studio-v1.yaml` |
| 전송·오류·재시도·관측 규약 | 현재 프론트 플랫폼 (`src/contracts/external-contract-runtime.ts`) |
UI는 Backend 도메인 구조를 그대로 노출하지 않는다. 프론트의 `WorkingCopy`는 Backend Aggregate가 아니라 **편집 계약**이다.
## 확인된 사실과 정정
착수 전 검증에서 원본 요구 문서 및 설계 패키지 README와 실제 코드가 어긋나는 지점을 확인했다. 아래는 구현 기준으로 채택하는 정정이다.
### 이미 충족된 항목
- 설계 패키지 README는 프론트 격차로 "`RecordKind``PROJECT_DECISION` 없음"을 든다. **이미 충족돼 있다**`contracts/studio/studio-api.openapi.yaml:236`, `contracts/studio/generated.ts:200`. README가 지칭하는 대상은 구 `techlog-studio-frontend` 저장소다.
- `application/ports/studio-gateway.ts`의 operation 집합·필터·`IdempotentOptions`는 원본 요구 §4와 이미 일치한다. 포트 재설계가 아니라 구현체 교체가 필요하다.
### 실재하는 격차
- 프론트 계약에 `X-CSRF-TOKEN` 선언이 없다 (canonical 6회 참조, 프론트 0회). canonical은 모든 mutating operation에 CSRF를 요구한다.
- 프론트 계약의 `Idempotency-Key` 선언이 1회로, canonical(2회, 공용 parameter)과 구조가 다르다.
- canonical에는 프론트 계약에 없는 operation이 6개 있다: `getStudioSession`, `listStudioAssets`, `uploadStudioAsset`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset`. canonical 총 operation은 **19개**(프론트 현재 13개).
- canonical 오류 코드는 **23개**로, 원본 요구 §10의 21개에 `IDEMPOTENCY_KEY_REUSED``WARNING_ACKNOWLEDGEMENT_REQUIRED`가 추가된다. 두 코드 모두 필요하다 — 전자는 §10이 요구하는 "version conflict와 idempotency replay 구분"의 실제 코드이고, 후자는 이미 존재하는 `warning-acknowledgements.tsx`가 처리해야 하는 거절 사유다. **23개 전부를 채택한다.**
- 원본 요구는 세션/CSRF를 다루지 않는다. canonical은 CSRF 토큰을 `getStudioSession`이 발급한다고 정한다. 아래 §"세션과 CSRF"에서 경계를 정한다.
### 플랫폼 제약
- `ContractContributionSource``TEMPLATE_FIXTURE`는 타입상 `fixtureId: "REFERENCE_FEATURE_V1"`로 닫혀 있다(`external-contract-runtime.ts:186-190`). TechLog는 `EXTERNAL_PACKAGE`만 사용할 수 있다.
- 플랫폼 계약 런타임은 `requestBody: "NONE" | "JSON"`만 허용하고, 그 외를 구성 시점에 거절한다(`external-contract-runtime.ts:145`, `:407`). 저수준 `client.ts`도 본문을 `JSON.stringify`로 고정한다(`client.ts:719`). **`multipart/form-data`를 표현할 수 없다.**
- 기존 `browser-transfer` capability는 presigned `GET`/`PUT`과 resumable part 업로드용이다(`PresignedTransferMethod = "GET" | "PUT"`). canonical의 `POST /assets` multipart MVP 계약에 그대로 맞지 않는다.
- 설계 패키지의 `MANIFEST.sha256`은 stale하다(yaml은 2026-08-17 갱신, manifest는 08-11 기준이며 `preview-v1.yaml`을 아직 나열한다). 계약 고정은 manifest가 아니라 위 §상태에 기록한 실측 digest를 기준으로 한다.
## 범위
### 포함
| | 범위 |
|---|---|
| **A. 계약 정합** | canonical `studio-v1.yaml` 도입, 타입 생성 자동화, digest 고정, drift 게이트 |
| **B. Studio 전송** | 계약 기여(JSON 18개 전체) + `StudioGateway` HTTP 어댑터(14개 소비) + 세션/CSRF + 오류 매핑 + 런타임 스위치 |
| **C. Asset** | `StudioAssetGateway` 포트, 5개 operation(JSON 4 + multipart 1), Asset Library/Picker/Upload UI, `/studio/assets` 라우트, evidence directive 연결, alt/decorative 규칙 정정 |
### 제외 — Public 조회의 HTTP 전환
원본 요구 §11은 `adapters/static/public-query.ts`를 HTTP로 교체하도록 나열한다. 이 사이클에서 제외하고 별도 사이클로 분리한다.
근거:
- `PublicContentQueries`는 전 메서드가 **동기**이고, 프레젠테이션 19개 파일 31개 호출 지점이 렌더 중 직접 호출한다. async 전환은 전 Public 화면에 loading/error/empty 상태를 도입하는 작업이다.
- 방금 완료한 UI 이식의 시각·접근성 parity 기준선을 광범위하게 흔든다.
- 실행 중인 Public Backend가 없어 지금 전환해도 검증할 대상이 없고 사용자 가치도 없다.
- canonical `public-v1.yaml`(1,765줄)은 준비돼 있으므로, Public API가 실제로 서비스될 때 자체 spec/plan 사이클로 수행한다.
이 제외는 범위 축소가 아니라 순서 결정이다. 완료 조건에서 해당 항목을 별도로 명시한다.
## 선택한 접근
### A. 계약 정합 — digest로 고정된 단일 출처
canonical yaml을 저장소에 vendor하고, 타입을 생성하고, **계약 기여의 `EXTERNAL_PACKAGE` provenance로 canonical revision에 암호학적으로 고정**한다.
`InstalledContractPackageIdentity`(`external-contract-runtime.ts:173`)는 이미 이 목적에 맞는 필드를 요구한다.
```text
packageId @tech-log/studio-contract
version 2.0.0 (canonical info.version, exact SemVer)
digest canonical studio-v1.yaml의 SHA-256
runtimeProtocolVersion 1
sourceRevision 설계 패키지 git revision
```
`digest`·`sourceRevision`의 실제 값은 이 문서가 아니라
`contracts/studio/canonical-source.json`에 기록한다(§상태 참고). 계약 기여가 그
파일을 직접 읽으므로, 문서에 값을 복제하면 갱신을 한쪽에서만 하다가 어긋난다 —
승인본이 실제로 그렇게 어긋났다.
`assertPackageIdentity`는 shape을 검증하므로 npm 레지스트리 없이 지금 사용할 수 있다. 실제 패키지 배포로 승격할 때 같은 필드를 그대로 채운다.
drift 방지는 저장소 관례(`generate:*` / `check:*`)를 따르는 스크립트 한 쌍으로 강제한다.
```text
generate:tech-log-contract canonical yaml → vendor 사본 + generated.ts + digest 기록
check:tech-log-contract 재생성 결과가 커밋 내용과 바이트 동일한지, digest가 계약 기여의
선언과 일치하는지 검증. 불일치 시 실패.
```
`openapi-typescript`를 devDependency로 고정한다. 현재 `generated.ts`는 이 도구로 만들어졌으나 도구도 스크립트도 저장소에 없어 재생성이 불가능하다 — 이것이 두 계약이 갈라진 근본 원인이다.
`X-CSRF-TOKEN`과 공용 `Idempotency-Key` parameter는 생성 결과에 자동 반영된다. 프론트 yaml을 손으로 고치지 않는다.
### B. Studio 전송 — 플랫폼 계약 런타임 사용
계약 기여는 서비스 패키지당 하나이므로, `features/tech-log/contracts/tech-log-studio-contract-contribution.ts` 한 파일에 canonical의 **JSON operation 18개 전부**(19개 중 multipart 업로드 제외)를 선언한다. Asset의 JSON operation 4개도 같은 기여에 속한다 — 같은 `studio-v1` 패키지이기 때문이다. 이 중 `StudioGateway`가 14개를, `StudioAssetGateway`가 4개를 소비한다.
선언 형식은 `reference-feature-contract-contribution.ts`가 확립한 것을 따른다: operation별 `inputValidator`/`outputValidator`/`problemValidator`, `acceptedStatuses`, `retrySemantics`, `commandRecovery`, `commandEffect`, `projectRequest`, 그리고 byte limit·deadline·retry budget·diagnostics 이름.
`retrySemantics`는 canonical의 안전성 구분을 그대로 반영한다. 조회는 `SAFE`, mutating operation은 `KEYED`이며 `commandRecovery.mode = "IDEMPOTENCY_REPLAY"`, retry budget은 0이다 — 발신된 KEYED 명령의 자동 재시도는 금지된다.
`StudioGateway` 구현체는 `contractOperations` executor 위에 얹고, 포트 시그니처는 변경하지 않는다. UI는 어댑터가 mock인지 HTTP인지 알지 못한다.
#### 세션과 CSRF
CSRF는 전송 관심사이며 UI 관심사가 아니다. 현재 Studio에는 인증 UI가 없고(이식 시 유보), 이 사이클에서도 추가하지 않는다.
따라서 `getStudioSession`을 포트로 노출하지 않는다. HTTP 어댑터 내부가 첫 mutating 요청 전에 세션을 조회해 CSRF 토큰을 캐시하고, `csrfHeaderName`으로 헤더를 붙인다. `401`/`403`은 기존 오류 경로로 흘려보낸다. `displayName`/`roles`는 이 사이클에서 소비하지 않는다.
이 결정으로 완료 조건 "현재 Studio 작업 흐름이 변경되지 않는다"가 유지된다.
#### 런타임 스위치
`config/runtime/*.json`에 스위치를 추가한다.
```text
TECH_LOG_STUDIO_SOURCE: "MOCK" | "HTTP"
local, development → MOCK (기본값)
staging, production → HTTP
```
`createTechLogFeatureInstalledInput`이 이 값으로 gateway factory를 고른다. mock은 삭제하지 않고 test fixture 겸 fallback으로 유지한다. Backend 완성 시 `local.json` 한 줄로 대조를 시작한다.
기본값을 `MOCK`으로 두는 이유는 현재 앱과 이식 parity 테스트가 그대로 통과해야 하기 때문이다.
### C. Asset — 포트 분리와 업로드 전송 경계
`StudioAssetGateway``StudioGateway`와 별도 포트로 둔다. 파일 전송과 JSON orchestration의 실패 모델이 다르고, 향후 presigned/resumable 교체가 이 포트 뒤에서 끝나야 한다.
```text
StudioAssetGateway
listAssets(query, options) GET /api/v1/studio/assets
uploadAsset(form, options) POST /api/v1/studio/assets (multipart)
getAsset(assetId, options) GET /api/v1/studio/assets/{assetId}
updateAssetMetadata(assetId, cmd, o) PUT /api/v1/studio/assets/{assetId}
deleteAsset(assetId, options) DELETE /api/v1/studio/assets/{assetId}
```
#### 업로드 전송
플랫폼 계약 런타임이 multipart를 표현할 수 없으므로, 업로드 한 operation만 전용 전송 seam으로 분리한다.
```text
StudioAssetUploadTransport (좁은 인터페이스: form + 헤더 → 결과)
└ fetch + FormData 구현체
런타임 config의 API_BASE_URL·타임아웃 재사용
CSRF·Idempotency-Key 헤더는 B와 동일 경로로 획득
오류는 B와 동일한 코드 매핑 테이블 사용
```
나머지 4개 JSON operation은 A/B와 같은 계약 런타임을 통과한다. 즉 계약 런타임을 우회하는 것은 **19개 중 1개**다.
이 경계를 명시적 seam으로 두는 이유는 §7.4의 교체 가능성 요구를 만족시키기 위해서다. 플랫폼에 `MULTIPART` 모드가 생기거나 presigned로 옮길 때 구현체 한 파일만 바뀐다. 플랫폼 파일(`external-contract-runtime.ts`, `client.ts`)은 template 동기화 대상이므로 이 사이클에서 수정하지 않는다.
결정 사유와 우회 범위는 `docs/reviews/adapters/`에 기록한다.
#### UI 접근점
Studio primary navigation을 Asset 중심 CMS로 되돌리지 않는다. 두 접근점을 둔다.
1. **Editor contextual Asset Picker** — 작성 흐름의 기본 진입점. 선택 시 evidence directive를 삽입한다.
2. **`/studio/assets` Asset Library** — 검색·메타데이터·사용처·정리용 보조 화면. navigation에는 secondary utility link로만 노출한다.
`/studio/assets``tech-log-route-contract.ts``layoutGroup: "STUDIO"`로 추가한다(현재 27개 → 28개). 라우트 registry 거버넌스 기준선을 함께 갱신한다.
#### Evidence Figure와 Asset 연결
현재 content format directive를 유지한다.
```text
:::evidence key="asset-key" alt="설명" caption="캡션" zoom="true"
```
변경점:
- 정적 `evidenceAssets` 레지스트리(`adapters/static/evidence-assets.ts`) 대신 Backend Asset의 `assetKey`를 사용한다. `assetKey`는 canonical에서 immutable이며 공개 이력 이후 재사용이 금지된 안정 key다.
- Asset Picker 선택 시 directive를 자동 삽입한다. 사용자가 raw object-storage URL을 Markdown에 직접 넣지 않게 한다.
- 렌더러는 API가 제공한 Asset descriptor를 resolver로 주입받는다. 렌더러 경계는 변경하지 않는다.
- `QUARANTINED` Asset은 Public/Preview에 렌더링하지 않는다.
- 사용자 업로드 SVG 원문을 `innerHTML`로 주입하지 않는다. 검증된 `publicPath``<img src>`로 렌더링한다.
정적 `evidenceAssets`는 기존 하드코딩 Case 화면의 parity 유지를 위해 fixture로 남기고, Asset 기반 경로와 공존시킨다.
#### alt / decorative 규칙 정정
현재 parser는 빈 alt를 syntax error로 차단한다.
```text
parse-case-content.ts:505
if (!attributes.alt) invalid(node, "evidence alt text is required");
```
Asset capability와 연결하면 이 위치에서는 판단할 수 없다 — 필요 여부가 Asset의 `decorative`에 달려 있다. 규칙을 옮긴다.
```text
parser 빈 alt를 문법 오류로 차단하지 않는다 (구조만 검증)
publish 검증 Asset decorative=false + 사용 위치 alt 비어 있음 → ERROR
Asset decorative=true → alt="" 허용
```
이는 content format의 **의미 변경**이므로 parser/serializer round-trip 픽스처와 기존 검증 테스트를 함께 갱신한다.
## 대상 아키텍처
```text
src/features/tech-log/
├── contracts/
│ ├── studio/
│ │ ├── studio-api.openapi.yaml canonical vendor 사본 (생성물, 수동 편집 금지)
│ │ ├── generated.ts 생성물
│ │ ├── contract.ts 타입 alias (Asset 계열 추가)
│ │ └── canonical-source.json digest·revision 기록
│ └── tech-log-studio-contract-contribution.ts JSON operation 18개 + EXTERNAL_PACKAGE provenance
├── application/ports/
│ ├── studio-gateway.ts 변경 없음
│ └── studio-asset-gateway.ts 신규
├── adapters/
│ ├── http/
│ │ ├── http-studio-gateway.ts contractOperations 위 구현
│ │ ├── http-studio-asset-gateway.ts JSON 4 + 업로드 위임
│ │ ├── asset-upload-transport.ts multipart seam
│ │ ├── studio-session-csrf.ts CSRF 토큰 획득·캐시
│ │ └── studio-error-mapping.ts 23 코드 → 플랫폼 FailureKind
│ ├── mock/ 유지 (fixture·fallback)
│ └── static/ 유지 (Public, 이 사이클 범위 외)
└── presentation/studio/
├── pages/assets-page.tsx 신규
└── components/
├── asset-library.tsx 신규
├── asset-picker.tsx 신규
└── asset-upload-dialog.tsx 신규
```
기존 유지 대상: `presentation/public/**`, `presentation/studio/pages/**`, `document-*`, `validation-*`, `public-preview-screen`, `publish-screen`, `publication-*`, `presentation/shared/public-render/**`.
## 오류·동시성 계약
23개 canonical 코드를 전부 처리한다. 매핑은 `studio-error-mapping.ts` 한 곳에 둔다.
```text
AUTHENTICATION_REQUIRED STUDIO_ACCESS_DENIED
DOCUMENT_NOT_FOUND VERSION_CONFLICT
REQUEST_VALIDATION_FAILED VALIDATION_FAILED
VALIDATION_STALE PREVIEW_NOT_FOUND
PREVIEW_STALE PREVIEW_EXPIRED
PUBLICATION_NOT_FOUND PUBLICATION_CONFLICT
PUBLICATION_EVENT_NOT_FOUND PUBLICATION_SNAPSHOT_NOT_FOUND
IDEMPOTENCY_KEY_REUSED WARNING_ACKNOWLEDGEMENT_REQUIRED
ASSET_NOT_FOUND ASSET_NOT_READY
ASSET_IN_USE ASSET_QUARANTINED
PAYLOAD_TOO_LARGE UNSUPPORTED_MEDIA_TYPE
STUDIO_UNAVAILABLE
```
규칙:
- 모든 mutation은 `Idempotency-Key`를 보낸다. 명령 하나당 새 key를 만들고, 그 명령의 안전한 재시도에만 같은 key를 재사용한다.
- `VERSION_CONFLICT`(낙관적 잠금 실패)와 `IDEMPOTENCY_KEY_REUSED`(같은 key·다른 요청)를 혼동하지 않는다. 전자는 사용자 데이터 손실 없는 충돌 화면으로, 후자는 클라이언트 결함으로 다룬다.
- `Idempotency-Replayed` 응답 헤더를 replay 판별에 사용한다.
- workflow 상태(`publicationStatus`, `hasUnpublishedChanges`, `nextAction`)는 서버가 계산한 값을 그대로 신뢰한다. 프론트에서 재계산하지 않는다.
- 업로드 상태는 `선택 실패 / 업로드 중 / 전송 실패 / READY / REJECTED / QUARANTINED / 크기 초과 / 미지원 형식`을 구분한다. 업로드 전송 성공과 서버 검증 성공을 분리한다.
- Asset은 `READY`일 때만 Preview/Publish에 사용한다.
## 테스트 전략
TDD로 진행한다. 각 단위는 red → green → 게이트 순서를 지킨다.
**계약(A)**
- `check:tech-log-contract`가 vendor 사본·`generated.ts`·digest 불일치를 잡는다 (의도적 변조로 red 확인).
- canonical 19개 operationId가 전부 덮이는지 parity 검증: 18개는 계약 기여에, `uploadStudioAsset`은 업로드 전송 seam에 존재해야 한다. 어느 쪽에도 없는 operationId가 있으면 실패한다.
- `EXTERNAL_PACKAGE` identity가 `assertPackageIdentity`를 통과하고 `contractSet`에 나타나는지 확인.
**전송(B)** — MSW로 canonical 응답·오류를 재현
- 불완전 draft 저장 성공, `expectedVersion` 충돌 → `VERSION_CONFLICT`.
- idempotency replay(`Idempotency-Replayed: true`)와 `IDEMPOTENCY_KEY_REUSED` 구분.
- workflow 전이: `INVALID → FIX_VALIDATION`, `VALID/WARNINGS → CREATE_PREVIEW`, `CURRENT → PUBLISH`, 게시 후 `NONE`, 편집 후 `VALIDATE` 복귀, expired preview 재생성.
- Publication: 최초 `PUBLISHED`, 재게시 `REPUBLISHED`, 취소 `UNPUBLISHED`, 과거 Snapshot 불변성.
- 23개 코드 전부의 UI 관측 가능한 처리.
- CSRF 토큰 획득 실패·만료 경로.
- 런타임 스위치: `MOCK`/`HTTP` 각각에서 gateway 종류가 선택되는지.
**Asset(C)**
- PNG/JPEG/WebP/SVG 업로드 성공, 미지원 형식 `UNSUPPORTED_MEDIA_TYPE`, 크기 초과 `PAYLOAD_TOO_LARGE`.
- `decorative=false` + 빈 alt → publish ERROR / `decorative=true` + `alt=""` 허용.
- `QUARANTINED` Asset의 Public·Preview 렌더링 차단.
- 사용 중 Asset hard delete 차단 → `ASSET_IN_USE`, `ARCHIVED` 전환 경로.
- Asset Picker가 evidence directive를 정확한 문법으로 삽입.
- parser가 빈 alt를 더 이상 syntax error로 차단하지 않음 + round-trip 픽스처 갱신.
**렌더러 불변**
동일 픽스처에 대해 `Instant Preview`, `Server Public Preview`, `Published Public`, `Publication Snapshot`의 semantic output이 동일해야 한다. Asset resolver도 같은 렌더러 경계로 주입한다.
**회귀**
기존 이식 parity 스위트(시각·접근성·라우트·아키텍처 경계)가 전부 통과해야 한다. 기본 스위치가 `MOCK`이므로 이 스위트는 영향받지 않아야 한다.
## 위험과 완화
| 위험 | 완화 |
|---|---|
| 실행 중 Backend 없음 → 실응답 미검증 | canonical 계약 기준 MSW 검증. 스위치로 대조 지점을 남긴다. 완료 조건에 "실서버 대조 미포함"을 명시한다. |
| canonical yaml이 계속 변경 중 (오늘도 수정됨) | digest·revision을 커밋에 고정하고 drift 게이트로 감지. canonical 갱신은 의도적 재생성 커밋으로만 반영. |
| 계약 런타임 우회(업로드 1개)가 거버넌스 위반으로 보일 수 있음 | 좁은 seam으로 격리, 사유·범위를 adapter review 문서에 기록, 나머지 18개는 런타임 통과. |
| content format 의미 변경(alt)이 기존 픽스처를 깨뜨림 | parser 변경과 픽스처·검증 갱신을 한 단위로 묶어 red→green으로 수행. |
| 라우트 1개 추가가 registry 기준선을 깨뜨림 | 거버넌스 기준선 갱신을 같은 단위에 포함. |
| `openapi-typescript` 도입이 생성물 diff를 크게 만듦 | 첫 생성 결과를 별도 커밋으로 분리해 리뷰 가능하게 한다. |
## 완료 조건
1. 현재 Public UI·라우트가 변경되지 않는다.
2. 현재 Studio 작업 흐름이 변경되지 않는다.
3. Studio 계약이 canonical `studio-v1.yaml`에서 생성되고, digest 고정과 drift 게이트가 동작한다.
4. `StudioGateway`의 모든 operation이 HTTP 어댑터로 구현되고 MSW 계약 테스트로 검증된다.
5. WorkingCopy 저장이 Public Projection을 변경하지 않는다.
6. Validation/Preview/Publish가 version과 dependency revision으로 묶인다.
7. Publication Event와 Snapshot을 현재 UI에서 조회할 수 있고 과거 Snapshot이 불변이다.
8. Image/SVG를 업로드하고 Case content에 Asset 기반 evidence로 삽입할 수 있다.
9. Asset은 `READY`일 때만 Preview/Publish에 사용된다. `QUARANTINED`는 렌더링되지 않는다.
10. 23개 오류 코드와 idempotency/version 충돌 구분이 처리된다.
11. 프론트가 Backend 도메인 Aggregate를 복제하지 않는다.
12. 런타임 스위치로 `MOCK`/`HTTP`를 전환할 수 있고, 기본 `MOCK`에서 기존 parity 스위트가 전부 통과한다.
명시적 비완료 항목:
- 실행 중 Backend와의 실응답 대조. Backend Studio 구현 완료 후 별도로 수행한다.
- Public 조회의 HTTP 전환. `public-v1.yaml` 기준 별도 spec/plan 사이클로 수행한다.
@@ -0,0 +1,54 @@
# TechLog 본문·콘텐츠 자료 너비 정렬 설계
## 상태
- 승인일: 2026-08-17
- 우선순위: Decision 작성 기능보다 먼저 적용
- 결정: 코드 블록, 데이터 표, SVG·이미지 evidence를 본문과 같은 `42rem` 너비에 맞춘다.
## 문제
Public 문서 본문은 `--body-copy: 42rem`이지만 코드 블록과 데이터 표는 최대 `58rem`, evidence figure는 최대 `61rem`으로 가운데 돌출된다. 이 때문에 본문 문장과 자료의 좌우 경계가 달라지고 문서를 읽을 때 시선축이 흔들린다.
## 선택한 접근
본문의 읽기 너비는 유지하고 자료 쪽을 본문 너비에 맞춘다.
- `.code-block`, `.data-table-wrap`, `.evidence-figure`의 기본 너비를 `min(var(--body-copy), 100%)`로 통일한다.
- 세 요소의 좌우 중앙 정렬은 일반 `margin-inline: auto`로 표현하고, 폭을 넓히기 위한 `50%` 이동과 `translateX`를 제거한다.
- 긴 코드는 기존처럼 `pre` 내부에서 가로 스크롤한다.
- 넓은 표는 기존처럼 wrapper 내부에서 가로 스크롤한다.
- SVG·이미지는 비율을 유지해 컨테이너 너비에 맞추고 기존 확대 dialog를 유지한다.
- 작은 화면에서는 세 자료가 계속 `width: 100%`를 사용한다.
본문 자체를 `58rem` 이상으로 넓히는 접근은 긴 문장의 가독성을 바꾸므로 사용하지 않는다. 코드·이미지만 계속 돌출시키는 접근도 이번 문제를 유지하므로 사용하지 않는다.
## 범위
다음 표면에 동일하게 적용한다.
- Case 공개 문서
- Studio 즉시 미리보기
- 저장·검증된 Public preview
- 게시 snapshot preview
- 공유 Public renderer를 사용하는 모든 코드, 표, evidence figure
다음은 이번 변경에 포함하지 않는다.
- 본문 글꼴, 행간, `--body-copy` 값 변경
- 이미지 업로드와 evidence catalog 자동 등록
- TOC 위치와 문서 전체 shell 너비 변경
- Decision 작성 기능
## 반응형·접근성
- 데스크톱에서 본문과 자료의 좌우 경계가 같아야 한다.
- 모바일에서는 viewport를 넘지 않아야 한다.
- 코드와 표의 가로 스크롤 가능성을 유지한다.
- evidence 확대 버튼과 keyboard focus 동작을 유지한다.
## 검증
먼저 style contract에 본문, 코드, 표, evidence figure의 계산된 너비 규칙이 모두 `min(var(--body-copy), 100%)`인지 확인하는 실패 테스트를 추가한다. 그 후 최소 CSS 변경으로 통과시킨다.
공유 renderer 회귀 테스트로 코드의 `pre` overflow, 표 wrapper overflow, evidence 확대 control이 그대로 존재하는지 확인한다. 마지막으로 Case 문서를 데스크톱과 모바일에서 확인해 자료가 본문 경계와 정렬되고 viewport overflow가 없는지 검증한다.
+194
View File
@@ -382,6 +382,194 @@ const browserDataBoundaryPlugin = {
},
};
/**
* A DOM element rendered by React carries `__reactFiber$*` / `__reactProps$*`
* as *own enumerable* properties. `node:assert` builds its `AssertionError`
* eagerly, running `util.inspect` over both operands with `depth: 1000`,
* `getters: true` and `maxArrayLength: Infinity`; the fiber graph re-expands
* once per traversal path, so inspecting one rendered element allocates
* gigabytes and the worker dies before any `AssertionError` is ever thrown.
* A genuine regression then reports as an OOM or an opaque timeout instead of
* a failed assertion. Measured on this repo's Asset Library heading:
* depth 6 = 1.3MB, depth 8 = 7.8MB, depth 10 = 36MB, depth 12 = 135MB.
*
* Equality assertions only inspect their operands on failure, so an unsafe
* comparison stays invisible while green and detonates the day the behaviour
* it guards regresses -- which is exactly when the diagnosis is needed.
*
* `expect` is not affected: vitest prints and diffs through pretty-format's
* DOM plugin, which reads tag/attributes/children and never touches the fiber.
* So the safe form is always an `expect` matcher -- `toHaveFocus()`,
* `not.toBeInTheDocument()`, `toBe(element)` -- and this rule only forbids
* handing a DOM element to `node:assert`.
*/
const TESTING_LIBRARY_QUERY =
/^(get|query|find)(All)?By(Role|Text|LabelText|PlaceholderText|AltText|Title|DisplayValue|TestId)$/u;
const domQueryMethods = new Set([
"querySelector",
"querySelectorAll",
"getElementById",
"closest",
]);
const domElementProperties = new Set([
"activeElement",
"parentElement",
"firstElementChild",
"lastElementChild",
"nextElementSibling",
"previousElementSibling",
"offsetParent",
]);
// Fail when the operands *differ*, so on failure at least one element is
// still there to be inspected.
const positiveAssertEqualities = new Set([
"equal",
"strictEqual",
"deepEqual",
"deepStrictEqual",
]);
// Fail when the operands *match*. `assert.notEqual(element, null)` can only
// fail with `null` on both sides, so a nullish literal operand makes these
// safe; anything else leaves an element to inspect.
const negativeAssertEqualities = new Set([
"notEqual",
"notStrictEqual",
"notDeepEqual",
"notDeepStrictEqual",
]);
const noElementOperandEqualityRule: Rule.RuleModule = {
meta: {
type: "problem",
schema: [],
messages: {
unbounded:
"node:assert inspects both operands at depth 1000 to build its failure message, and a React-rendered element's __reactFiber$* graph exhausts the worker heap there, so the regression reports as an OOM instead of an assertion. Use an expect matcher instead -- expect(el).toHaveFocus(), expect(el).not.toBeInTheDocument(), expect(actual).toBe(expected) -- which prints DOM nodes through pretty-format's DOM plugin.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const unwrap = (input: any): any => {
let node = input;
while (
node &&
[
"AwaitExpression",
"ChainExpression",
"TSAsExpression",
"TSNonNullExpression",
"TSSatisfiesExpression",
"TSTypeAssertion",
].includes(node.type)
) {
node = node.type === "AwaitExpression" ? node.argument : node.expression;
}
return node;
};
const memberName = (node: any): string | null => {
if (!node.computed && node.property?.type === "Identifier") {
return node.property.name;
}
if (
node.computed &&
(node.property?.type === "Literal" ||
node.property?.type === "StringLiteral") &&
typeof node.property.value === "string"
) {
return node.property.value;
}
return null;
};
const resolveInit = (node: any): any => {
const scope = sourceCode.getScope(node);
let current: any = scope;
while (current) {
const variable = current.variables.find(
(entry: any) => entry.name === node.name,
);
if (variable) {
const definition = variable.defs.at(-1);
return definition?.node?.type === "VariableDeclarator"
? definition.node.init
: null;
}
current = current.upper;
}
return null;
};
const isElementValued = (input: any, seen = new Set<any>()): boolean => {
const node = unwrap(input);
if (!node || seen.has(node)) return false;
seen.add(node);
if (node.type === "MemberExpression") {
const name = memberName(node);
return name !== null && domElementProperties.has(name);
}
if (node.type === "CallExpression") {
const callee = unwrap(node.callee);
if (callee?.type !== "MemberExpression") return false;
const name = memberName(callee);
return (
name !== null &&
(TESTING_LIBRARY_QUERY.test(name) || domQueryMethods.has(name))
);
}
if (node.type === "Identifier") {
return isElementValued(resolveInit(node), seen);
}
if (node.type === "ConditionalExpression") {
return (
isElementValued(node.consequent, seen) ||
isElementValued(node.alternate, seen)
);
}
return false;
};
const isNullish = (input: any): boolean => {
const node = unwrap(input);
if (!node) return false;
return (
(node.type === "Literal" && node.value === null) ||
(node.type === "Identifier" && node.name === "undefined")
);
};
return {
CallExpression(node: any) {
const callee = unwrap(node.callee);
if (callee?.type !== "MemberExpression") return;
const object = unwrap(callee.object);
if (object?.type !== "Identifier" || object.name !== "assert") return;
const name = memberName(callee);
if (name === null) return;
const positive = positiveAssertEqualities.has(name);
if (!positive && !negativeAssertEqualities.has(name)) return;
const operands = (node.arguments ?? []).slice(0, 2);
if (!positive && operands.some((argument: any) => isNullish(argument))) {
return;
}
const operand = operands.find((argument: any) =>
isElementValued(argument),
);
if (operand) context.report({ node: operand, messageId: "unbounded" });
},
};
},
};
const testAssertionBoundaryPlugin = {
rules: {
"no-element-operand-equality": noElementOperandEqualityRule,
},
};
const commonLanguageOptions = {
ecmaVersion: "latest",
sourceType: "module",
@@ -811,6 +999,12 @@ export default [
...globals.node,
},
},
plugins: {
"test-assertion-boundary": testAssertionBoundaryPlugin,
},
rules: {
"test-assertion-boundary/no-element-operand-equality": "error",
},
},
{
files: [`tests/support/browser/**/*.${sourceExtensions}`],
+18 -3
View File
@@ -11,14 +11,19 @@
"scripts": {
"dev": "vite",
"build": "node scripts/build-frontend.ts",
"build:profile": "node scripts/generate-runtime-config.ts",
"build:release-candidate": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security && corepack pnpm verify:release && node scripts/verify-supply-chain-artifacts.ts && node scripts/create-release-candidate.ts",
"preview": "vite preview",
"preview": "node dist/server.mjs",
"verify:tech-log-source-parity": "node tests/support/browser/verify-tech-log-source-parity.ts",
"lint": "eslint src scripts tests recipes .storybook vite.config.ts vitest.config.ts playwright*.config.ts --max-warnings=0",
"check:architecture": "node scripts/check-architecture.ts",
"check:design-system": "node scripts/check-design-system.ts",
"check:design-system:fixture": "node scripts/check-design-system.ts --fixture",
"check:i18n": "node scripts/check-i18n.ts",
"check:i18n:fixture": "node scripts/check-i18n.ts --fixture",
"check:adapter-inventory": "node scripts/check-adapter-inventory.ts",
"check:remediation-ledger": "node scripts/check-remediation-ledger.ts",
"check:release-admission": "node scripts/check-release-admission.ts",
"check:diagnostics": "node scripts/check-diagnostics.ts",
"check:diagnostics:fixture": "node scripts/check-diagnostics.ts --fixture",
"check:types": "corepack pnpm check:types:app && corepack pnpm check:types:node && corepack pnpm check:types:test && corepack pnpm check:types:recipes && corepack pnpm check:types:web-worker && corepack pnpm check:types:service-worker",
@@ -41,6 +46,7 @@
"check:types:fixture:i18n-key": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-message-key.ts",
"check:types:fixture:i18n-params": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-message-params.ts",
"check:types:fixture:diagnostics": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-diagnostics-port.ts",
"check:types:fixture:image-resolve-signal": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-image-cdn-resolve-signal.ts",
"test:runtime-schema": "vitest run tests/runtime-schema --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/runtime-schema.xml --passWithNoTests",
"test:unit": "vitest run tests/unit --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/unit.xml",
"test:component": "vitest run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml",
@@ -70,14 +76,17 @@
"test:browser-file-storage-removal": "node scripts/test-browser-file-storage-runtime-removal.ts",
"test:realtime-removal": "node scripts/test-realtime-runtime-removal.ts",
"test:reference-feature": "vitest run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests",
"test:tech-log": "vitest run tests/features/tech-log --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/tech-log.xml",
"check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts",
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts",
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature tests/features/tech-log --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts",
"check:coverage:fixture": "node scripts/check-risk-coverage.ts --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json",
"test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:recipes",
"test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm check:tech-log-contract && corepack pnpm test:tech-log && corepack pnpm test:recipes",
"verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts",
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
"generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts",
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
"generate:tech-log-contract": "node scripts/generate-tech-log-contract.ts",
"check:tech-log-contract": "node scripts/generate-tech-log-contract.ts --check",
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
"verify:local-evidence": "node scripts/verify-release-candidate.ts && node scripts/verify-release.ts && node scripts/verify-supply-chain-artifacts.ts && node scripts/verify-archived-local-evidence.ts && node scripts/verify-release-candidate.ts",
"verify:promotion": "node scripts/verify-exact-promotion-bundle.ts",
@@ -120,11 +129,17 @@
"check:types:service-worker": "tsc --project tsconfig.service-worker.json"
},
"dependencies": {
"@fontsource/ibm-plex-mono": "5.3.0",
"@tanstack/react-query": "5.101.4",
"lucide-react": "1.25.0",
"pretendard": "1.3.9",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-router-dom": "7.18.1",
"remark-directive": "4.0.0",
"remark-gfm": "4.0.1",
"remark-parse": "11.0.0",
"unified": "11.0.5",
"zod": "4.4.3"
},
"devDependencies": {
+7 -7
View File
@@ -9,35 +9,35 @@ export default defineConfig({
["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }],
],
use: {
baseURL: "http://127.0.0.1:4173",
baseURL: "http://127.0.0.1:4273",
colorScheme: "light",
locale: "ko-KR",
timezoneId: "Asia/Seoul",
trace: "retain-on-failure",
screenshot: "only-on-failure",
},
webServer: {
command:
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4173",
url: "http://127.0.0.1:4173",
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4273",
url: "http://127.0.0.1:4273",
reuseExistingServer: false,
},
projects: [
{
name: "chromium",
testIgnore: "**/compact-smoke.spec.ts",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
testIgnore: "**/compact-smoke.spec.ts",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
testIgnore: "**/compact-smoke.spec.ts",
use: { ...devices["Desktop Safari"] },
},
{
name: "chromium-compact",
testMatch: "**/compact-smoke.spec.ts",
testMatch: "**/tech-log-responsive.spec.ts",
use: {
...devices["Desktop Chrome"],
viewport: { width: 390, height: 844 },
+11 -4
View File
@@ -16,15 +16,17 @@ export default defineConfig({
toHaveScreenshot: {
animations: "disabled",
caret: "hide",
maxDiffPixelRatio: 0.002,
scale: "css",
maxDiffPixels: 0,
maxDiffPixelRatio: 0,
scale: "device",
},
},
use: {
...devices["Desktop Chrome"],
baseURL: "http://127.0.0.1:4174",
colorScheme: "light",
locale: "en-US",
locale: "ko-KR",
timezoneId: "Asia/Seoul",
trace: "retain-on-failure",
},
webServer: {
@@ -33,5 +35,10 @@ export default defineConfig({
url: "http://127.0.0.1:4174",
reuseExistingServer: false,
},
projects: [{ name: "chromium-visual" }],
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"], deviceScaleFactor: 1 },
},
],
});
+729
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -14,5 +14,8 @@
"WEB_WORKER": "DEFAULT",
"SERVICE_WORKER": "DEFAULT",
"OFFLINE_COMMANDS": "DEFAULT"
},
"FEATURE_OVERRIDES": {
"reference-feature": "DEFAULT"
}
}
+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 19.2727C22 20.779 20.779 22 19.2727 22H14.7273C13.221 22 12 20.779 12 19.2727V12H19.2727C20.779 12 22 13.221 22 14.7273V19.2727Z" fill="#68C4FF"/>
<path d="M20 2C21.1046 2 22 2.89543 22 4V7C22 8.10457 21.1046 9 20 9H17C15.8954 9 15 8.10457 15 7V4C15 2.89543 15.8954 2 17 2H20Z" fill="#0C79D8"/>
<path d="M7 15C8.10457 15 9 15.8954 9 17V20C9 21.1046 8.10457 22 7 22H4C2.89543 22 2 21.1046 2 20V17C2 15.8954 2.89543 15 4 15H7Z" fill="#0C79D8"/>
<path d="M12 12H4.72727C3.22104 12 2 10.779 2 9.27273V4.72727C2 3.22104 3.22104 2 4.72727 2H9.27273C10.779 2 12 3.22104 12 4.72727V12Z" fill="#2E9EFF"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

+57
View File
@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="420" viewBox="0 0 1080 420">
<title>Fetch Join과 Batch Fetch의 페이징 경계</title>
<desc>Fetch Join은 전체 조인 결과를 읽은 뒤 메모리에서 20개를 고른다. Batch Fetch는 부모 20개를 먼저 고른 뒤 해당 ID의 컬렉션만 조회한다.</desc>
<rect width="1080" height="420" rx="18" fill="#FCFCFB"/>
<style>
.label { font: 600 18px Pretendard, sans-serif; fill: #17181B; }
.text { font: 500 15px Pretendard, sans-serif; fill: #3F4249; }
.muted { font: 500 13px Pretendard, sans-serif; fill: #686B72; }
.box { fill: #F7F7F5; stroke: #D9DBDE; stroke-width: 1.5; }
.bad { fill: #FFF4E8; stroke: #D99A4E; stroke-width: 1.5; }
.good { fill: #ECF6F2; stroke: #4F927F; stroke-width: 1.5; }
.arrow { stroke: #A5A8AE; stroke-width: 2; fill: none; marker-end: url(#arrow); }
</style>
<defs>
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M0 0 L8 4 L0 8 Z" fill="#A5A8AE"/>
</marker>
</defs>
<text class="label" x="40" y="54">Collection Fetch Join</text>
<rect class="box" x="40" y="80" width="190" height="76" rx="10"/>
<text class="text" x="66" y="112">목록 + 컬렉션 JOIN</text>
<text class="muted" x="66" y="136">부모 × 자식 행</text>
<path class="arrow" d="M230 118 H286"/>
<rect class="bad" x="286" y="80" width="230" height="76" rx="10"/>
<text class="text" x="312" y="112">전체 Join 결과 로드</text>
<text class="muted" x="312" y="136">DB LIMIT 없음 · 1,961행</text>
<path class="arrow" d="M516 118 H572"/>
<rect class="box" x="572" y="80" width="220" height="76" rx="10"/>
<text class="text" x="598" y="112">부모 엔티티 복원</text>
<text class="muted" x="598" y="136">중복 부모 정리</text>
<path class="arrow" d="M792 118 H848"/>
<rect class="bad" x="848" y="80" width="192" height="76" rx="10"/>
<text class="text" x="874" y="112">메모리에서 20개</text>
<text class="muted" x="874" y="136">페이지 경계가 뒤에 있음</text>
<line x1="40" y1="210" x2="1040" y2="210" stroke="#E2E3E1"/>
<text class="label" x="40" y="258">Parent Paging + Batch Fetch</text>
<rect class="good" x="40" y="284" width="230" height="76" rx="10"/>
<text class="text" x="66" y="316">부모 목록 LIMIT 20</text>
<text class="muted" x="66" y="340">정렬과 페이지 경계 확정</text>
<path class="arrow" d="M270 322 H342"/>
<rect class="box" x="342" y="284" width="210" height="76" rx="10"/>
<text class="text" x="368" y="316">부모 ID 20개</text>
<text class="muted" x="368" y="340">현재 페이지 집합</text>
<path class="arrow" d="M552 322 H624"/>
<rect class="good" x="624" y="284" width="220" height="76" rx="10"/>
<text class="text" x="650" y="316">컬렉션 IN 조회</text>
<text class="muted" x="650" y="340">현재 부모만 로드</text>
<path class="arrow" d="M844 322 H900"/>
<rect class="box" x="900" y="284" width="140" height="76" rx="10"/>
<text class="text" x="926" y="316">화면 조립</text>
<text class="muted" x="926" y="340">비용 상한 설명 가능</text>
<text class="muted" x="40" y="396">관찰: 페이징이 적용되는 지점이 부모 조회 앞으로 이동한다.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+27 -10
View File
@@ -8,16 +8,33 @@
"releaseId": "local-release",
"builtAt": "1970-01-01T00:00:00.000Z",
"routeChunks": {
"route-home": "src/presentation/pages/home-page.tsx",
"route-examples-platform": "src/presentation/examples/platform-overview-page.tsx",
"route-examples-ui": "src/presentation/examples/ui-gallery-page.tsx",
"route-examples-states": "src/presentation/examples/state-gallery-page.tsx",
"route-examples-auth": "src/presentation/examples/auth-example-page.tsx",
"route-reference-resources": "src/features/reference-feature/presentation/reference-resource-page.tsx",
"route-reference-resource-detail": "src/features/reference-feature/presentation/reference-resource-detail-page.tsx",
"route-reference-resource-form": "src/features/reference-feature/presentation/reference-resource-form-page.tsx",
"route-reference-resource-status": "src/features/reference-feature/presentation/reference-resource-status-page.tsx",
"route-not-found": "src/presentation/pages/not-found-page.tsx"
"route-tech-log-home": "src/features/tech-log/presentation/public/pages/home-page.tsx",
"route-tech-log-explore": "src/features/tech-log/presentation/public/pages/explore-page.tsx",
"route-tech-log-explore-kind": "src/features/tech-log/presentation/public/pages/explore-kind-page.tsx",
"route-tech-log-case": "src/features/tech-log/presentation/public/pages/case-page.tsx",
"route-tech-log-reference": "src/features/tech-log/presentation/public/pages/reference-page.tsx",
"route-tech-log-question": "src/features/tech-log/presentation/public/pages/question-page.tsx",
"route-tech-log-topic": "src/features/tech-log/presentation/public/pages/topic-page.tsx",
"route-tech-log-projects": "src/features/tech-log/presentation/public/pages/projects-page.tsx",
"route-tech-log-project": "src/features/tech-log/presentation/public/pages/project-overview-page.tsx",
"route-tech-log-project-records": "src/features/tech-log/presentation/public/pages/project-records-page.tsx",
"route-tech-log-project-decisions": "src/features/tech-log/presentation/public/pages/project-decisions-page.tsx",
"route-tech-log-project-activity": "src/features/tech-log/presentation/public/pages/project-activity-page.tsx",
"route-tech-log-releases": "src/features/tech-log/presentation/public/pages/releases-page.tsx",
"route-tech-log-release": "src/features/tech-log/presentation/public/pages/release-page.tsx",
"route-tech-log-profile": "src/features/tech-log/presentation/public/pages/profile-page.tsx",
"route-tech-log-search": "src/features/tech-log/presentation/public/pages/search-page.tsx",
"route-tech-log-studio-home": "src/features/tech-log/presentation/studio/pages/studio-home-page.tsx",
"route-tech-log-studio-documents": "src/features/tech-log/presentation/studio/pages/documents-page.tsx",
"route-tech-log-studio-document-new": "src/features/tech-log/presentation/studio/pages/new-document-page.tsx",
"route-tech-log-studio-document-edit": "src/features/tech-log/presentation/studio/pages/document-edit-page.tsx",
"route-tech-log-studio-document-validation": "src/features/tech-log/presentation/studio/pages/document-validation-page.tsx",
"route-tech-log-studio-document-preview": "src/features/tech-log/presentation/studio/pages/document-preview-page.tsx",
"route-tech-log-studio-document-publish": "src/features/tech-log/presentation/studio/pages/document-publish-page.tsx",
"route-tech-log-studio-publications": "src/features/tech-log/presentation/studio/pages/publications-page.tsx",
"route-tech-log-studio-publication-preview": "src/features/tech-log/presentation/studio/pages/publication-preview-page.tsx",
"route-tech-log-studio-not-found": "src/features/tech-log/presentation/studio/pages/studio-not-found-page.tsx",
"route-not-found": "src/features/tech-log/presentation/public/pages/public-not-found-page.tsx"
},
"contractSet": {
"setAlgorithm": "CA_CONTRACT_SET_V1",
+21 -7
View File
@@ -13,12 +13,20 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim
* 1. clean dist and .generated/frontend-runtime
* 2. generate contractSet and build-info source
* 3. Vite app build (emptyOutDir = true)
* 4. scan app dist and generate the static asset source
* 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
* 6. generate Release Manifest V2 and the build manifest
* 4. materialize dist/config.json from the declared APP_PROFILE
* 5. scan app dist and generate the static asset source
* 6. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
* 7. generate the self-contained TechLog production serving boundary
* 8. generate Release Manifest V2 and the build manifest
*
* Steps 4 and 5 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
* Steps 5 and 6 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
* and `null`: those modes never run an active worker build.
*
* Step 4 has to follow the Vite build and precede the asset scan. Vite copies
* `public/` verbatim, so without it every build including a production one
* ships the local runtime document; and the Service Worker hashes the emitted
* `config.json`, so the profile must be in place before that inventory is
* taken.
*/
const selection = INSTALLED_RUNTIME_CAPABILITIES.serviceWorker;
@@ -45,10 +53,13 @@ run("node", ["scripts/generate-contract-set.ts"]);
// 3. app build
run("npx", ["vite", "build"]);
// 4. runtime config for the declared profile
run("node", ["scripts/generate-runtime-config.ts"]);
if (buildsActiveWorker) {
// 4. hashed asset inventory
// 5. hashed asset inventory
run("node", ["scripts/generate-service-worker-assets.ts", "dist"]);
// 5. service worker build
// 6. service worker build
run("npx", ["vite", "build", "--config", "vite.service-worker.config.ts"]);
} else {
process.stdout.write(
@@ -56,5 +67,8 @@ if (buildsActiveWorker) {
);
}
// 6. release + build manifest
// 7. production serving boundary
run("node", ["scripts/generate-tech-log-serving-artifact.ts"]);
// 8. release + build manifest
run("node", ["scripts/generate-build-manifest.ts"]);
+202
View File
@@ -0,0 +1,202 @@
import { readFile } from "node:fs/promises";
import { readFileSync } from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { CACHEABLE_ASSET_CONTENT_TYPES } from "../src/contracts/service-worker-static-manifest.ts";
/**
* GOV-01 / SW-RR-03. Structural gates for facts that a hand-maintained document
* cannot keep true.
*
* The adapter review inventory claimed 118/118 while the tree held 119 files,
* so a whole adapter was outside every review's coverage without anything
* failing. And the Service Worker asset generator and the shared manifest
* decoder each carried their own extension table, so a build could emit an
* asset the runtime contract then refused. Both are now equalities this script
* checks rather than numbers someone has to remember to update.
*/
const INVENTORY_PATH = "docs/reviews/adapters/INVENTORY.md";
const GENERATOR_PATH = "scripts/generate-service-worker-assets.ts";
function trackedAdapterFiles(): readonly string[] {
const listed = spawnSync("git", ["ls-files", "src/adapters"], {
encoding: "utf8",
});
if (listed.status !== 0) {
throw new Error(`git ls-files failed: ${listed.stderr}`);
}
return listed.stdout.split("\n").filter(Boolean).sort();
}
function inventoryRows(markdown: string): readonly string[] {
const rows: string[] = [];
for (const line of markdown.split("\n")) {
const match = /^\|\s*\d+\s*\|\s*`([^`]+)`\s*\|/u.exec(line);
if (match?.[1]) rows.push(match[1]);
}
return rows;
}
function reportDifference(
label: string,
expected: readonly string[],
actual: readonly string[],
): readonly string[] {
const missing = expected.filter((value) => !actual.includes(value));
const extra = actual.filter((value) => !expected.includes(value));
const problems: string[] = [];
for (const value of missing) problems.push(`${label}: missing ${value}`);
for (const value of extra) problems.push(`${label}: unexpected ${value}`);
return problems;
}
async function main(): Promise<void> {
const problems: string[] = [];
const tracked = trackedAdapterFiles();
const markdown = await readFile(INVENTORY_PATH, "utf8");
const listed = inventoryRows(markdown);
problems.push(...reportDifference("adapter inventory", tracked, listed));
if (listed.length !== new Set(listed).size) {
problems.push("adapter inventory: duplicate row");
}
const total = /: \*\*(\d+)\/(\d+)\*\*/u.exec(markdown);
if (
!total ||
Number(total[1]) !== tracked.length ||
Number(total[2]) !== tracked.length
) {
problems.push(
`adapter inventory: total does not equal ${tracked.length} tracked files`,
);
}
// SW-RR-03. The generator must read the shared table rather than declare one.
const generator = await readFile(GENERATOR_PATH, "utf8");
if (!generator.includes("CACHEABLE_ASSET_CONTENT_TYPES")) {
problems.push(
"service worker assets: generator does not use the shared extension table",
);
}
if (/const CACHEABLE_EXTENSIONS[^=]*=\s*Object\.freeze\(\{/u.test(generator)) {
problems.push(
"service worker assets: generator declares its own extension table",
);
}
for (const [extension, contentType] of Object.entries(
CACHEABLE_ASSET_CONTENT_TYPES,
)) {
if (!extension.startsWith(".") || contentType.length === 0) {
problems.push(`service worker assets: invalid table row ${extension}`);
}
}
// A fixture that links the repository's node_modules with a single directory
// symlink is destructive: pnpm running inside that fixture purges the modules
// directory it does not recognise, follows the link, and deletes the real
// dependencies mid-run. `linkFixtureNodeModules` is the only sanctioned form.
const sources = spawnSync(
"git",
["grep", "-n", "-e", 'symlink(', "--", "scripts", "tests"],
{ encoding: "utf8" },
);
if (sources.status === 0) {
for (const line of sources.stdout.split("\n").filter(Boolean)) {
if (!line.includes("node_modules")) continue;
if (line.startsWith("scripts/lib/fixture-node-modules.ts:")) continue;
problems.push(
`fixture node_modules: use linkFixtureNodeModules instead — ${line}`,
);
}
}
const linkedFixtures = spawnSync(
"git",
["grep", "-l", "linkFixtureNodeModules", "--", "scripts", "tests"],
{ encoding: "utf8" },
);
if (
linkedFixtures.status !== 0 ||
linkedFixtures.stdout.split("\n").filter(Boolean).length < 2
) {
problems.push(
"fixture node_modules: the shared linker has no callers, so it is not the sanctioned path",
);
}
// TR-RR-05 / GOV-04. Every consumer the re-review named must use the shared
// primitive, not merely one file somewhere. Checking `importers.length > 0`
// let an unrelated production import satisfy the gate while Image and
// Resumable kept their own diverging copies of the same mechanics — which is
// exactly how the four hand-written versions drifted apart in the first place.
const REQUIRED_ABORT_CONSUMERS: readonly string[] = [
"src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts",
"src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts",
"src/adapters/browser-transfer/image-cdn/browser-image-probe.ts",
"src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts",
];
const primitiveImporters = spawnSync(
"git",
["grep", "-l", "platform/abortable-operation.ts", "--", "src"],
{ encoding: "utf8" },
);
const importers = (
primitiveImporters.status === 0 ? primitiveImporters.stdout : ""
)
.split("\n")
.filter(Boolean)
.filter((file) => !file.endsWith("platform/abortable-operation.ts"))
.sort();
const importerSet = new Set(importers);
const missingConsumers = REQUIRED_ABORT_CONSUMERS.filter(
(consumer) => !importerSet.has(consumer),
);
if (missingConsumers.length > 0) {
problems.push(
`abortable-operation: required consumers do not import the shared primitive: ${missingConsumers.join(
", ",
)}`,
);
}
// The importer must reach the primitive by a specifier that resolves to the
// primitive itself, so a same-named local helper cannot satisfy the gate.
const PRIMITIVE_PATH = path.resolve(
"src/adapters/platform/abortable-operation.ts",
);
for (const consumer of REQUIRED_ABORT_CONSUMERS) {
if (!importerSet.has(consumer)) continue;
const source = readFileSync(consumer, "utf8");
const specifiers = [
...source.matchAll(/from\s+"([^"]*platform\/abortable-operation\.ts)"/gu),
].map((match) => match[1] ?? "");
const resolved = specifiers.some(
(specifier) =>
path.resolve(path.dirname(consumer), specifier) === PRIMITIVE_PATH,
);
if (!resolved) {
problems.push(
`abortable-operation: ${consumer} does not resolve its import to the shared primitive`,
);
}
}
if (problems.length > 0) {
for (const problem of problems) console.error(problem);
process.exitCode = 1;
return;
}
// GOV-04. The exact importer set is part of the receipt, so a reviewer can
// see which consumers the gate actually verified rather than a bare count.
console.log(
`Adapter inventory: ${tracked.length} files PASS; ` +
`service worker asset table: ${
Object.keys(CACHEABLE_ASSET_CONTENT_TYPES).length
} shared extensions PASS; ` +
`fixture node_modules linking PASS; ` +
`shared abort primitive: ${importers.length} importers ` +
`(${importers.join(", ")}) PASS`,
);
}
await main();
+43 -3
View File
@@ -723,9 +723,13 @@ function findArchitectureViolations(
continue;
}
for (const dependency of dependencies) {
const sourceGroups = rule.from?.path
? (new RegExp(rule.from.path, "u").exec(dependency.source)?.slice(1) ??
[])
: [];
if (
matchesPath(dependency.source, rule.from) &&
matchesPath(dependency.target, rule.to)
matchesPath(dependency.target, rule.to, sourceGroups)
) {
violations.push({
rule: rule.name,
@@ -746,16 +750,52 @@ function findArchitectureViolations(
function matchesPath(
modulePath: string,
criterion: PathRule | undefined,
sourceGroups: readonly string[] = [],
): boolean {
if (!criterion) return true;
if (criterion.path && !new RegExp(criterion.path, "u").test(modulePath)) {
if (
criterion.path &&
!new RegExp(expandSourceGroups(criterion.path, sourceGroups), "u").test(
modulePath,
)
) {
return false;
}
return !(
criterion.pathNot && new RegExp(criterion.pathNot, "u").test(modulePath)
criterion.pathNot &&
new RegExp(expandSourceGroups(criterion.pathNot, sourceGroups), "u").test(
modulePath,
)
);
}
/**
* Substitutes `$1`..`$9` in a `to` pattern with the capture groups the `from`
* pattern matched on the importing module.
*
* Without it, "an adapter may not import a *different* adapter" cannot be
* written as one rule: the target pattern has to name the importer's own
* directory to exempt it. The alternative is one rule per adapter group, which
* silently stops covering a group the moment somebody adds one exactly the
* gap that let `diagnostics` import `telemetry` while the documented rule said
* it could not.
*/
function expandSourceGroups(
pattern: string,
sourceGroups: readonly string[],
): string {
return pattern.replaceAll(/\$([1-9])/gu, (whole, index: string) => {
const captured = sourceGroups[Number(index) - 1];
// A `from` pattern that did not capture leaves the token literal rather
// than quietly matching everything.
return captured === undefined ? whole : escapeRegExp(captured);
});
}
function escapeRegExp(value: string): string {
return value.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
}
function validateArchitectureRules(rules: readonly ArchitectureRule[]): void {
if (!rules.some((rule) => rule.to?.circular === true)) {
throw new Error("Architecture configuration must contain a circular rule");
+2 -2
View File
@@ -77,7 +77,7 @@ if (!architecture?.evidenceArtifactIds.some((id) => index.artifacts.get(id)?.pat
}
const expectedGateIds = Array.from(
{ length: 26 },
{ length: 27 },
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
);
const passingResults: Record<string, GateResult> = Object.fromEntries(
@@ -139,4 +139,4 @@ if (failures.length > 0) {
process.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`);
process.exit(1);
}
process.stdout.write("CI contract: 26 gates, strict v2 graph and generated workflow model PASS\n");
process.stdout.write("CI contract: 27 gates, strict v2 graph and generated workflow model PASS\n");

Some files were not shown because too many files have changed in this diff Show More