Compare commits
90
Commits
002ba3624e
...
d84b57bb3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84b57bb3f | ||
|
|
cf45bcc7dc | ||
|
|
b2f577ef49 | ||
|
|
2483c4032f | ||
|
|
d2574a3be7 | ||
|
|
9950cb9d6b | ||
|
|
1f2cba79e9 | ||
|
|
f69edb633d | ||
|
|
a5825c18b5 | ||
|
|
419d9d006d | ||
|
|
74281b0277 | ||
|
|
1a40522ff8 | ||
|
|
ef17bca445 | ||
|
|
813f9e16cd | ||
|
|
a889cb5c00 | ||
|
|
65f8528ccc | ||
|
|
e2a0e695dd | ||
|
|
e4f9f81a3f | ||
|
|
172497591f | ||
|
|
6085af51b6 | ||
|
|
f19be639a3 | ||
|
|
5c997f3a7e | ||
|
|
83d47e7185 | ||
|
|
3ab04a236d | ||
|
|
b11aa94f1c | ||
|
|
073fda87eb | ||
|
|
783e9b2cf1 | ||
|
|
7ff9728a5c | ||
|
|
98649585e6 | ||
|
|
54d9bf9120 | ||
|
|
e2c1d076f4 | ||
|
|
9d91001a31 | ||
|
|
0c071aaabc | ||
|
|
35cc5c868a | ||
|
|
2cab4974b7 | ||
|
|
c9c832c365 | ||
|
|
d9c2d8bc5e | ||
|
|
7724a8720c | ||
|
|
3b641906b8 | ||
|
|
7424ed4594 | ||
|
|
1d01a522de | ||
|
|
9313018ef5 | ||
|
|
66c047cec8 | ||
|
|
a6fc536d8a | ||
|
|
6c40c291d9 | ||
|
|
7381be1477 | ||
|
|
a0e0be6522 | ||
|
|
639e1a49c9 | ||
|
|
ac555a85e8 | ||
|
|
9f0599a389 | ||
|
|
78766accdf | ||
|
|
12329ec9c0 | ||
|
|
9e5fbd1384 | ||
|
|
93ce86eef4 | ||
|
|
cd1ef5cda2 | ||
|
|
7d1ccccbcd | ||
|
|
0355b644a0 | ||
|
|
79e9aa8328 | ||
|
|
72669949bc | ||
|
|
47cbf4ddca | ||
|
|
3a7c5deca0 | ||
|
|
6c2780b7a7 | ||
|
|
c5c8b9423c | ||
|
|
9c6906fc6f | ||
|
|
5933265975 | ||
|
|
887f5e6eb1 | ||
|
|
2b6fa42620 | ||
|
|
4283e40bb2 | ||
|
|
512aa4a1e9 | ||
|
|
ef1d5cc548 | ||
|
|
c9164c1a03 | ||
|
|
3bc74e0195 | ||
|
|
bdee07a93b | ||
|
|
627df884dd | ||
|
|
01316763e3 | ||
|
|
adb8613cb9 | ||
|
|
27dda3e0e3 | ||
|
|
708680b28e | ||
|
|
8342fb14dc | ||
|
|
16753f53af | ||
|
|
82f94423e5 | ||
|
|
01ed1e9300 | ||
|
|
5479101c8c | ||
|
|
034b702e8e | ||
|
|
05e3d50ba0 | ||
|
|
954ca8a1fd | ||
|
|
325a2a0843 | ||
|
|
c2d03165b3 | ||
|
|
93db3c184b | ||
|
|
4bff9ca151 |
@@ -156,6 +156,41 @@
|
|||||||
"path": "^(src/(presentation|bootstrap)|react|react-dom|@tanstack)"
|
"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",
|
"name": "no-circular-dependencies",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -126,6 +126,9 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
dist_sha256: ${{ steps.candidate.outputs.dist_sha256 }}
|
dist_sha256: ${{ steps.candidate.outputs.dist_sha256 }}
|
||||||
archive_sha256: ${{ steps.candidate.outputs.archive_sha256 }}
|
archive_sha256: ${{ steps.candidate.outputs.archive_sha256 }}
|
||||||
|
env:
|
||||||
|
APP_PROFILE: "${{ vars.APP_PROFILE }}"
|
||||||
|
RELEASE_TARGET: "${{ vars.RELEASE_TARGET }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
@@ -187,6 +190,7 @@ jobs:
|
|||||||
scripts/lib/secret-scan.ts \
|
scripts/lib/secret-scan.ts \
|
||||||
scripts/lib/supply-chain.ts \
|
scripts/lib/supply-chain.ts \
|
||||||
scripts/lib/validated-json-artifact.ts \
|
scripts/lib/validated-json-artifact.ts \
|
||||||
|
scripts/lib/vite-route-chunks.ts \
|
||||||
src/contracts/release-artifacts.ts \
|
src/contracts/release-artifacts.ts \
|
||||||
src/features/installed-contract-contributions.ts \
|
src/features/installed-contract-contributions.ts \
|
||||||
src/features/installed-feature-contracts.ts \
|
src/features/installed-feature-contracts.ts \
|
||||||
|
|||||||
+11
@@ -6,6 +6,7 @@ dist/
|
|||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
coverage/
|
coverage/
|
||||||
|
.worktrees/
|
||||||
!tests/fixtures/coverage/
|
!tests/fixtures/coverage/
|
||||||
!tests/fixtures/coverage/below-threshold.json
|
!tests/fixtures/coverage/below-threshold.json
|
||||||
artifacts/**/*.json
|
artifacts/**/*.json
|
||||||
@@ -18,3 +19,13 @@ artifacts/storybook/
|
|||||||
artifacts/tests/storybook/
|
artifacts/tests/storybook/
|
||||||
artifacts/tests/visual/
|
artifacts/tests/visual/
|
||||||
!artifacts/**/.gitkeep
|
!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/
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import { ApplicationProvider } from "../src/presentation/providers/application-p
|
|||||||
import { SessionProvider } from "../src/presentation/providers/session-provider.tsx";
|
import { SessionProvider } from "../src/presentation/providers/session-provider.tsx";
|
||||||
import { ThemeProvider } from "../src/presentation/providers/theme-provider.tsx";
|
import { ThemeProvider } from "../src/presentation/providers/theme-provider.tsx";
|
||||||
import "../src/presentation/styles/theme.css";
|
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 preferences = new Map<string, unknown>();
|
||||||
const application = createApplication({
|
const application = createApplication({
|
||||||
@@ -45,6 +50,16 @@ const application = createApplication({
|
|||||||
routeChunks: {},
|
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: {
|
runtimeCapabilities: {
|
||||||
getSnapshot: () =>
|
getSnapshot: () =>
|
||||||
Object.freeze(
|
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.
|
||||||
@@ -4,9 +4,9 @@ Initialized from `clean-architecture-frontend-template` revision
|
|||||||
`4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is
|
`4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is
|
||||||
recorded in `template.lock.json`.
|
recorded in `template.lock.json`.
|
||||||
|
|
||||||
A React/Vite reference implementation where architecture boundaries,
|
A React/Vite TechLog application where architecture boundaries, integration
|
||||||
integration behavior, release coherence, accessibility, performance, and
|
behavior, release coherence, accessibility, performance, and operations are
|
||||||
operations are executable contracts rather than conventions.
|
executable contracts rather than conventions.
|
||||||
|
|
||||||
## Start locally
|
## Start locally
|
||||||
|
|
||||||
@@ -28,26 +28,84 @@ corepack pnpm dev
|
|||||||
Runtime-public settings live in `public/config.json` and are validated before
|
Runtime-public settings live in `public/config.json` and are validated before
|
||||||
the product tree mounts. Client secrets are forbidden.
|
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,
|
The default build mounts the source-faithful TechLog Public and Studio
|
||||||
responsive sidebar, route focus management, session integration status, and a
|
experience. Public routes provide discovery, search, documents, topics,
|
||||||
persistent `system` / `light` / `dark` theme selector.
|
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 |
|
The 27 canonical route definitions are divided into `PUBLIC` and `STUDIO`
|
||||||
| --- | --- |
|
nested layouts. Studio authentication remains deliberately deferred; its mock
|
||||||
| `/` | implementation readiness and starter links |
|
gateway state lasts for one Studio shell session and resets on a full document
|
||||||
| `/examples/ui` | buttons, fields, cards, alerts, badges, modal, and tokens |
|
load. The exact route, dependency, stylesheet, asset, and parity inventory is
|
||||||
| `/examples/states` | loading, refresh, empty, error, auth, forbidden, and not-found states |
|
recorded in
|
||||||
| `/examples/auth` | reactive external-auth integration seam |
|
[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md).
|
||||||
| `/examples/reference-resources` | removable, session-required reference feature |
|
|
||||||
|
|
||||||
`AUTH_MODE=demo` is credential-free and accepted only in local/development
|
`corepack pnpm build` emits a self-contained `dist/server.mjs` production
|
||||||
environments. Deployments use `AUTH_MODE=external` and provide the opaque auth
|
boundary. `corepack pnpm preview --host 127.0.0.1 --port 4174` serves known
|
||||||
owner described in
|
Public and Studio routes as SPA documents, preserves the in-shell Studio 404,
|
||||||
[`docs/architecture/starter-experience.md`](docs/architecture/starter-experience.md).
|
and returns source-exact raw `404 text/plain` responses for missing Public
|
||||||
The client route policy is user experience only; server authorization remains
|
content. With the read-only source temp-copy server on `4375`, run:
|
||||||
authoritative.
|
|
||||||
|
```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
|
## Architecture
|
||||||
|
|
||||||
@@ -61,11 +119,13 @@ contracts own cross-cutting registries
|
|||||||
```
|
```
|
||||||
|
|
||||||
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||||
`docs/architecture/starter-experience.md`. The removable vertical slice is
|
`docs/architecture/starter-experience.md`. TechLog is one feature boundary
|
||||||
under `src/features/reference-feature`; its domain, application input, HTTP
|
under `src/features/tech-log`. Its immutable Public catalog and session-scoped
|
||||||
adapter, contracts, route runtime, and presentation are installed through the
|
Studio mock gateway are injected through the application feature input;
|
||||||
feature contribution files in `src/features`. The generic starter routes
|
Public and Studio presentation code shares the typed content renderer without
|
||||||
continue to typecheck, test, and build after that contribution is removed.
|
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
|
### Platform capability review
|
||||||
|
|
||||||
@@ -117,6 +177,9 @@ corepack pnpm verify:release
|
|||||||
corepack pnpm check:registries
|
corepack pnpm check:registries
|
||||||
corepack pnpm drill:runbooks
|
corepack pnpm drill:runbooks
|
||||||
corepack pnpm check:ci
|
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
|
`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:
|
Two gates intentionally need external evidence:
|
||||||
|
|
||||||
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
|
- `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
|
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
||||||
eligible-sample threshold and 28 days of production data exist.
|
eligible-sample threshold and 28 days of production data exist.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: 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.
|
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_FORM accessibility review
|
# TECH_LOG_CASE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_FORM
|
Route ID: TECH_LOG_CASE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -15,4 +15,4 @@ M5 Error association: pending
|
|||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
# EXAMPLES_UI accessibility review
|
# TECH_LOG_EXPLORE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: EXAMPLES_UI
|
Route ID: TECH_LOG_EXPLORE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -15,4 +15,4 @@ M5 Error association: pending
|
|||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: 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.
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_LIST accessibility review
|
# TECH_LOG_HOME accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_LIST
|
Route ID: TECH_LOG_HOME
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_DETAIL accessibility review
|
# TECH_LOG_PROFILE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_DETAIL
|
Route ID: TECH_LOG_PROFILE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: 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
@@ -174,6 +174,11 @@
|
|||||||
"script": "test:reference-feature",
|
"script": "test:reference-feature",
|
||||||
"expect": "pass"
|
"expect": "pass"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "test-tech-log",
|
||||||
|
"script": "test:tech-log",
|
||||||
|
"expect": "pass"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "test-recipes",
|
"id": "test-recipes",
|
||||||
"script": "test:recipes",
|
"script": "test:recipes",
|
||||||
@@ -323,6 +328,11 @@
|
|||||||
"expectedExitCode": 1,
|
"expectedExitCode": 1,
|
||||||
"expectedDiagnosticId": "duplicates routeId=DUPLICATE"
|
"expectedDiagnosticId": "duplicates routeId=DUPLICATE"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "check-tech-log-contract",
|
||||||
|
"script": "check:tech-log-contract",
|
||||||
|
"expect": "pass"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "build",
|
"id": "build",
|
||||||
"script": "build",
|
"script": "build",
|
||||||
@@ -472,6 +482,11 @@
|
|||||||
"id": "check-ci",
|
"id": "check-ci",
|
||||||
"script": "check:ci",
|
"script": "check:ci",
|
||||||
"expect": "pass"
|
"expect": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "check-release-admission",
|
||||||
|
"script": "check:release-admission",
|
||||||
|
"expect": "pass"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"artifactSchemas": [
|
"artifactSchemas": [
|
||||||
@@ -732,6 +747,12 @@
|
|||||||
"id": "sarif-secret-scan",
|
"id": "sarif-secret-scan",
|
||||||
"kind": "sarif",
|
"kind": "sarif",
|
||||||
"maxBytes": 67108864
|
"maxBytes": 67108864
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "json-deployment-admission",
|
||||||
|
"kind": "json",
|
||||||
|
"maxBytes": 67108864,
|
||||||
|
"executableSchemaId": "deployment-admission"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
@@ -885,6 +906,15 @@
|
|||||||
"test-reference-feature"
|
"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",
|
"id": "artifact-artifacts-tests-optional-recipes-xml",
|
||||||
"path": "artifacts/tests/optional-recipes.xml",
|
"path": "artifacts/tests/optional-recipes.xml",
|
||||||
@@ -1006,58 +1036,172 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
|
||||||
"path": "artifacts/tests/a11y-manual/APP_HOME.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_HOME.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_UI.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_CASE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
},
|
||||||
"review-a11y-manual"
|
{
|
||||||
]
|
"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",
|
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
||||||
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
|
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-report-json",
|
"id": "artifact-artifacts-tests-a11y-manual-report-json",
|
||||||
@@ -1602,6 +1746,21 @@
|
|||||||
"producerCommandIds": [
|
"producerCommandIds": [
|
||||||
"check-ci"
|
"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": [
|
"gates": [
|
||||||
@@ -1707,6 +1866,7 @@
|
|||||||
"test-integration",
|
"test-integration",
|
||||||
"test-http-scenario-evidence",
|
"test-http-scenario-evidence",
|
||||||
"test-reference-feature",
|
"test-reference-feature",
|
||||||
|
"test-tech-log",
|
||||||
"test-recipes"
|
"test-recipes"
|
||||||
],
|
],
|
||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-007-txt",
|
"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-json",
|
||||||
"artifact-artifacts-quality-http-scenario-evidence-fixture-json",
|
"artifact-artifacts-quality-http-scenario-evidence-fixture-json",
|
||||||
"artifact-artifacts-tests-reference-feature-xml",
|
"artifact-artifacts-tests-reference-feature-xml",
|
||||||
|
"artifact-artifacts-tests-tech-log-xml",
|
||||||
"artifact-artifacts-tests-optional-recipes-xml"
|
"artifact-artifacts-tests-optional-recipes-xml"
|
||||||
],
|
],
|
||||||
"retentionClassId": "merge-cycle"
|
"retentionClassId": "merge-cycle"
|
||||||
@@ -1757,11 +1918,33 @@
|
|||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
|
||||||
"evidenceArtifactIds": [
|
"evidenceArtifactIds": [
|
||||||
"artifact-artifacts-tests-a11y-json",
|
"artifact-artifacts-tests-a11y-json",
|
||||||
"artifact-artifacts-tests-a11y-manual-APP-HOME-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-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-NOT-FOUND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-report-json"
|
"artifact-artifacts-tests-a11y-manual-report-json"
|
||||||
],
|
],
|
||||||
@@ -1788,6 +1971,7 @@
|
|||||||
"check-registries-baseline-fixture",
|
"check-registries-baseline-fixture",
|
||||||
"check-registries-fixture",
|
"check-registries-fixture",
|
||||||
"check-routes-fixture",
|
"check-routes-fixture",
|
||||||
|
"check-tech-log-contract",
|
||||||
"check-ci"
|
"check-ci"
|
||||||
],
|
],
|
||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
|
||||||
@@ -2049,6 +2233,18 @@
|
|||||||
"artifact-artifacts-performance-lab-json"
|
"artifact-artifacts-performance-lab-json"
|
||||||
],
|
],
|
||||||
"retentionClassId": "release-coherence"
|
"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": [
|
"stages": [
|
||||||
@@ -2083,7 +2279,8 @@
|
|||||||
"FE-GATE-014",
|
"FE-GATE-014",
|
||||||
"FE-GATE-015",
|
"FE-GATE-015",
|
||||||
"FE-GATE-019",
|
"FE-GATE-019",
|
||||||
"FE-GATE-026"
|
"FE-GATE-026",
|
||||||
|
"FE-GATE-027"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2236,10 +2433,20 @@
|
|||||||
"condition": "release",
|
"condition": "release",
|
||||||
"timeoutMinutes": 45,
|
"timeoutMinutes": 45,
|
||||||
"gateIds": [
|
"gateIds": [
|
||||||
"FE-GATE-015"
|
"FE-GATE-015",
|
||||||
|
"FE-GATE-027"
|
||||||
],
|
],
|
||||||
"browserGateIds": [],
|
"browserGateIds": [],
|
||||||
"environment": [],
|
"environment": [
|
||||||
|
{
|
||||||
|
"name": "APP_PROFILE",
|
||||||
|
"value": "${{ vars.APP_PROFILE }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RELEASE_TARGET",
|
||||||
|
"value": "${{ vars.RELEASE_TARGET }}"
|
||||||
|
}
|
||||||
|
],
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"kind": "checkout"
|
"kind": "checkout"
|
||||||
@@ -2301,6 +2508,7 @@
|
|||||||
"scripts/lib/secret-scan.ts",
|
"scripts/lib/secret-scan.ts",
|
||||||
"scripts/lib/supply-chain.ts",
|
"scripts/lib/supply-chain.ts",
|
||||||
"scripts/lib/validated-json-artifact.ts",
|
"scripts/lib/validated-json-artifact.ts",
|
||||||
|
"scripts/lib/vite-route-chunks.ts",
|
||||||
"src/contracts/release-artifacts.ts",
|
"src/contracts/release-artifacts.ts",
|
||||||
"src/features/installed-contract-contributions.ts",
|
"src/features/installed-contract-contributions.ts",
|
||||||
"src/features/installed-feature-contracts.ts",
|
"src/features/installed-feature-contracts.ts",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8",
|
"snapshotDigest": "428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b",
|
||||||
"owner": "frontend-platform",
|
"owner": "tech-log-frontend",
|
||||||
"reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
|
"reason": "Install approved TechLog Public and Studio route contract",
|
||||||
"approvedAt": "2026-08-01T15:15:45.537Z"
|
"approvedAt": "2026-08-15T16:32:32.042Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
"registryId": "FE-REG-ROUTE",
|
"registryId": "FE-REG-ROUTE",
|
||||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 10,
|
"rowCount": 27,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"fieldTypes": {
|
"fieldTypes": {
|
||||||
"routeId": "string",
|
"routeId": "string",
|
||||||
"path": "string",
|
"path": "string",
|
||||||
|
"layoutGroup": "string",
|
||||||
"paramsSchema": "string|null",
|
"paramsSchema": "string|null",
|
||||||
"searchSchema": "string|null",
|
"searchSchema": "string|null",
|
||||||
"access": "string",
|
"access": "string",
|
||||||
@@ -43,14 +45,29 @@
|
|||||||
"public",
|
"public",
|
||||||
"session-required"
|
"session-required"
|
||||||
],
|
],
|
||||||
|
"layoutGroup": [
|
||||||
|
"PUBLIC",
|
||||||
|
"STUDIO"
|
||||||
|
],
|
||||||
"paramsSchema": [
|
"paramsSchema": [
|
||||||
null,
|
null,
|
||||||
"NotFoundSplat",
|
"NotFoundSplat",
|
||||||
"ReferenceResourceParams"
|
"ReferenceResourceParams",
|
||||||
|
"TechLogExploreKindParams",
|
||||||
|
"TechLogSlugParams",
|
||||||
|
"TechLogVersionParams",
|
||||||
|
"TechLogDocumentIdParams",
|
||||||
|
"TechLogPublicationEventIdParams",
|
||||||
|
"TechLogStudioSplat"
|
||||||
],
|
],
|
||||||
"searchSchema": [
|
"searchSchema": [
|
||||||
null,
|
null,
|
||||||
"ReferenceResourceListQuery"
|
"ReferenceResourceListQuery",
|
||||||
|
"TechLogHomeSearch",
|
||||||
|
"TechLogExploreSearch",
|
||||||
|
"TechLogExploreKindSearch",
|
||||||
|
"TechLogSearchQuery",
|
||||||
|
"TechLogCaseStateSearch"
|
||||||
],
|
],
|
||||||
"loadingSurface": [
|
"loadingSurface": [
|
||||||
"app-shell",
|
"app-shell",
|
||||||
@@ -88,6 +105,7 @@
|
|||||||
"breakingFields": [
|
"breakingFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -95,75 +113,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"rows": {
|
"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": {
|
"NOT_FOUND": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"chunkId": "route-not-found",
|
"chunkId": "route-not-found",
|
||||||
"errorSurface": "not-found",
|
"errorSurface": "not-found",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
"loadingSurface": "none",
|
"loadingSurface": "none",
|
||||||
"navigationLabel": null,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
@@ -171,59 +125,371 @@
|
|||||||
"path": "*",
|
"path": "*",
|
||||||
"routeId": "NOT_FOUND",
|
"routeId": "NOT_FOUND",
|
||||||
"searchSchema": null,
|
"searchSchema": null,
|
||||||
"title": "페이지를 찾을 수 없음"
|
"title": "페이지를 찾을 수 없습니다."
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_DETAIL": {
|
"TECH_LOG_CASE": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resource-detail",
|
"chunkId": "route-tech-log-case",
|
||||||
"errorSurface": "feature-boundary",
|
"errorSurface": "feature-boundary",
|
||||||
"loadingSurface": "reference-resource-detail",
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
"navigationLabel": null,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
"paramsSchema": "ReferenceResourceParams",
|
"paramsSchema": "TechLogSlugParams",
|
||||||
"path": "/examples/reference-resources/:resourceId",
|
"path": "/cases/:slug",
|
||||||
"routeId": "REFERENCE_RESOURCE_DETAIL",
|
"routeId": "TECH_LOG_CASE",
|
||||||
"searchSchema": null,
|
"searchSchema": "TechLogCaseStateSearch",
|
||||||
"title": "Reference detail"
|
"title": "Case"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_FORM": {
|
"TECH_LOG_EXPLORE": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resource-form",
|
"chunkId": "route-tech-log-explore",
|
||||||
"errorSurface": "feature-boundary",
|
"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,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
"paramsSchema": null,
|
"paramsSchema": null,
|
||||||
"path": "/examples/reference-resources/new",
|
"path": "/",
|
||||||
"routeId": "REFERENCE_RESOURCE_FORM",
|
"routeId": "TECH_LOG_HOME",
|
||||||
"searchSchema": null,
|
"searchSchema": "TechLogHomeSearch",
|
||||||
"title": "Reference form"
|
"title": "TechLog"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_LIST": {
|
"TECH_LOG_PROFILE": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resources",
|
"chunkId": "route-tech-log-profile",
|
||||||
"errorSurface": "feature-boundary",
|
"errorSurface": "feature-boundary",
|
||||||
"loadingSurface": "reference-resource-list",
|
"layoutGroup": "PUBLIC",
|
||||||
"navigationLabel": "Reference feature",
|
"loadingSurface": "app-shell",
|
||||||
"navigationOrder": 50,
|
"navigationLabel": "프로필",
|
||||||
|
"navigationOrder": 40,
|
||||||
"paramsSchema": null,
|
"paramsSchema": null,
|
||||||
"path": "/examples/reference-resources",
|
"path": "/profile",
|
||||||
"routeId": "REFERENCE_RESOURCE_LIST",
|
"routeId": "TECH_LOG_PROFILE",
|
||||||
"searchSchema": "ReferenceResourceListQuery",
|
"searchSchema": null,
|
||||||
"title": "Reference feature"
|
"title": "프로필"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_STATUS": {
|
"TECH_LOG_PROJECT": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resource-status",
|
"chunkId": "route-tech-log-project",
|
||||||
"errorSurface": "feature-boundary",
|
"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,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
"paramsSchema": null,
|
"paramsSchema": null,
|
||||||
"path": "/examples/reference-resources/status",
|
"path": "/search",
|
||||||
"routeId": "REFERENCE_RESOURCE_STATUS",
|
"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,
|
"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",
|
"registryId": "FE-REG-ROUTE-RUNTIME",
|
||||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 10,
|
"rowCount": 27,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
@@ -276,64 +542,166 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"rows": {
|
"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": {
|
"NOT_FOUND": {
|
||||||
"moduleId": "not-found-page",
|
"moduleId": "route-not-found",
|
||||||
"paramsCodec": "NotFoundSplat",
|
"paramsCodec": "NotFoundSplat",
|
||||||
"routeId": "NOT_FOUND",
|
"routeId": "NOT_FOUND",
|
||||||
"searchCodec": "none"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_DETAIL": {
|
"TECH_LOG_CASE": {
|
||||||
"moduleId": "reference-resource-detail-page",
|
"moduleId": "route-tech-log-case",
|
||||||
"paramsCodec": "ReferenceResourceParams",
|
"paramsCodec": "TechLogSlugParams",
|
||||||
"routeId": "REFERENCE_RESOURCE_DETAIL",
|
"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"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_FORM": {
|
"TECH_LOG_PROJECT": {
|
||||||
"moduleId": "reference-resource-form-page",
|
"moduleId": "route-tech-log-project",
|
||||||
"paramsCodec": "none",
|
"paramsCodec": "TechLogSlugParams",
|
||||||
"routeId": "REFERENCE_RESOURCE_FORM",
|
"routeId": "TECH_LOG_PROJECT",
|
||||||
"searchCodec": "none"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_LIST": {
|
"TECH_LOG_PROJECT_ACTIVITY": {
|
||||||
"moduleId": "reference-resource-page",
|
"moduleId": "route-tech-log-project-activity",
|
||||||
"paramsCodec": "none",
|
"paramsCodec": "TechLogSlugParams",
|
||||||
"routeId": "REFERENCE_RESOURCE_LIST",
|
"routeId": "TECH_LOG_PROJECT_ACTIVITY",
|
||||||
"searchCodec": "ReferenceResourceListQuery"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_STATUS": {
|
"TECH_LOG_PROJECT_DECISIONS": {
|
||||||
"moduleId": "reference-resource-status-page",
|
"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",
|
"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"
|
"searchCodec": "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,7 +898,7 @@
|
|||||||
"registryId": "FE-REG-SCHEMA",
|
"registryId": "FE-REG-SCHEMA",
|
||||||
"owner": "feature-frontend-contract-schema-registry",
|
"owner": "feature-frontend-contract-schema-registry",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 8,
|
"rowCount": 19,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"schemaId",
|
"schemaId",
|
||||||
@@ -633,6 +1001,105 @@
|
|||||||
"schemaId": "ReferenceResourcePayload",
|
"schemaId": "ReferenceResourcePayload",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
"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",
|
"registryId": "FE-REG-STORAGE",
|
||||||
"owner": "feature-frontend-storage-registry-contract",
|
"owner": "feature-frontend-storage-registry-contract",
|
||||||
"source": "src/contracts/storage-keys.ts",
|
"source": "src/contracts/storage-keys.ts",
|
||||||
"rowCount": 4,
|
"rowCount": 5,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"logicalName",
|
"logicalName",
|
||||||
@@ -847,6 +1314,19 @@
|
|||||||
"ttl": null,
|
"ttl": null,
|
||||||
"valueCodec": "none"
|
"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": {
|
"CHUNK_RELOAD_GUARD": {
|
||||||
"backend": "sessionStorage",
|
"backend": "sessionStorage",
|
||||||
"classification": "opaque-cache",
|
"classification": "opaque-cache",
|
||||||
|
|||||||
@@ -1,6 +1,190 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"changes": [
|
"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",
|
"changeId": "FE-REG-ENV:API_CONTRACT_VERSION:*:removed",
|
||||||
"versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.",
|
"versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"fieldTypes": {
|
"fieldTypes": {
|
||||||
"routeId": "string",
|
"routeId": "string",
|
||||||
"path": "string",
|
"path": "string",
|
||||||
|
"layoutGroup": "string",
|
||||||
"paramsSchema": "string|null",
|
"paramsSchema": "string|null",
|
||||||
"searchSchema": "string|null",
|
"searchSchema": "string|null",
|
||||||
"access": "string",
|
"access": "string",
|
||||||
@@ -41,8 +43,27 @@
|
|||||||
"uniqueFields": ["routeId", "path", "chunkId"],
|
"uniqueFields": ["routeId", "path", "chunkId"],
|
||||||
"allowedValues": {
|
"allowedValues": {
|
||||||
"access": ["public", "session-required"],
|
"access": ["public", "session-required"],
|
||||||
"paramsSchema": [null, "NotFoundSplat", "ReferenceResourceParams"],
|
"layoutGroup": ["PUBLIC", "STUDIO"],
|
||||||
"searchSchema": [null, "ReferenceResourceListQuery"],
|
"paramsSchema": [
|
||||||
|
null,
|
||||||
|
"NotFoundSplat",
|
||||||
|
"ReferenceResourceParams",
|
||||||
|
"TechLogExploreKindParams",
|
||||||
|
"TechLogSlugParams",
|
||||||
|
"TechLogVersionParams",
|
||||||
|
"TechLogDocumentIdParams",
|
||||||
|
"TechLogPublicationEventIdParams",
|
||||||
|
"TechLogStudioSplat"
|
||||||
|
],
|
||||||
|
"searchSchema": [
|
||||||
|
null,
|
||||||
|
"ReferenceResourceListQuery",
|
||||||
|
"TechLogHomeSearch",
|
||||||
|
"TechLogExploreSearch",
|
||||||
|
"TechLogExploreKindSearch",
|
||||||
|
"TechLogSearchQuery",
|
||||||
|
"TechLogCaseStateSearch"
|
||||||
|
],
|
||||||
"loadingSurface": [
|
"loadingSurface": [
|
||||||
"app-shell",
|
"app-shell",
|
||||||
"example-page",
|
"example-page",
|
||||||
@@ -84,6 +105,7 @@
|
|||||||
"breakingFields": [
|
"breakingFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -234,7 +256,7 @@
|
|||||||
"consumerIdentityField": "schemaId",
|
"consumerIdentityField": "schemaId",
|
||||||
"consumerDirectories": [
|
"consumerDirectories": [
|
||||||
"src/presentation/routes",
|
"src/presentation/routes",
|
||||||
"src/features/reference-feature/presentation",
|
"src/features/tech-log/presentation",
|
||||||
"src/features/reference-feature/contracts"
|
"src/features/reference-feature/contracts"
|
||||||
],
|
],
|
||||||
"breakingFields": ["schemaId", "boundary", "runtime"]
|
"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"],
|
"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",
|
"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."],
|
"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.",
|
"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."],
|
"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"
|
"serverStatePolicy": "query-cache-metadata-only"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"ISC",
|
"ISC",
|
||||||
"MIT",
|
"MIT",
|
||||||
"MIT-0",
|
"MIT-0",
|
||||||
"MPL-2.0"
|
"MPL-2.0",
|
||||||
|
"OFL-1.1"
|
||||||
],
|
],
|
||||||
"deniedLicensePatterns": [
|
"deniedLicensePatterns": [
|
||||||
"(^|\\s)AGPL",
|
"(^|\\s)AGPL",
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
# Manual accessibility review checklist
|
# Manual accessibility review checklist
|
||||||
|
|
||||||
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
||||||
review all six route records in `artifacts/tests/a11y-manual/` against one
|
review all 27 route records in `artifacts/tests/a11y-manual/` against one
|
||||||
release candidate and sign them. The required scope is derived from the route
|
release candidate and sign them. The required TechLog Public and Studio scope is
|
||||||
registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`,
|
derived from the installed route registry, so the gate rejects stale, missing,
|
||||||
`REFERENCE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the
|
or additional route records as well as blank identity/timestamp/signature
|
||||||
gate rejects blank identity/timestamp/signature fields, pending verdicts,
|
fields, pending verdicts, and mismatched release IDs. The scope is:
|
||||||
mismatched release IDs, or missing 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`.
|
||||||
|
|
||||||
|
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:
|
Allowed item verdicts:
|
||||||
|
|
||||||
@@ -17,7 +32,7 @@ Required record:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
Status: reviewed
|
Status: reviewed
|
||||||
Route ID: APP_HOME
|
Route ID: <exact installed route ID>
|
||||||
Release ID: <immutable release ID>
|
Release ID: <immutable release ID>
|
||||||
Reviewer: <human reviewer identity>
|
Reviewer: <human reviewer identity>
|
||||||
Reviewed at: <RFC 3339 timestamp>
|
Reviewed at: <RFC 3339 timestamp>
|
||||||
@@ -45,7 +60,8 @@ The reviewer must verify:
|
|||||||
- M7: non-essential motion is suppressed with reduced-motion preference
|
- M7: non-essential motion is suppressed with reduced-motion preference
|
||||||
- Screen reader: headings, live regions, errors, and actions are announced once
|
- 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
|
Routes with dialogs or form errors require real M4 modal-focus or M5
|
||||||
not be marked not-applicable on that route. Passing automated evidence means
|
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
|
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
|
```text
|
||||||
/ca-frontend-opfs-v1/
|
/ca-frontend-opfs-v1/
|
||||||
authorities/<authorityToken>/<namespaceToken>/<partitionToken>/
|
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
|
chunks/sha256/<digest-prefix>/<digest>.bin
|
||||||
staging/<transaction-id>/receipt.json
|
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가
|
구조화 metadata, query, revision, refcount와 operation journal은 IndexedDB가
|
||||||
소유한다. OPFS에는 immutable chunk와 bounded runtime-schema-validated manifest만
|
소유한다. OPFS에는 immutable chunk와 bounded runtime-schema-validated manifest만
|
||||||
둔다. readable `scope.namespace`는 경로에 쓰지 않는다.
|
둔다. readable `scope.namespace`는 경로에 쓰지 않는다.
|
||||||
@@ -736,6 +745,23 @@ COMMITTED <- 사용자에게 보이는 유일한 commit point
|
|||||||
CLEANED -> journal 제거
|
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한다.
|
- `PREPARING` crash: partial staging을 검증 후 resume하거나 purge한다.
|
||||||
- `FILES_READY` crash: expected generation과 digest가 맞으면 idempotent logical
|
- `FILES_READY` crash: expected generation과 digest가 맞으면 idempotent logical
|
||||||
commit, 아니면 quarantine한다.
|
commit, 아니면 quarantine한다.
|
||||||
|
|||||||
@@ -37,12 +37,41 @@ vendor 결정 전에는 안전한 기본값이 아니다.
|
|||||||
abort마다 `http.request.completed` diagnostics를 정확히 한 번 남긴다.
|
abort마다 `http.request.completed` diagnostics를 정확히 한 번 남긴다.
|
||||||
`api.request.failed` telemetry는 retry가 끝난 terminal non-abort failure에만
|
`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`,
|
6. `app.boot.failed`, `ui.render.failed`, `release.mismatch.detected`,
|
||||||
`telemetry.delivery.dropped`를 production path에 연결한다. cache와 storage
|
`telemetry.delivery.dropped`를 production path에 연결한다. cache와 storage
|
||||||
실패는 diagnostics로 기록하되 raw key/value를 기록하지 않는다.
|
실패는 diagnostics로 기록하되 raw key/value를 기록하지 않는다.
|
||||||
7. queue full, invalid event/context, serialization과 sink failure는 제한된
|
7. queue full, invalid event/context, serialization과 sink failure는 제한된
|
||||||
reason bucket으로 집계한다. drop observer의 failure는 다시 telemetry를
|
reason bucket으로 집계한다. drop observer의 failure는 다시 telemetry를
|
||||||
발행하지 않는 nonrecursive 경계다.
|
발행하지 않는 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,
|
8. diagnostics와 telemetry failure는 제품 흐름, HTTP 결과, route transition,
|
||||||
storage/cache fallback과 React error surface를 바꾸지 않는다.
|
storage/cache fallback과 React error surface를 바꾸지 않는다.
|
||||||
9. mount 전 bootstrap failure는 안전한 build/config/error kind만 별도 evidence로
|
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를 검증한다.
|
queue full, sink/observer failure와 pre-mount boot evidence를 검증한다.
|
||||||
- HTTP integration은 success, retry recovery, terminal failure와 abort의 producer
|
- HTTP integration은 success, retry recovery, terminal failure와 abort의 producer
|
||||||
횟수, route/operation/correlation context와 요청 값 비노출을 검증한다.
|
횟수, 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과
|
- cache/storage/release/application/runtime test는 각 production wiring과
|
||||||
diagnostics failure isolation을 검증한다.
|
diagnostics failure isolation을 검증한다.
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,55 @@ auth-required operation은 session state가 `authenticated`가 아니면 fetch
|
|||||||
`integration-failed`, `unauthenticated`와 credential attach rejection을 anonymous
|
`integration-failed`, `unauthenticated`와 credential attach rejection을 anonymous
|
||||||
request로 downgrade하지 않는다.
|
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
|
### 6. Cookie auth, CSRF와 CORS
|
||||||
|
|
||||||
same-origin BFF cookie session을 기본 권장한다.
|
same-origin BFF cookie session을 기본 권장한다.
|
||||||
|
|||||||
@@ -11,6 +11,26 @@
|
|||||||
첫 제품 stream/Web Push를 선택할 때, 또는 backend replay/hosting/provider
|
첫 제품 stream/Web Push를 선택할 때, 또는 backend replay/hosting/provider
|
||||||
protocol이 바뀔 때
|
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에
|
현재 optional recipe catalog는 realtime capability에
|
||||||
|
|||||||
@@ -17,11 +17,33 @@ The following edges are forbidden:
|
|||||||
|
|
||||||
- domain to application, presentation, adapters, bootstrap, React, or browser globals
|
- domain to application, presentation, adapters, bootstrap, React, or browser globals
|
||||||
- application to presentation, concrete 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
|
- 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
|
- an adapter to presentation, bootstrap internals, or another concrete adapter
|
||||||
- feature domain/application to its presentation or outbound adapter, and
|
- feature domain/application to its presentation or outbound adapter, and
|
||||||
feature presentation to its outbound adapter
|
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
|
`bootstrap` contains composition only. Business rules and page-specific
|
||||||
orchestration belong to domain/application.
|
orchestration belong to domain/application.
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,37 @@
|
|||||||
- 운영 절차:
|
- 운영 절차:
|
||||||
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
|
[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. 먼저 축을 분리한다
|
## 1. 먼저 축을 분리한다
|
||||||
|
|
||||||
네 이름은 같은 종류의 대안이 아니다.
|
네 이름은 같은 종류의 대안이 아니다.
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ capability가 설치됐거나 production-ready라는 뜻이 아니다.
|
|||||||
[Frontend ports, adapters, and boundaries](./frontend-ports-adapters-and-boundaries.md)
|
[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
|
## 0. 현재 상태와 목표 delta
|
||||||
|
|
||||||
이 문서에서 설계 승인, reference source 존재, production 조합과 target browser의
|
이 문서에서 설계 승인, reference source 존재, production 조합과 target browser의
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"standard": "rules/diagram-standards.md v2",
|
"standard": "rules/diagram-standards.md v2",
|
||||||
"evidenceReport": {
|
"evidenceReport": {
|
||||||
"repoPath": "docs/architecture/review-evidence.md",
|
"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"
|
"canonicalSha256": "b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29"
|
||||||
},
|
},
|
||||||
"reviews": {
|
"reviews": {
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"thresholdSatisfied": true,
|
"thresholdSatisfied": true,
|
||||||
"scope": "immutable static assets and mutable /config.json delivery"
|
"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 (367–724ms 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
|
자동 조치는 해당 runtime이 구현·조합된 제품에서만 실행한다. 현재 reference
|
||||||
primitive를 coordinator 완료 증거로 사용하지 않는다.
|
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. 공통 원칙
|
## 1. 공통 원칙
|
||||||
|
|
||||||
incident 중에도 다음 작업은 금지한다.
|
incident 중에도 다음 작업은 금지한다.
|
||||||
|
|||||||
@@ -12,6 +12,26 @@ session/account Query lifecycle, strict query policy와 Web Storage v2 lifecycle
|
|||||||
현재 `DESIGNED_NOT_IMPLEMENTED`다. 아래 목표 절차를 현재 runtime의 보장으로
|
현재 `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. 변경할 수 없는 복구 원칙
|
## 1. 변경할 수 없는 복구 원칙
|
||||||
|
|
||||||
- 서버가 server state와 authorization의 source of truth다.
|
- 서버가 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 B’s 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 readBoundedJson’s 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 TanStack’s 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 design’s 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 owner’s current header allowlist, but V3’s 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.
|
||||||
|
- telemetry’s 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`에 있다.
|
||||||
@@ -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
@@ -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 1–3 | 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 1–5 | 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가 없는지 검증한다.
|
||||||
@@ -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 = {
|
const commonLanguageOptions = {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
@@ -811,6 +999,12 @@ export default [
|
|||||||
...globals.node,
|
...globals.node,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
"test-assertion-boundary": testAssertionBoundaryPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"test-assertion-boundary/no-element-operand-equality": "error",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [`tests/support/browser/**/*.${sourceExtensions}`],
|
files: [`tests/support/browser/**/*.${sourceExtensions}`],
|
||||||
|
|||||||
+18
-3
@@ -11,14 +11,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "node scripts/build-frontend.ts",
|
"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",
|
"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",
|
"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:architecture": "node scripts/check-architecture.ts",
|
||||||
"check:design-system": "node scripts/check-design-system.ts",
|
"check:design-system": "node scripts/check-design-system.ts",
|
||||||
"check:design-system:fixture": "node scripts/check-design-system.ts --fixture",
|
"check:design-system:fixture": "node scripts/check-design-system.ts --fixture",
|
||||||
"check:i18n": "node scripts/check-i18n.ts",
|
"check:i18n": "node scripts/check-i18n.ts",
|
||||||
"check:i18n:fixture": "node scripts/check-i18n.ts --fixture",
|
"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": "node scripts/check-diagnostics.ts",
|
||||||
"check:diagnostics:fixture": "node scripts/check-diagnostics.ts --fixture",
|
"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",
|
"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-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: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: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: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: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",
|
"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:browser-file-storage-removal": "node scripts/test-browser-file-storage-runtime-removal.ts",
|
||||||
"test:realtime-removal": "node scripts/test-realtime-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: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",
|
"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",
|
"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",
|
"verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts",
|
||||||
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
|
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
|
||||||
"generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts",
|
"generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts",
|
||||||
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
"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",
|
"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: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",
|
"verify:promotion": "node scripts/verify-exact-promotion-bundle.ts",
|
||||||
@@ -120,11 +129,17 @@
|
|||||||
"check:types:service-worker": "tsc --project tsconfig.service-worker.json"
|
"check:types:service-worker": "tsc --project tsconfig.service-worker.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/ibm-plex-mono": "5.3.0",
|
||||||
"@tanstack/react-query": "5.101.4",
|
"@tanstack/react-query": "5.101.4",
|
||||||
"lucide-react": "1.25.0",
|
"lucide-react": "1.25.0",
|
||||||
|
"pretendard": "1.3.9",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-router-dom": "7.18.1",
|
"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"
|
"zod": "4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -9,35 +9,35 @@ export default defineConfig({
|
|||||||
["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }],
|
["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }],
|
||||||
],
|
],
|
||||||
use: {
|
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",
|
trace: "retain-on-failure",
|
||||||
screenshot: "only-on-failure",
|
screenshot: "only-on-failure",
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command:
|
command:
|
||||||
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4173",
|
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4273",
|
||||||
url: "http://127.0.0.1:4173",
|
url: "http://127.0.0.1:4273",
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "chromium",
|
||||||
testIgnore: "**/compact-smoke.spec.ts",
|
|
||||||
use: { ...devices["Desktop Chrome"] },
|
use: { ...devices["Desktop Chrome"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "firefox",
|
name: "firefox",
|
||||||
testIgnore: "**/compact-smoke.spec.ts",
|
|
||||||
use: { ...devices["Desktop Firefox"] },
|
use: { ...devices["Desktop Firefox"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "webkit",
|
name: "webkit",
|
||||||
testIgnore: "**/compact-smoke.spec.ts",
|
|
||||||
use: { ...devices["Desktop Safari"] },
|
use: { ...devices["Desktop Safari"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "chromium-compact",
|
name: "chromium-compact",
|
||||||
testMatch: "**/compact-smoke.spec.ts",
|
testMatch: "**/tech-log-responsive.spec.ts",
|
||||||
use: {
|
use: {
|
||||||
...devices["Desktop Chrome"],
|
...devices["Desktop Chrome"],
|
||||||
viewport: { width: 390, height: 844 },
|
viewport: { width: 390, height: 844 },
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ export default defineConfig({
|
|||||||
toHaveScreenshot: {
|
toHaveScreenshot: {
|
||||||
animations: "disabled",
|
animations: "disabled",
|
||||||
caret: "hide",
|
caret: "hide",
|
||||||
maxDiffPixelRatio: 0.002,
|
maxDiffPixels: 0,
|
||||||
scale: "css",
|
maxDiffPixelRatio: 0,
|
||||||
|
scale: "device",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
use: {
|
use: {
|
||||||
...devices["Desktop Chrome"],
|
...devices["Desktop Chrome"],
|
||||||
baseURL: "http://127.0.0.1:4174",
|
baseURL: "http://127.0.0.1:4174",
|
||||||
colorScheme: "light",
|
colorScheme: "light",
|
||||||
locale: "en-US",
|
locale: "ko-KR",
|
||||||
|
timezoneId: "Asia/Seoul",
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
@@ -33,5 +35,10 @@ export default defineConfig({
|
|||||||
url: "http://127.0.0.1:4174",
|
url: "http://127.0.0.1:4174",
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
},
|
},
|
||||||
projects: [{ name: "chromium-visual" }],
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"], deviceScaleFactor: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+729
File diff suppressed because it is too large
Load Diff
@@ -14,5 +14,8 @@
|
|||||||
"WEB_WORKER": "DEFAULT",
|
"WEB_WORKER": "DEFAULT",
|
||||||
"SERVICE_WORKER": "DEFAULT",
|
"SERVICE_WORKER": "DEFAULT",
|
||||||
"OFFLINE_COMMANDS": "DEFAULT"
|
"OFFLINE_COMMANDS": "DEFAULT"
|
||||||
|
},
|
||||||
|
"FEATURE_OVERRIDES": {
|
||||||
|
"reference-feature": "DEFAULT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -8,16 +8,33 @@
|
|||||||
"releaseId": "local-release",
|
"releaseId": "local-release",
|
||||||
"builtAt": "1970-01-01T00:00:00.000Z",
|
"builtAt": "1970-01-01T00:00:00.000Z",
|
||||||
"routeChunks": {
|
"routeChunks": {
|
||||||
"route-home": "src/presentation/pages/home-page.tsx",
|
"route-tech-log-home": "src/features/tech-log/presentation/public/pages/home-page.tsx",
|
||||||
"route-examples-platform": "src/presentation/examples/platform-overview-page.tsx",
|
"route-tech-log-explore": "src/features/tech-log/presentation/public/pages/explore-page.tsx",
|
||||||
"route-examples-ui": "src/presentation/examples/ui-gallery-page.tsx",
|
"route-tech-log-explore-kind": "src/features/tech-log/presentation/public/pages/explore-kind-page.tsx",
|
||||||
"route-examples-states": "src/presentation/examples/state-gallery-page.tsx",
|
"route-tech-log-case": "src/features/tech-log/presentation/public/pages/case-page.tsx",
|
||||||
"route-examples-auth": "src/presentation/examples/auth-example-page.tsx",
|
"route-tech-log-reference": "src/features/tech-log/presentation/public/pages/reference-page.tsx",
|
||||||
"route-reference-resources": "src/features/reference-feature/presentation/reference-resource-page.tsx",
|
"route-tech-log-question": "src/features/tech-log/presentation/public/pages/question-page.tsx",
|
||||||
"route-reference-resource-detail": "src/features/reference-feature/presentation/reference-resource-detail-page.tsx",
|
"route-tech-log-topic": "src/features/tech-log/presentation/public/pages/topic-page.tsx",
|
||||||
"route-reference-resource-form": "src/features/reference-feature/presentation/reference-resource-form-page.tsx",
|
"route-tech-log-projects": "src/features/tech-log/presentation/public/pages/projects-page.tsx",
|
||||||
"route-reference-resource-status": "src/features/reference-feature/presentation/reference-resource-status-page.tsx",
|
"route-tech-log-project": "src/features/tech-log/presentation/public/pages/project-overview-page.tsx",
|
||||||
"route-not-found": "src/presentation/pages/not-found-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": {
|
"contractSet": {
|
||||||
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
||||||
|
|||||||
@@ -13,12 +13,20 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim
|
|||||||
* 1. clean dist and .generated/frontend-runtime
|
* 1. clean dist and .generated/frontend-runtime
|
||||||
* 2. generate contractSet and build-info source
|
* 2. generate contractSet and build-info source
|
||||||
* 3. Vite app build (emptyOutDir = true)
|
* 3. Vite app build (emptyOutDir = true)
|
||||||
* 4. scan app dist and generate the static asset source
|
* 4. materialize dist/config.json from the declared APP_PROFILE
|
||||||
* 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
|
* 5. scan app dist and generate the static asset source
|
||||||
* 6. generate Release Manifest V2 and the build manifest
|
* 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.
|
* 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;
|
const selection = INSTALLED_RUNTIME_CAPABILITIES.serviceWorker;
|
||||||
@@ -45,10 +53,13 @@ run("node", ["scripts/generate-contract-set.ts"]);
|
|||||||
// 3. app build
|
// 3. app build
|
||||||
run("npx", ["vite", "build"]);
|
run("npx", ["vite", "build"]);
|
||||||
|
|
||||||
|
// 4. runtime config for the declared profile
|
||||||
|
run("node", ["scripts/generate-runtime-config.ts"]);
|
||||||
|
|
||||||
if (buildsActiveWorker) {
|
if (buildsActiveWorker) {
|
||||||
// 4. hashed asset inventory
|
// 5. hashed asset inventory
|
||||||
run("node", ["scripts/generate-service-worker-assets.ts", "dist"]);
|
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"]);
|
run("npx", ["vite", "build", "--config", "vite.service-worker.config.ts"]);
|
||||||
} else {
|
} else {
|
||||||
process.stdout.write(
|
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"]);
|
run("node", ["scripts/generate-build-manifest.ts"]);
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -723,9 +723,13 @@ function findArchitectureViolations(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const dependency of dependencies) {
|
for (const dependency of dependencies) {
|
||||||
|
const sourceGroups = rule.from?.path
|
||||||
|
? (new RegExp(rule.from.path, "u").exec(dependency.source)?.slice(1) ??
|
||||||
|
[])
|
||||||
|
: [];
|
||||||
if (
|
if (
|
||||||
matchesPath(dependency.source, rule.from) &&
|
matchesPath(dependency.source, rule.from) &&
|
||||||
matchesPath(dependency.target, rule.to)
|
matchesPath(dependency.target, rule.to, sourceGroups)
|
||||||
) {
|
) {
|
||||||
violations.push({
|
violations.push({
|
||||||
rule: rule.name,
|
rule: rule.name,
|
||||||
@@ -746,16 +750,52 @@ function findArchitectureViolations(
|
|||||||
function matchesPath(
|
function matchesPath(
|
||||||
modulePath: string,
|
modulePath: string,
|
||||||
criterion: PathRule | undefined,
|
criterion: PathRule | undefined,
|
||||||
|
sourceGroups: readonly string[] = [],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!criterion) return true;
|
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 false;
|
||||||
}
|
}
|
||||||
return !(
|
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 {
|
function validateArchitectureRules(rules: readonly ArchitectureRule[]): void {
|
||||||
if (!rules.some((rule) => rule.to?.circular === true)) {
|
if (!rules.some((rule) => rule.to?.circular === true)) {
|
||||||
throw new Error("Architecture configuration must contain a circular rule");
|
throw new Error("Architecture configuration must contain a circular rule");
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ if (!architecture?.evidenceArtifactIds.some((id) => index.artifacts.get(id)?.pat
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expectedGateIds = Array.from(
|
const expectedGateIds = Array.from(
|
||||||
{ length: 26 },
|
{ length: 27 },
|
||||||
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
||||||
);
|
);
|
||||||
const passingResults: Record<string, GateResult> = Object.fromEntries(
|
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.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`);
|
||||||
process.exit(1);
|
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
Reference in New Issue
Block a user