Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec7f20e2ee | ||
|
|
5cc41467ae | ||
|
|
c10a709f2c | ||
|
|
a91e78f035 | ||
|
|
217c1dd52a | ||
|
|
3366a81f0f | ||
|
|
bb6080bb1c | ||
|
|
35f3c9ce23 | ||
|
|
9c0124ef08 | ||
|
|
cb62bfb9a9 | ||
|
|
0f157494cd | ||
|
|
d777fb84b0 | ||
|
|
5d9ad4f74a | ||
|
|
7756856bb2 | ||
|
|
eb40bc9611 | ||
|
|
27ab17d6d6 | ||
|
|
1606d9b0c1 | ||
|
|
b29a4717da | ||
|
|
a42d96185f | ||
|
|
5434760ddf | ||
|
|
9ca5c3f668 | ||
|
|
711d61e73f | ||
|
|
0a97d235e4 | ||
|
|
b23a1b80ca | ||
|
|
10a04d3695 | ||
|
|
3ea3397691 | ||
|
|
7485cd86e4 | ||
|
|
dfb7734674 | ||
|
|
a0fbafb77b | ||
|
|
8157ad4029 | ||
|
|
d5e7f4127a | ||
|
|
d7b35cfca3 | ||
|
|
39a4a973a8 | ||
|
|
aa8ac35600 | ||
|
|
632b230c82 | ||
|
|
df18349682 | ||
|
|
cc91fc6ae0 | ||
|
|
8d6d84bfcc | ||
|
|
5a76f95291 | ||
|
|
46e067e555 | ||
|
|
c0f53d1855 | ||
|
|
a7390e3b3a | ||
|
|
250531aa43 | ||
|
|
af7f35058b | ||
|
|
69cb7e35ca | ||
|
|
fb5b449031 | ||
|
|
efc577de63 | ||
|
|
bd90e0c983 | ||
|
|
6a8281a941 | ||
|
|
ca210d3bc5 | ||
|
|
f4bfdf0365 | ||
|
|
3b481eb4cf | ||
|
|
78f1bb273e | ||
|
|
000a2581af | ||
|
|
976c8a8da4 | ||
|
|
db52f02d73 | ||
|
|
f6098242be | ||
|
|
fce8e046ea | ||
|
|
58efe6ddbd | ||
|
|
cc4e875c2d | ||
|
|
8f67974f68 | ||
|
|
2f29ccbf1a | ||
|
|
c9e820aed5 | ||
|
|
4fe924ee0f | ||
|
|
b893d95b36 | ||
|
|
ba79060a83 | ||
|
|
618da9abf5 | ||
|
|
6d1e44f206 | ||
|
|
e06e4377ca | ||
|
|
4e87bacdf3 | ||
|
|
67cc5b6d2c | ||
|
|
f7bec8274b | ||
|
|
4dc033cf33 | ||
|
|
30ceac23c1 | ||
|
|
42ffb79997 | ||
|
|
f49d147b01 | ||
|
|
1bb2cc4a20 | ||
|
|
777ce5c9ed | ||
|
|
8565b96782 | ||
|
|
d2eb320936 | ||
|
|
e08d8c2dd8 | ||
|
|
abdd90ad5d | ||
|
|
76bf9f1aa3 | ||
|
|
5cc6b8a51c | ||
|
|
e0373de4d9 | ||
|
|
5cecbb9820 | ||
|
|
6e05a35790 | ||
|
|
8d6fbb97e9 | ||
|
|
67cd37659d | ||
|
|
5a73f7a1b5 | ||
|
|
f487823442 | ||
|
|
1b4b0c2821 | ||
|
|
92e5cace5c | ||
|
|
7c5ed80407 | ||
|
|
100a3bb6ba | ||
|
|
76d0ab0f62 | ||
|
|
d6c98489ee | ||
|
|
381d5549e2 | ||
|
|
c9f5887cac | ||
|
|
2c3cab2518 | ||
|
|
172a26b8bd | ||
|
|
990603e24a | ||
|
|
184bd98d92 | ||
|
|
d9afccdd60 | ||
|
|
15645541b7 | ||
|
|
fa2f699125 | ||
|
|
cbcc7b5ed7 | ||
|
|
53d181fbe4 | ||
|
|
0eb23875cb | ||
|
|
73a50426d6 | ||
|
|
853c2e3f30 | ||
|
|
92c3d438ab | ||
|
|
a49c76b5b2 |
@@ -156,6 +156,54 @@
|
||||
"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": "adapter-groups-are-reached-through-their-barrel",
|
||||
"comment": "어댑터 그룹의 공개 표면은 그 그룹의 index.ts다. 그룹 바깥(bootstrap, features, presentation)은 배럴만 import한다. 배럴이 없던 시절 bootstrap은 어댑터 내부 파일 15곳을 직접 겨눴고, 그래서 어떤 파일이 공개이고 어떤 파일이 내부 헬퍼인지 아무 데도 적혀 있지 않았다. 출발점에서 src/adapters를 뺀 이유는 어댑터끼리의 간선은 바로 위 adapters-do-not-know-other-concrete-adapters가 이미 담당하고, 커널(platform/**)은 파일 단위로 공유되기 때문이다 — scripts/check-adapter-inventory.ts가 네 소비자에게 platform/abortable-operation.ts로 해석되는 specifier를 직접 요구한다. 도착점에서 1단계 중첩 index.ts를 허용한 이유는 storage/indexeddb와 storage/opfs가 각자 독립적으로 제거 가능한 런타임이고(scripts/test-browser-file-storage-runtime-removal.ts), 그래서 각자의 배럴이 곧 경계이기 때문이다.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/",
|
||||
"pathNot": "^src/adapters/"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/[^/]+/",
|
||||
"pathNot": "^src/adapters/[^/]+/index\\.ts$|^src/adapters/[^/]+/[^/]+/index\\.ts$"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no-circular-dependencies",
|
||||
"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
|
||||
@@ -1,3 +1,4 @@
|
||||
# GENERATED FILE — edit config/ci/gates.json and run `corepack pnpm generate:ci-workflow`.
|
||||
name: frontend-quality-gates
|
||||
|
||||
on:
|
||||
@@ -31,7 +32,7 @@ env:
|
||||
|
||||
jobs:
|
||||
merge_gate:
|
||||
name: ${{ matrix.gate }} / ${{ matrix.name }}
|
||||
name: "${{ matrix.gate }} / ${{ matrix.name }}"
|
||||
if: ${{ gitea.event_name != 'workflow_dispatch' || inputs.stage != 'documentation' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
@@ -51,71 +52,323 @@ jobs:
|
||||
- { gate: FE-GATE-010, name: architecture, browser: false }
|
||||
- { gate: FE-GATE-011, name: build, browser: false }
|
||||
- { gate: FE-GATE-013, name: security, browser: false }
|
||||
- { gate: FE-GATE-020, name: sample-removal, browser: false }
|
||||
- { gate: FE-GATE-020, name: removability, browser: false }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Install Playwright browsers
|
||||
if: ${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||
- name: Upload gate evidence
|
||||
- name: Upload merge gate evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: ${{ matrix.gate }}-${{ gitea.run_id }}
|
||||
name: "${{ matrix.gate }}-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
release_gate:
|
||||
name: ${{ matrix.gate }} / ${{ matrix.name }}
|
||||
name: "${{ matrix.gate }} / ${{ matrix.name }}"
|
||||
needs: merge_gate
|
||||
if: ${{ startsWith(gitea.ref, 'refs/tags/v') || (gitea.event_name == 'workflow_dispatch' && (inputs.stage == 'release' || inputs.stage == 'production' || inputs.stage == 'field')) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
HOSTING_BASE_URL: ${{ vars.HOSTING_BASE_URL }}
|
||||
HOSTING_BASE_URL: "${{ vars.HOSTING_BASE_URL }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { gate: FE-GATE-012, name: bundle, browser: false }
|
||||
- { gate: FE-GATE-014, name: config-compatibility, browser: false }
|
||||
- { gate: FE-GATE-015, name: release-coherence, browser: false }
|
||||
- { gate: FE-GATE-019, name: hosting-header, browser: false }
|
||||
- { gate: FE-GATE-026, name: lab-performance, browser: true }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Install Playwright browsers
|
||||
if: ${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||
- name: Upload gate evidence
|
||||
- name: Upload release gate evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: ${{ matrix.gate }}-${{ gitea.run_id }}
|
||||
name: "${{ matrix.gate }}-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
production_gate:
|
||||
name: ${{ matrix.gate }} / ${{ matrix.name }}
|
||||
immutable_build:
|
||||
name: "FE-GATE-015 / immutable-release-candidate"
|
||||
needs: release_gate
|
||||
if: ${{ startsWith(gitea.ref, 'refs/tags/v') || (gitea.event_name == 'workflow_dispatch' && (inputs.stage == 'release' || inputs.stage == 'production' || inputs.stage == 'field')) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
dist_sha256: ${{ steps.candidate.outputs.dist_sha256 }}
|
||||
archive_sha256: ${{ steps.candidate.outputs.archive_sha256 }}
|
||||
env:
|
||||
APP_PROFILE: "${{ vars.APP_PROFILE }}"
|
||||
RELEASE_TARGET: "${{ vars.RELEASE_TARGET }}"
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Build candidate once and verify local evidence
|
||||
run: corepack pnpm ci:gate -- FE-GATE-015
|
||||
- name: Archive and validate the exact candidate file set
|
||||
id: candidate
|
||||
run: |
|
||||
mkdir -p .release
|
||||
tar --sort=name --mtime="@0" --owner=0 --group=0 --numeric-owner -czf ".release/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" \
|
||||
dist \
|
||||
pnpm-lock.yaml \
|
||||
artifacts/performance/bundle.json \
|
||||
artifacts/quality/vite-module-inventory.json \
|
||||
artifacts/release/build-manifest.json \
|
||||
artifacts/release/checksums.txt \
|
||||
artifacts/release/dependency-inventory.json \
|
||||
artifacts/release/provenance.json \
|
||||
artifacts/release/verification.json \
|
||||
artifacts/release/sbom.cdx.json \
|
||||
artifacts/security/dependency-diff.json \
|
||||
artifacts/security/license-report.json \
|
||||
artifacts/security/local-evidence-assessment.json \
|
||||
artifacts/security/scan.sarif \
|
||||
artifacts/security/supply-chain-coherence.json \
|
||||
artifacts/security/supply-chain-verification.json \
|
||||
artifacts/security/vulnerability-report.json \
|
||||
config/security/dependency-baseline.approval.json \
|
||||
config/security/dependency-baseline.json \
|
||||
config/security/dependency-change-evidence.json \
|
||||
config/security/dependency-policy.json \
|
||||
config/security/secret-scan-policy.json \
|
||||
config/security/vulnerability-exceptions.json \
|
||||
config/security/vulnerability-policy.json \
|
||||
schemas/artifacts/build-manifest.schema.json \
|
||||
schemas/artifacts/dependency-inventory.schema.json \
|
||||
schemas/artifacts/supply-chain-verification.schema.json \
|
||||
scripts/contracts/release-artifacts.ts \
|
||||
scripts/create-release-candidate.ts \
|
||||
scripts/generate-supply-chain.ts \
|
||||
scripts/lib/build-manifest-outputs.ts \
|
||||
scripts/lib/json-schema.ts \
|
||||
scripts/lib/local-policy-evidence.ts \
|
||||
scripts/lib/local-release-evidence.ts \
|
||||
scripts/lib/release-candidate.ts \
|
||||
scripts/lib/release-input-evidence.ts \
|
||||
scripts/lib/release-runtime-coherence.ts \
|
||||
scripts/lib/repository-file-inventory.ts \
|
||||
scripts/lib/secret-scan-evaluator.ts \
|
||||
scripts/lib/secret-scan-policy.ts \
|
||||
scripts/lib/secret-scan.ts \
|
||||
scripts/lib/supply-chain.ts \
|
||||
scripts/lib/validated-json-artifact.ts \
|
||||
src/contracts/release-artifacts.ts \
|
||||
src/features/installed-contract-contributions.ts \
|
||||
src/features/installed-feature-contracts.ts \
|
||||
artifacts/release/release-candidate.json
|
||||
node scripts/verify-ci-candidate-archive.ts --archive ".release/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" --github-output "$GITHUB_OUTPUT"
|
||||
- name: Upload release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: ".release/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz"
|
||||
if-no-files-found: error
|
||||
|
||||
vulnerability_provider:
|
||||
name: external-vulnerability-provider
|
||||
needs: immutable_build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
invocation_nonce: ${{ steps.supervise_vulnerability.outputs.invocation_nonce }}
|
||||
env:
|
||||
CANDIDATE_ARCHIVE_SHA256: "${{ needs.immutable_build.outputs.archive_sha256 }}"
|
||||
CANDIDATE_ARCHIVE_PATH: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz"
|
||||
CI_RUN_ID: "${{ gitea.run_id }}"
|
||||
CI_RUN_ATTEMPT: "${{ gitea.run_attempt }}"
|
||||
EXPECTED_SOURCE_REVISION: "${{ gitea.sha }}"
|
||||
VULNERABILITY_PUBLIC_KEY_PATH: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}"
|
||||
VULNERABILITY_KEY_ID: "${{ vars.VULNERABILITY_KEY_ID }}"
|
||||
VULNERABILITY_PROVIDER_COMMAND: "${{ vars.VULNERABILITY_PROVIDER_COMMAND }}"
|
||||
VULNERABILITY_REPORT_PATH: provider-evidence/untrusted/vulnerability-report.json
|
||||
VALIDATED_PROVIDER_REPORT_PATH: provider-evidence/vulnerability-report.json
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
name: "release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: .release/vulnerability-candidate
|
||||
- name: Run and validate external vulnerability provider in one trusted supervisor
|
||||
id: supervise_vulnerability
|
||||
run: node scripts/run-and-validate-provider.ts --kind vulnerability
|
||||
- name: Confirm sealed vulnerability provider evidence
|
||||
run: test -s "$VALIDATED_PROVIDER_REPORT_PATH"
|
||||
- name: Upload vulnerability provider evidence
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "vulnerability-provider-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: provider-evidence/vulnerability-report.json
|
||||
if-no-files-found: error
|
||||
|
||||
provenance_provider:
|
||||
name: external-provenance-provider
|
||||
needs: immutable_build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
invocation_nonce: ${{ steps.supervise_provenance.outputs.invocation_nonce }}
|
||||
env:
|
||||
CANDIDATE_ARCHIVE_SHA256: "${{ needs.immutable_build.outputs.archive_sha256 }}"
|
||||
CANDIDATE_ARCHIVE_PATH: ".release/provenance-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz"
|
||||
CI_RUN_ID: "${{ gitea.run_id }}"
|
||||
CI_RUN_ATTEMPT: "${{ gitea.run_attempt }}"
|
||||
EXPECTED_SOURCE_REVISION: "${{ gitea.sha }}"
|
||||
PROVENANCE_PUBLIC_KEY_PATH: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}"
|
||||
PROVENANCE_KEY_ID: "${{ vars.PROVENANCE_KEY_ID }}"
|
||||
PROVENANCE_PROVIDER_COMMAND: "${{ vars.PROVENANCE_PROVIDER_COMMAND }}"
|
||||
PROVENANCE_ATTESTATION_PATH: provider-evidence/untrusted/provenance-attestation.json
|
||||
VALIDATED_PROVIDER_REPORT_PATH: provider-evidence/provenance-attestation.json
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
name: "release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: .release/provenance-candidate
|
||||
- name: Run and validate external provenance provider in one trusted supervisor
|
||||
id: supervise_provenance
|
||||
run: node scripts/run-and-validate-provider.ts --kind provenance
|
||||
- name: Confirm sealed provenance provider evidence
|
||||
run: test -s "$VALIDATED_PROVIDER_REPORT_PATH"
|
||||
- name: Upload provenance provider evidence
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "provenance-provider-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: provider-evidence/provenance-attestation.json
|
||||
if-no-files-found: error
|
||||
|
||||
promotion:
|
||||
name: promote-verified-immutable-candidate
|
||||
needs: [immutable_build, vulnerability_provider, provenance_provider]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
CANDIDATE_ARCHIVE_SHA256: "${{ needs.immutable_build.outputs.archive_sha256 }}"
|
||||
CANDIDATE_ARCHIVE_PATH: ".release/candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz"
|
||||
CI_RUN_ID: "${{ gitea.run_id }}"
|
||||
CI_RUN_ATTEMPT: "${{ gitea.run_attempt }}"
|
||||
VULNERABILITY_REPORT_PATH: "${{ gitea.workspace }}/.release/vulnerability/vulnerability-report.json"
|
||||
PROVENANCE_ATTESTATION_PATH: "${{ gitea.workspace }}/.release/provenance/provenance-attestation.json"
|
||||
VULNERABILITY_PUBLIC_KEY_PATH: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}"
|
||||
VULNERABILITY_KEY_ID: "${{ vars.VULNERABILITY_KEY_ID }}"
|
||||
PROVENANCE_PUBLIC_KEY_PATH: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}"
|
||||
PROVENANCE_KEY_ID: "${{ vars.PROVENANCE_KEY_ID }}"
|
||||
VULNERABILITY_INVOCATION_NONCE: "${{ needs.vulnerability_provider.outputs.invocation_nonce }}"
|
||||
PROVENANCE_INVOCATION_NONCE: "${{ needs.provenance_provider.outputs.invocation_nonce }}"
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
name: "release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: .release/candidate
|
||||
- name: Download vulnerability provider evidence
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
name: "vulnerability-provider-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: .release/vulnerability
|
||||
- name: Download provenance provider evidence
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
name: "provenance-provider-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: .release/provenance
|
||||
- name: Finalize verified promotion from inode-bound captured inputs
|
||||
id: finalize
|
||||
run: node scripts/stage-verified-promotion.ts
|
||||
- name: Upload promoted release
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "promoted-release-${{ gitea.run_id }}-${{ gitea.run_attempt }}"
|
||||
path: |
|
||||
${{ steps.finalize.outputs.staging_root }}/release-candidate.tar.gz
|
||||
${{ steps.finalize.outputs.staging_root }}/vulnerability-report.json
|
||||
${{ steps.finalize.outputs.staging_root }}/provenance-attestation.json
|
||||
${{ steps.finalize.outputs.staging_root }}/provider-verification.json
|
||||
${{ steps.finalize.outputs.staging_root }}/promotion-verification.json
|
||||
if-no-files-found: error
|
||||
- name: Always remove private promotion staging
|
||||
if: always()
|
||||
env:
|
||||
PROMOTION_STAGING_ROOT: ${{ steps.finalize.outputs.staging_root }}
|
||||
PROMOTION_CLEANUP_TOKEN: ${{ steps.finalize.outputs.cleanup_token }}
|
||||
PROMOTION_RUNNER_TEMP_DEV: ${{ steps.finalize.outputs.runner_temp_dev }}
|
||||
PROMOTION_RUNNER_TEMP_INO: ${{ steps.finalize.outputs.runner_temp_ino }}
|
||||
PROMOTION_STAGING_DEV: ${{ steps.finalize.outputs.staging_dev }}
|
||||
PROMOTION_STAGING_INO: ${{ steps.finalize.outputs.staging_ino }}
|
||||
run: |
|
||||
if [ -n "$PROMOTION_STAGING_ROOT" ] && [ -n "$PROMOTION_CLEANUP_TOKEN" ] && [ -n "$PROMOTION_RUNNER_TEMP_DEV" ] && [ -n "$PROMOTION_RUNNER_TEMP_INO" ] && [ -n "$PROMOTION_STAGING_DEV" ] && [ -n "$PROMOTION_STAGING_INO" ]; then
|
||||
node scripts/cleanup-verified-promotion.ts
|
||||
fi
|
||||
|
||||
production_gate:
|
||||
name: "${{ matrix.gate }} / ${{ matrix.name }}"
|
||||
needs: promotion
|
||||
if: ${{ gitea.event_name == 'workflow_dispatch' && (inputs.stage == 'production' || inputs.stage == 'field') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
@@ -130,72 +383,78 @@ jobs:
|
||||
- { gate: FE-GATE-024, name: runbook-telemetry }
|
||||
- { gate: FE-GATE-025, name: runbook-release-rollback }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||
- name: Upload gate evidence
|
||||
- name: Upload production gate evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: ${{ matrix.gate }}-${{ gitea.run_id }}
|
||||
name: "${{ matrix.gate }}-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
field_gate:
|
||||
name: FE-GATE-018 / field-web-vitals
|
||||
name: "FE-GATE-018 / field-web-vitals"
|
||||
needs: production_gate
|
||||
if: ${{ gitea.event_name == 'workflow_dispatch' && inputs.stage == 'field' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
FIELD_WEB_VITALS_INPUT: ${{ vars.FIELD_WEB_VITALS_INPUT }}
|
||||
MIN_ELIGIBLE_SAMPLES: ${{ vars.MIN_ELIGIBLE_SAMPLES }}
|
||||
FIELD_WEB_VITALS_INPUT: "${{ vars.FIELD_WEB_VITALS_INPUT }}"
|
||||
MIN_ELIGIBLE_SAMPLES: "${{ vars.MIN_ELIGIBLE_SAMPLES }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-018
|
||||
- name: Upload gate evidence
|
||||
- name: Upload field gate evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: FE-GATE-018-${{ gitea.run_id }}
|
||||
name: "FE-GATE-018-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
documentation_gate:
|
||||
name: FE-GATE-017 / diagram-review
|
||||
name: "FE-GATE-017 / diagram-review"
|
||||
if: ${{ gitea.event_name == 'workflow_dispatch' && inputs.stage == 'documentation' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run documentation gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-017
|
||||
- name: Upload gate evidence
|
||||
- name: Upload documentation gate evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: FE-GATE-017-${{ gitea.run_id }}
|
||||
name: "FE-GATE-017-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -18,3 +18,9 @@ artifacts/storybook/
|
||||
artifacts/tests/storybook/
|
||||
artifacts/tests/visual/
|
||||
!artifacts/**/.gitkeep
|
||||
|
||||
# Local environment overrides. `.env.example` is the tracked template; every
|
||||
# other `.env*` file is a developer's own machine and never enters the repo.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
+17
-2
@@ -2,14 +2,19 @@ import type { Preview } from "@storybook/react-vite";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
import { createAnonymousSessionAdapter } from "../src/adapters/auth/external-session-adapter.ts";
|
||||
import { createQueryClient } from "../src/adapters/query-cache/tanstack-query-cache.ts";
|
||||
import { createAnonymousSessionAdapter } from "../src/adapters/auth/index.ts";
|
||||
import { createQueryClient } from "../src/adapters/query-cache/index.ts";
|
||||
import { createApplication } from "../src/application/create-application.ts";
|
||||
import { LocaleProvider } from "../src/presentation/i18n/index.ts";
|
||||
import { ApplicationProvider } from "../src/presentation/providers/application-provider.tsx";
|
||||
import { SessionProvider } from "../src/presentation/providers/session-provider.tsx";
|
||||
import { ThemeProvider } from "../src/presentation/providers/theme-provider.tsx";
|
||||
import "../src/presentation/styles/theme.css";
|
||||
import { resolveProductFeatures } from "../src/contracts/product-features.ts";
|
||||
import {
|
||||
COMPILED_PRODUCT_FEATURE_IDS,
|
||||
INSTALLED_PRODUCT_FEATURE_IDS,
|
||||
} from "../src/features/installed-product-manifest.ts";
|
||||
|
||||
const preferences = new Map<string, unknown>();
|
||||
const application = createApplication({
|
||||
@@ -45,6 +50,16 @@ const application = createApplication({
|
||||
routeChunks: {},
|
||||
}),
|
||||
},
|
||||
// Storybook renders components, not a product: every declared feature is
|
||||
// shown as active so a story is never blank because of a deployment switch.
|
||||
productFeatures: {
|
||||
getSnapshot: () =>
|
||||
resolveProductFeatures(
|
||||
COMPILED_PRODUCT_FEATURE_IDS,
|
||||
INSTALLED_PRODUCT_FEATURE_IDS,
|
||||
),
|
||||
isActive: () => true,
|
||||
},
|
||||
runtimeCapabilities: {
|
||||
getSnapshot: () =>
|
||||
Object.freeze(
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Task 5 Report: Retain and reconcile uncertain optimistic mutations
|
||||
|
||||
## Status
|
||||
|
||||
Task 5 is implemented. Mutation settlement now follows explicit effect certainty, preserves unknown optimistic projections as ordered uncertain layers, and exposes one-at-a-time reconciliation bound to the original mutation record. Missing post-dispatch certainty is fail-safe `MAYBE_APPLIED`; only controller-owned pre-dispatch failures are marked `NOT_STARTED`.
|
||||
|
||||
## RED evidence
|
||||
|
||||
- Initial focused command: `corepack pnpm exec vitest run tests/unit/optimistic-layer-runtime.test.ts tests/component/application-query.test.tsx`.
|
||||
- Initial result: exit `1`, 2 files, 11 failed / 28 passed. Missing lease/controller APIs failed directly; `APPLIED_CONFIRMED` and `MAYBE_APPLIED` were rolled back; effectless failures retained generic retry semantics.
|
||||
- Review-driven RED: the three-file focused command including `tests/component/async-surface.test.tsx` exited `1` with 7 failed / 56 passed. It exposed applied-confirmed retry actions, active-submit reset, double reconciliation, stale-scope queue retention, overlay priority, and the incorrect refreshing copy.
|
||||
- A final isolated RED proved a synchronous `NOT_APPLIED` double action could consume two FIFO records in one event turn.
|
||||
- The production-form RED exited `1` with 2 failed / 8 passed: applied reconciliation left the original command dirty/retryable, while an `APPLIED_CONFIRMED` failure rendered generic unavailable.
|
||||
- The final durability/lifecycle RED failed 2 / 2: an anonymous non-optimistic legacy channel did not survive remount, and render-time registry allocation exhausted the definition cap during an abandoned server render.
|
||||
|
||||
## Implementation
|
||||
|
||||
1. `OptimisticLayerLease` now supports `markUncertain()` and `reconcile("APPLIED" | "NOT_APPLIED")`. Layers are `pending | uncertain | committed`; only a committed prefix collapses into the base, while projection continues to apply every later layer in order.
|
||||
2. Reconciliation is single-settlement and idempotent. `APPLIED` converts the uncertain layer to committed; `NOT_APPLIED` removes only that layer; both then collapse/reproject later committed or pending layers. Scope expiry removes stale cache instead of restoring it.
|
||||
3. Legacy optimistic mutations use the same reusable always-current ordered runtime. This prevents an old manual snapshot from erasing a later successful mutation or authoritative projection. The runtime also supports optimistic entries whose base data was absent and removes them on a not-applied rollback.
|
||||
4. The mutation bridge derives effect before touching optimistic state:
|
||||
- `NOT_STARTED` / `NOT_APPLIED`: rollback;
|
||||
- `APPLIED_CONFIRMED`: commit, then best-effort invalidate;
|
||||
- `MAYBE_APPLIED`: retain as uncertain, do not invalidate, and enqueue explicit reconciliation.
|
||||
5. Missing or `NOT_APPLICABLE` command effects, returned failures after dispatch, and thrown execution failures normalize to `MAYBE_APPLIED`. Unknown effects are non-retryable with `contact-support`; applied-confirmed failures are non-retryable with no resend action. Controller-owned stale scope, duplicate admission, identity/preparation, and other known pre-dispatch failures carry `NOT_STARTED`.
|
||||
6. Unknown records retain their original intent, scope, layer lease, invalidation topics, and coordinator. A FIFO queue prevents parallel `ALLOW_PARALLEL` failures from overwriting each other. Reconciliation is locked through the event turn so a double action cannot consume the next intent, and it does not reset a newer active submit.
|
||||
7. Scope abort discards every queued record from that scope, settles only its local stale layers, performs no invalidation, and cannot later overwrite new-generation cache data.
|
||||
8. Async state adds the mutually exclusive `mutation-effect-unknown` overlay with priority `unknown > conflict > pending > stale-degraded > refreshing`. `AsyncSurface` uses dedicated safe copy and only `APPLIED` / `NOT_APPLIED` actions; it does not expose generic retry or mark the surface busy.
|
||||
9. Unknown-effect admissions live in a bounded QueryClient-owned registry, so bound and legacy controllers can remount without losing reconciliation state. Channels include definition version and generation, validate the exact scope owner, count active admissions globally in O(1), release after late settlement, and preserve FIFO order even when executions finish in reverse.
|
||||
10. Channel creation and scope-abort listener registration occur only in a committed React effect. An abandoned/server render performs no registry mutation and consumes no channel capacity. Non-optimistic legacy callers must provide a stable `definitionId`; optimistic legacy callers also include their query identity.
|
||||
11. The reference create form blocks all generic resubmission while effect certainty is unknown. `NOT_APPLIED` preserves input and re-enables submission; `APPLIED` reconciliation and `APPLIED_CONFIRMED` settlement use the form's success-equivalent reset path so the same create command cannot be resent.
|
||||
|
||||
## Test coverage
|
||||
|
||||
- Certainty matrix for all four mutation effects, missing effect, `NOT_APPLICABLE`, thrown execution, ambiguous conflicts, and non-retry semantics.
|
||||
- Applied-confirmed commit-before-invalidate ordering and retained commit when invalidation fails.
|
||||
- Out-of-order later commits behind uncertain layers; both reconciliation outcomes; duplicate/reversed lease transitions; external cache projection; expired scope.
|
||||
- Bound and legacy reconciliation, no-layer legacy fallback, parallel unknown queues, same-turn double actions, active newer submit preservation, scope-wide stale cleanup, and no-prior-cache rollback.
|
||||
- Bound and non-optimistic legacy remount durability, unrelated legacy isolation, generation isolation, exact scope-owner collision handling, abandoned-render capacity, late empty-channel cleanup, QueryClient-global admission bounds, reverse completion, and fence-during-invalidation races.
|
||||
- Production create-form coverage for the `MAYBE_APPLIED` block, `NOT_APPLIED` input preservation, and success-equivalent `APPLIED` / `APPLIED_CONFIRMED` settlement.
|
||||
- Unknown overlay derivation, mutual-exclusion priority, dedicated localized copy, non-busy state, and reconciliation-only actions.
|
||||
- The negative async-overlay type fixture now includes `mutationEffectUnknown: false`, so it continues to fail for the intended pending/conflict exclusivity violation.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `src/presentation/adapters/query/optimistic-layer-runtime.ts`
|
||||
- `src/presentation/adapters/query/application-query.ts`
|
||||
- `src/application/view-models/async-state.ts`
|
||||
- `src/contracts/errors.ts`
|
||||
- `src/presentation/components/async-surface.tsx`
|
||||
- `src/presentation/forms/form-contracts.ts`
|
||||
- `src/presentation/forms/use-app-form.ts`
|
||||
- `src/presentation/i18n/catalog.ts`
|
||||
- `src/features/reference-feature/presentation/reference-resource-form-page.tsx`
|
||||
- `tests/unit/optimistic-layer-runtime.test.ts`
|
||||
- `tests/component/application-query.test.tsx`
|
||||
- `tests/component/async-surface.test.tsx`
|
||||
- `tests/features/reference-feature/reference-page.test.tsx`
|
||||
- `tests/fixtures/typecheck/invalid-async-overlay.ts`
|
||||
|
||||
The AsyncSurface, catalog, UI test, and type-fixture additions are a narrow scope expansion required to avoid rendering the new indicator as a background refresh and to preserve the overlay type contract.
|
||||
|
||||
## Verification
|
||||
|
||||
- Final focused command (error classification, optimistic runtime, mutation bridge, async surface, form facade, and production reference form): 6 files / 100 tests — PASS.
|
||||
- `corepack pnpm check:types` — PASS for app, node, test, recipes, web worker, and service worker.
|
||||
- `corepack pnpm lint` — PASS with zero warnings.
|
||||
- `corepack pnpm test:all` — PASS: runtime schema 40, unit 741, component 123, integration 23, reference feature 24, recipes 17.
|
||||
- `git diff --check` — PASS.
|
||||
- `corepack pnpm run check:types:fixture:async-overlay` — expected non-zero; TypeScript rejects `mutationConflict: true` when `mutationPending: true`, confirming the negative fixture still reaches its intended invariant.
|
||||
|
||||
## Self-review decisions
|
||||
|
||||
- The plan-prescribed `reconcileUnknownEffect(resolution)` API remains intact. Rather than introduce a public token incompatible with that interface, the controller retains intent-bound FIFO records and serializes reconciliation through the current event turn. A repeated action after the first promise settles is an explicit action on the next visible unknown record.
|
||||
- Scope cleanup removes stale local projection without claiming or invalidating a server outcome. A stale generation cannot use its former record after the queue is discarded.
|
||||
- Legacy manual snapshot restoration was removed because it could erase later successful work. Shared ordered layers are the minimal mechanism that gives legacy and bound mutations the same re-projection guarantees.
|
||||
- A non-optimistic legacy mutation has no cache key from which a durable logical identity can be inferred. Its type contract therefore requires a stable caller-supplied `definitionId`; this preserves remount durability without merging unrelated controllers.
|
||||
- Registry mutation was moved out of render into the committed effect lifecycle. The server-render regression fills the nominal definition count with abandoned renders, then proves a committed mutation can still acquire and execute.
|
||||
- Browser/Playwright gates were not run; this task changed no browser-only integration. The jsdom component tests cover the new accessible status and actions.
|
||||
|
||||
## Final review
|
||||
|
||||
The scoped reviewer completed two fix rounds covering durable ownership, FIFO/races, global bounds, scope fences, and production form settlement. The final verdict reported no findings, independently passed 4 files / 84 tests, confirmed `git diff --check`, and assessed the change ready to merge.
|
||||
|
||||
## Runtime final-review fix round 3
|
||||
|
||||
The runtime-wide final review identified three additional Task 5 authorities. This round addresses only those findings; the provider-neutral HTTP operation port remains deferred to its separately owned remediation plans.
|
||||
|
||||
### RED evidence
|
||||
|
||||
- Composite optimistic admission: `tests/unit/optimistic-layer-runtime.test.ts` failed 2 / 8 cases because a base-valid candidate that threw only after the prior layer returned a lease and orphaned both rollback and reconciliation authority.
|
||||
- Candidate replay: the isolated admission test failed with candidate updater call count `2` instead of `1`; replay through `project()` could still delete the entry after successful preflight.
|
||||
- Form reconciliation: `tests/component/form-foundation.test.tsx` failed 3 / 8 cases. The hook admitted a second submit during unknown effect, settled edited value B instead of submitted snapshot A, and exposed no explicit not-applied release authority.
|
||||
- Production namespace parity: the mounted reference-page regression failed because `REFERENCE_RESOURCE_QUERY_NAMESPACE` was not exported; production list/detail hooks could only duplicate its id/version literals.
|
||||
|
||||
### Implementation
|
||||
|
||||
1. `OptimisticLayerRuntime.begin()` now computes the complete ordered projection before admission. A composite failure returns pessimistic fallback `null` without changing the existing entry, cache projection, layer IDs, or earlier lease authority. The admitted candidate is written from that precomputed value, so its updater runs exactly once during admission.
|
||||
2. `useAppForm` retains the exact parsed values for a `MAYBE_APPLIED` submission. The ref is the hook-level admission lock until `settleApplied`, `settleNotApplied`, or `reset` releases it; later edits preserve the unknown result and cannot trigger another command. With `resetOnSuccess: false`, APPLIED makes submitted A the baseline while edited B remains dirty. Success, applied-confirmed, validation/conflict/unavailable outcomes, reset, and explicit not-applied settlement clear the retained snapshot.
|
||||
3. The reference form routes both reconciliation outcomes into the corresponding form settlement authority.
|
||||
4. `REFERENCE_RESOURCE_QUERY_NAMESPACE` is exported from the governed feature contract. Both production list and detail query definitions consume its fields, while the mounted-key regression compares both real query prefixes with the installed invalidation edge.
|
||||
|
||||
### Verification
|
||||
|
||||
- Focused runtime/form/reference command: 5 files / 87 tests — PASS.
|
||||
- `corepack pnpm check:types` — PASS for app, node, test, recipes, web worker, and service worker.
|
||||
- `corepack pnpm lint` — PASS with zero warnings.
|
||||
- `corepack pnpm test:all` — PASS: runtime schema 40, unit 744, component 126, integration 23, reference feature 25, recipes 17.
|
||||
- `git diff --check` — PASS.
|
||||
- Scoped re-review by the existing Task 5 reviewer: no findings, ready to merge. The reviewer independently passed the 5-file scoped suite (96 / 96), confirmed `git diff --check`, verified all three reconciliation authorities plus candidate single-invocation, and confirmed the deferred HTTP adapter remained untouched.
|
||||
@@ -47,21 +47,28 @@ authoritative.
|
||||
|
||||
## Architecture
|
||||
|
||||
This repository is a **Frontend Application Foundation**: a starter/composition
|
||||
skeleton plus a reusable capability platform. Platform capabilities stay
|
||||
horizontal while product business features use vertical slices.
|
||||
|
||||
Dependencies point inward:
|
||||
|
||||
```text
|
||||
presentation -> application -> domain
|
||||
adapters -----^
|
||||
bootstrap composes concrete adapters
|
||||
contracts own cross-cutting registries
|
||||
feature use case -> feature port <- feature-owned capability binding
|
||||
```
|
||||
|
||||
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||
See
|
||||
`docs/architecture/frontend-application-foundation.md`,
|
||||
`docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||
`docs/architecture/starter-experience.md`. The removable vertical slice is
|
||||
under `src/features/reference-feature`; its domain, application input, HTTP
|
||||
adapter, contracts, route runtime, and presentation are installed through the
|
||||
feature contribution files in `src/features`. The generic starter routes
|
||||
continue to typecheck, test, and build after that contribution is removed.
|
||||
binding, contracts, route runtime, and presentation own their contributions.
|
||||
The central installed catalogs only aggregate selected contributions. The
|
||||
generic starter routes continue to typecheck, test, and build after that
|
||||
feature is removed.
|
||||
|
||||
### Platform capability review
|
||||
|
||||
@@ -103,6 +110,7 @@ corepack pnpm check:types:node
|
||||
corepack pnpm check:types:test
|
||||
corepack pnpm check:architecture
|
||||
corepack pnpm test:all
|
||||
corepack pnpm test:contract
|
||||
corepack pnpm test:e2e
|
||||
corepack pnpm test:a11y
|
||||
corepack pnpm build
|
||||
@@ -115,6 +123,11 @@ corepack pnpm drill:runbooks
|
||||
corepack pnpm check:ci
|
||||
```
|
||||
|
||||
`test:all` is the ordinary product-development loop and intentionally excludes
|
||||
host-level CI-runner assurance. Run `corepack pnpm test:system` only on the
|
||||
compatible Linux assurance host described in
|
||||
[`docs/testing/taxonomy.md`](docs/testing/taxonomy.md).
|
||||
|
||||
`check:types`는 source, Node scripts/config와 tests를 분리된 TypeScript
|
||||
project로 모두 검사한다. type/architecture/security/registry의 invalid
|
||||
fixture는 `config/ci/gates.json`에서 “실패해야 통과”하는 negative gate로
|
||||
@@ -140,7 +153,12 @@ corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
Two gates intentionally need external evidence:
|
||||
|
||||
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
|
||||
for all six registered routes.
|
||||
for all ten registered routes: `APP_HOME`, `EXAMPLES_PLATFORM`,
|
||||
`EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`, `NOT_FOUND`,
|
||||
`REFERENCE_RESOURCE_LIST`, `REFERENCE_RESOURCE_DETAIL`,
|
||||
`REFERENCE_RESOURCE_FORM` and `REFERENCE_RESOURCE_STATUS`.
|
||||
`verify:documentation` derives that list from the route registry and fails if
|
||||
this paragraph falls behind it.
|
||||
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
||||
eligible-sample threshold and 28 days of production data exist.
|
||||
|
||||
@@ -148,7 +166,7 @@ Live release verification additionally requires `HOSTING_BASE_URL`.
|
||||
|
||||
## CI and evidence
|
||||
|
||||
The 26-gate registry is `config/ci/gates.json`; the Gitea workflow is
|
||||
The 27-gate registry is `config/ci/gates.json`; the Gitea workflow is
|
||||
`.gitea/workflows/quality-gates.yml`. It follows:
|
||||
|
||||
```text
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
body, html {
|
||||
margin:0; padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, Arial;
|
||||
font-size: 14px;
|
||||
color:#333;
|
||||
}
|
||||
.small { font-size: 12px; }
|
||||
*, *:after, *:before {
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing:border-box;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
h1 { font-size: 20px; margin: 0;}
|
||||
h2 { font-size: 14px; }
|
||||
pre {
|
||||
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
}
|
||||
a { color:#0074D9; text-decoration:none; }
|
||||
a:hover { text-decoration:underline; }
|
||||
.strong { font-weight: bold; }
|
||||
.space-top1 { padding: 10px 0 0 0; }
|
||||
.pad2y { padding: 20px 0; }
|
||||
.pad1y { padding: 10px 0; }
|
||||
.pad2x { padding: 0 20px; }
|
||||
.pad2 { padding: 20px; }
|
||||
.pad1 { padding: 10px; }
|
||||
.space-left2 { padding-left:55px; }
|
||||
.space-right2 { padding-right:20px; }
|
||||
.center { text-align:center; }
|
||||
.clearfix { display:block; }
|
||||
.clearfix:after {
|
||||
content:'';
|
||||
display:block;
|
||||
height:0;
|
||||
clear:both;
|
||||
visibility:hidden;
|
||||
}
|
||||
.fl { float: left; }
|
||||
@media only screen and (max-width:640px) {
|
||||
.col3 { width:100%; max-width:100%; }
|
||||
.hide-mobile { display:none!important; }
|
||||
}
|
||||
|
||||
.quiet {
|
||||
color: #7f7f7f;
|
||||
color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.quiet a { opacity: 0.7; }
|
||||
|
||||
.fraction {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
background: #E8E8E8;
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.path a:link, div.path a:visited { color: #333; }
|
||||
table.coverage {
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.coverage td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.coverage td.line-count {
|
||||
text-align: right;
|
||||
padding: 0 5px 0 20px;
|
||||
}
|
||||
table.coverage td.line-coverage {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
min-width:20px;
|
||||
}
|
||||
|
||||
table.coverage td span.cline-any {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
}
|
||||
.missing-if-branch {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #333;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.skip-if-branch {
|
||||
display: none;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #ccc;
|
||||
color: white;
|
||||
}
|
||||
.missing-if-branch .typ, .skip-if-branch .typ {
|
||||
color: inherit !important;
|
||||
}
|
||||
.coverage-summary {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
||||
.keyline-all { border: 1px solid #ddd; }
|
||||
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
||||
.coverage-summary tbody { border: 1px solid #bbb; }
|
||||
.coverage-summary td { border-right: 1px solid #bbb; }
|
||||
.coverage-summary td:last-child { border-right: none; }
|
||||
.coverage-summary th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.coverage-summary th.file { border-right: none !important; }
|
||||
.coverage-summary th.pct { }
|
||||
.coverage-summary th.pic,
|
||||
.coverage-summary th.abs,
|
||||
.coverage-summary td.pct,
|
||||
.coverage-summary td.abs { text-align: right; }
|
||||
.coverage-summary td.file { white-space: nowrap; }
|
||||
.coverage-summary td.pic { min-width: 120px !important; }
|
||||
.coverage-summary tfoot td { }
|
||||
|
||||
.coverage-summary .sorter {
|
||||
height: 10px;
|
||||
width: 7px;
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
||||
}
|
||||
.coverage-summary .sorted .sorter {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
.coverage-summary .sorted-desc .sorter {
|
||||
background-position: 0 -10px;
|
||||
}
|
||||
.status-line { height: 10px; }
|
||||
/* yellow */
|
||||
.cbranch-no { background: yellow !important; color: #111; }
|
||||
/* dark red */
|
||||
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
||||
.low .chart { border:1px solid #C21F39 }
|
||||
.highlighted,
|
||||
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
||||
background: #C21F39 !important;
|
||||
}
|
||||
/* medium red */
|
||||
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
||||
/* light red */
|
||||
.low, .cline-no { background:#FCE1E5 }
|
||||
/* light green */
|
||||
.high, .cline-yes { background:rgb(230,245,208) }
|
||||
/* medium green */
|
||||
.cstat-yes { background:rgb(161,215,106) }
|
||||
/* dark green */
|
||||
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
||||
.high .chart { border:1px solid rgb(77,146,33) }
|
||||
/* dark yellow (gold) */
|
||||
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
||||
.medium .chart { border:1px solid #f9cd0b; }
|
||||
/* light yellow */
|
||||
.medium { background: #fff4c2; }
|
||||
|
||||
.cstat-skip { background: #ddd; color: #111; }
|
||||
.fstat-skip { background: #ddd; color: #111 !important; }
|
||||
.cbranch-skip { background: #ddd !important; color: #111; }
|
||||
|
||||
span.cline-neutral { background: #eaeaea; }
|
||||
|
||||
.coverage-summary td.empty {
|
||||
opacity: .5;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
line-height: 1;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cover-fill, .cover-empty {
|
||||
display:inline-block;
|
||||
height: 12px;
|
||||
}
|
||||
.chart {
|
||||
line-height: 0;
|
||||
}
|
||||
.cover-empty {
|
||||
background: white;
|
||||
}
|
||||
.cover-full {
|
||||
border-right: none !important;
|
||||
}
|
||||
pre.prettyprint {
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.com { color: #999 !important; }
|
||||
.ignore-none { color: #999; font-weight: normal; }
|
||||
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -48px;
|
||||
}
|
||||
.footer, .push {
|
||||
height: 48px;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable */
|
||||
var jumpToCode = (function init() {
|
||||
// Classes of code we would like to highlight in the file view
|
||||
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
||||
|
||||
// Elements to highlight in the file listing view
|
||||
var fileListingElements = ['td.pct.low'];
|
||||
|
||||
// We don't want to select elements that are direct descendants of another match
|
||||
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
||||
|
||||
// Selector that finds elements on the page to which we can jump
|
||||
var selector =
|
||||
fileListingElements.join(', ') +
|
||||
', ' +
|
||||
notSelector +
|
||||
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
||||
|
||||
// The NodeList of matching elements
|
||||
var missingCoverageElements = document.querySelectorAll(selector);
|
||||
|
||||
var currentIndex;
|
||||
|
||||
function toggleClass(index) {
|
||||
missingCoverageElements
|
||||
.item(currentIndex)
|
||||
.classList.remove('highlighted');
|
||||
missingCoverageElements.item(index).classList.add('highlighted');
|
||||
}
|
||||
|
||||
function makeCurrent(index) {
|
||||
toggleClass(index);
|
||||
currentIndex = index;
|
||||
missingCoverageElements.item(index).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
|
||||
function goToPrevious() {
|
||||
var nextIndex = 0;
|
||||
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
||||
nextIndex = missingCoverageElements.length - 1;
|
||||
} else if (missingCoverageElements.length > 1) {
|
||||
nextIndex = currentIndex - 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
function goToNext() {
|
||||
var nextIndex = 0;
|
||||
|
||||
if (
|
||||
typeof currentIndex === 'number' &&
|
||||
currentIndex < missingCoverageElements.length - 1
|
||||
) {
|
||||
nextIndex = currentIndex + 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
return function jump(event) {
|
||||
if (
|
||||
document.getElementById('fileSearch') === document.activeElement &&
|
||||
document.activeElement != null
|
||||
) {
|
||||
// if we're currently focused on the search input, we don't want to navigate
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.which) {
|
||||
case 78: // n
|
||||
case 74: // j
|
||||
goToNext();
|
||||
break;
|
||||
case 66: // b
|
||||
case 75: // k
|
||||
case 80: // p
|
||||
goToPrevious();
|
||||
break;
|
||||
}
|
||||
};
|
||||
})();
|
||||
window.addEventListener('keydown', jumpToCode);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 445 B |
@@ -0,0 +1,116 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for All files</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="prettify.css" />
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1>All files</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>11/11</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>8/8</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>5/5</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>11/11</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line high'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td class="file high" data-value="reference-http-gateway.ts"><a href="reference-http-gateway.ts.html">reference-http-gateway.ts</a></td>
|
||||
<td data-value="100" class="pic high">
|
||||
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||
</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="11" class="abs high">11/11</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="8" class="abs high">8/8</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="5" class="abs high">5/5</td>
|
||||
<td data-value="100" class="pct high">100%</td>
|
||||
<td data-value="11" class="abs high">11/11</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-09-18T11:39:00.427Z
|
||||
</div>
|
||||
<script src="prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="sorter.js"></script>
|
||||
<script src="block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,331 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for reference-http-gateway.ts</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="prettify.css" />
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1><a href="index.html">All files</a> reference-http-gateway.ts</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>11/11</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>8/8</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>5/5</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>11/11</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line high'></div>
|
||||
<pre><table class="coverage">
|
||||
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||
<a name='L2'></a><a href='#L2'>2</a>
|
||||
<a name='L3'></a><a href='#L3'>3</a>
|
||||
<a name='L4'></a><a href='#L4'>4</a>
|
||||
<a name='L5'></a><a href='#L5'>5</a>
|
||||
<a name='L6'></a><a href='#L6'>6</a>
|
||||
<a name='L7'></a><a href='#L7'>7</a>
|
||||
<a name='L8'></a><a href='#L8'>8</a>
|
||||
<a name='L9'></a><a href='#L9'>9</a>
|
||||
<a name='L10'></a><a href='#L10'>10</a>
|
||||
<a name='L11'></a><a href='#L11'>11</a>
|
||||
<a name='L12'></a><a href='#L12'>12</a>
|
||||
<a name='L13'></a><a href='#L13'>13</a>
|
||||
<a name='L14'></a><a href='#L14'>14</a>
|
||||
<a name='L15'></a><a href='#L15'>15</a>
|
||||
<a name='L16'></a><a href='#L16'>16</a>
|
||||
<a name='L17'></a><a href='#L17'>17</a>
|
||||
<a name='L18'></a><a href='#L18'>18</a>
|
||||
<a name='L19'></a><a href='#L19'>19</a>
|
||||
<a name='L20'></a><a href='#L20'>20</a>
|
||||
<a name='L21'></a><a href='#L21'>21</a>
|
||||
<a name='L22'></a><a href='#L22'>22</a>
|
||||
<a name='L23'></a><a href='#L23'>23</a>
|
||||
<a name='L24'></a><a href='#L24'>24</a>
|
||||
<a name='L25'></a><a href='#L25'>25</a>
|
||||
<a name='L26'></a><a href='#L26'>26</a>
|
||||
<a name='L27'></a><a href='#L27'>27</a>
|
||||
<a name='L28'></a><a href='#L28'>28</a>
|
||||
<a name='L29'></a><a href='#L29'>29</a>
|
||||
<a name='L30'></a><a href='#L30'>30</a>
|
||||
<a name='L31'></a><a href='#L31'>31</a>
|
||||
<a name='L32'></a><a href='#L32'>32</a>
|
||||
<a name='L33'></a><a href='#L33'>33</a>
|
||||
<a name='L34'></a><a href='#L34'>34</a>
|
||||
<a name='L35'></a><a href='#L35'>35</a>
|
||||
<a name='L36'></a><a href='#L36'>36</a>
|
||||
<a name='L37'></a><a href='#L37'>37</a>
|
||||
<a name='L38'></a><a href='#L38'>38</a>
|
||||
<a name='L39'></a><a href='#L39'>39</a>
|
||||
<a name='L40'></a><a href='#L40'>40</a>
|
||||
<a name='L41'></a><a href='#L41'>41</a>
|
||||
<a name='L42'></a><a href='#L42'>42</a>
|
||||
<a name='L43'></a><a href='#L43'>43</a>
|
||||
<a name='L44'></a><a href='#L44'>44</a>
|
||||
<a name='L45'></a><a href='#L45'>45</a>
|
||||
<a name='L46'></a><a href='#L46'>46</a>
|
||||
<a name='L47'></a><a href='#L47'>47</a>
|
||||
<a name='L48'></a><a href='#L48'>48</a>
|
||||
<a name='L49'></a><a href='#L49'>49</a>
|
||||
<a name='L50'></a><a href='#L50'>50</a>
|
||||
<a name='L51'></a><a href='#L51'>51</a>
|
||||
<a name='L52'></a><a href='#L52'>52</a>
|
||||
<a name='L53'></a><a href='#L53'>53</a>
|
||||
<a name='L54'></a><a href='#L54'>54</a>
|
||||
<a name='L55'></a><a href='#L55'>55</a>
|
||||
<a name='L56'></a><a href='#L56'>56</a>
|
||||
<a name='L57'></a><a href='#L57'>57</a>
|
||||
<a name='L58'></a><a href='#L58'>58</a>
|
||||
<a name='L59'></a><a href='#L59'>59</a>
|
||||
<a name='L60'></a><a href='#L60'>60</a>
|
||||
<a name='L61'></a><a href='#L61'>61</a>
|
||||
<a name='L62'></a><a href='#L62'>62</a>
|
||||
<a name='L63'></a><a href='#L63'>63</a>
|
||||
<a name='L64'></a><a href='#L64'>64</a>
|
||||
<a name='L65'></a><a href='#L65'>65</a>
|
||||
<a name='L66'></a><a href='#L66'>66</a>
|
||||
<a name='L67'></a><a href='#L67'>67</a>
|
||||
<a name='L68'></a><a href='#L68'>68</a>
|
||||
<a name='L69'></a><a href='#L69'>69</a>
|
||||
<a name='L70'></a><a href='#L70'>70</a>
|
||||
<a name='L71'></a><a href='#L71'>71</a>
|
||||
<a name='L72'></a><a href='#L72'>72</a>
|
||||
<a name='L73'></a><a href='#L73'>73</a>
|
||||
<a name='L74'></a><a href='#L74'>74</a>
|
||||
<a name='L75'></a><a href='#L75'>75</a>
|
||||
<a name='L76'></a><a href='#L76'>76</a>
|
||||
<a name='L77'></a><a href='#L77'>77</a>
|
||||
<a name='L78'></a><a href='#L78'>78</a>
|
||||
<a name='L79'></a><a href='#L79'>79</a>
|
||||
<a name='L80'></a><a href='#L80'>80</a>
|
||||
<a name='L81'></a><a href='#L81'>81</a>
|
||||
<a name='L82'></a><a href='#L82'>82</a>
|
||||
<a name='L83'></a><a href='#L83'>83</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">6x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import {
|
||||
defineFeatureHttpOperationForRoutes,
|
||||
type FeatureHttpBinding,
|
||||
} from "../../../adapters/http/index.ts";
|
||||
import {
|
||||
CREATE_REFERENCE_RESOURCE_CONTRACT,
|
||||
GET_REFERENCE_RESOURCE_CONTRACT,
|
||||
LIST_REFERENCE_RESOURCES_CONTRACT,
|
||||
} from "../contracts/reference-feature-contract-contribution.ts";
|
||||
import {
|
||||
REFERENCE_FEATURE_CONTRACT,
|
||||
} from "../contracts/reference-feature-contract.ts";
|
||||
import {
|
||||
mapReferenceResourceListPayload,
|
||||
mapReferenceResourcePayload,
|
||||
} from "../contracts/reference-mapper.ts";
|
||||
import type {
|
||||
ReferenceGateway,
|
||||
} from "../application/reference-feature-api.ts";
|
||||
|
||||
export type ReferenceFeatureRouteId =
|
||||
keyof typeof REFERENCE_FEATURE_CONTRACT.routes;
|
||||
|
||||
const defineReferenceHttpOperation =
|
||||
defineFeatureHttpOperationForRoutes<ReferenceFeatureRouteId>();
|
||||
|
||||
export const REFERENCE_HTTP_OPERATIONS = Object.freeze({
|
||||
LIST_REFERENCE_RESOURCES: defineReferenceHttpOperation({
|
||||
contract: LIST_REFERENCE_RESOURCES_CONTRACT,
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
mapSuccess: mapReferenceResourceListPayload,
|
||||
}),
|
||||
CREATE_REFERENCE_RESOURCE: defineReferenceHttpOperation({
|
||||
contract: CREATE_REFERENCE_RESOURCE_CONTRACT,
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
mapSuccess: mapReferenceResourcePayload,
|
||||
mapProblem(problem, metadata) {
|
||||
if (metadata.status === 409) {
|
||||
return Object.freeze({
|
||||
kind: "CONFLICT" as const,
|
||||
code: problem.code ?? "REFERENCE_RESOURCE_CONFLICT",
|
||||
});
|
||||
}
|
||||
if (metadata.status === 422) {
|
||||
return Object.freeze({
|
||||
kind: "VALIDATION_REJECTED" as const,
|
||||
code: problem.code ?? "REFERENCE_RESOURCE_REJECTED",
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
GET_REFERENCE_RESOURCE: defineReferenceHttpOperation({
|
||||
contract: GET_REFERENCE_RESOURCE_CONTRACT,
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL",
|
||||
mapSuccess: mapReferenceResourcePayload,
|
||||
}),
|
||||
} as const);
|
||||
|
||||
export type ReferenceHttpBinding = FeatureHttpBinding<
|
||||
typeof REFERENCE_HTTP_OPERATIONS
|
||||
>;
|
||||
|
||||
export function createReferenceHttpGateway(
|
||||
http: ReferenceHttpBinding,
|
||||
): ReferenceGateway {
|
||||
return Object.freeze({
|
||||
list(filters, context) {
|
||||
return http.execute("LIST_REFERENCE_RESOURCES", filters, context);
|
||||
},
|
||||
create(command, context) {
|
||||
return http.execute("CREATE_REFERENCE_RESOURCE", command, context);
|
||||
},
|
||||
get(resourceId, context) {
|
||||
return http.execute(
|
||||
"GET_REFERENCE_RESOURCE",
|
||||
Object.freeze({ resourceId }),
|
||||
context,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
</pre></td></tr></table></pre>
|
||||
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-09-18T11:39:00.427Z
|
||||
</div>
|
||||
<script src="prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="sorter.js"></script>
|
||||
<script src="block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 138 B |
@@ -0,0 +1,210 @@
|
||||
/* eslint-disable */
|
||||
var addSorting = (function() {
|
||||
'use strict';
|
||||
var cols,
|
||||
currentSort = {
|
||||
index: 0,
|
||||
desc: false
|
||||
};
|
||||
|
||||
// returns the summary table element
|
||||
function getTable() {
|
||||
return document.querySelector('.coverage-summary');
|
||||
}
|
||||
// returns the thead element of the summary table
|
||||
function getTableHeader() {
|
||||
return getTable().querySelector('thead tr');
|
||||
}
|
||||
// returns the tbody element of the summary table
|
||||
function getTableBody() {
|
||||
return getTable().querySelector('tbody');
|
||||
}
|
||||
// returns the th element for nth column
|
||||
function getNthColumn(n) {
|
||||
return getTableHeader().querySelectorAll('th')[n];
|
||||
}
|
||||
|
||||
function onFilterInput() {
|
||||
const searchValue = document.getElementById('fileSearch').value;
|
||||
const rows = document.getElementsByTagName('tbody')[0].children;
|
||||
|
||||
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
|
||||
// it will be treated as a plain text search
|
||||
let searchRegex;
|
||||
try {
|
||||
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
|
||||
} catch (error) {
|
||||
searchRegex = null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
let isMatch = false;
|
||||
|
||||
if (searchRegex) {
|
||||
// If a valid regex was created, use it for matching
|
||||
isMatch = searchRegex.test(row.textContent);
|
||||
} else {
|
||||
// Otherwise, fall back to the original plain text search
|
||||
isMatch = row.textContent
|
||||
.toLowerCase()
|
||||
.includes(searchValue.toLowerCase());
|
||||
}
|
||||
|
||||
row.style.display = isMatch ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// loads the search box
|
||||
function addSearchBox() {
|
||||
var template = document.getElementById('filterTemplate');
|
||||
var templateClone = template.content.cloneNode(true);
|
||||
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
||||
template.parentElement.appendChild(templateClone);
|
||||
}
|
||||
|
||||
// loads all columns
|
||||
function loadColumns() {
|
||||
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||
colNode,
|
||||
cols = [],
|
||||
col,
|
||||
i;
|
||||
|
||||
for (i = 0; i < colNodes.length; i += 1) {
|
||||
colNode = colNodes[i];
|
||||
col = {
|
||||
key: colNode.getAttribute('data-col'),
|
||||
sortable: !colNode.getAttribute('data-nosort'),
|
||||
type: colNode.getAttribute('data-type') || 'string'
|
||||
};
|
||||
cols.push(col);
|
||||
if (col.sortable) {
|
||||
col.defaultDescSort = col.type === 'number';
|
||||
colNode.innerHTML =
|
||||
colNode.innerHTML + '<span class="sorter"></span>';
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
// attaches a data attribute to every tr element with an object
|
||||
// of data values keyed by column name
|
||||
function loadRowData(tableRow) {
|
||||
var tableCols = tableRow.querySelectorAll('td'),
|
||||
colNode,
|
||||
col,
|
||||
data = {},
|
||||
i,
|
||||
val;
|
||||
for (i = 0; i < tableCols.length; i += 1) {
|
||||
colNode = tableCols[i];
|
||||
col = cols[i];
|
||||
val = colNode.getAttribute('data-value');
|
||||
if (col.type === 'number') {
|
||||
val = Number(val);
|
||||
}
|
||||
data[col.key] = val;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// loads all row data
|
||||
function loadData() {
|
||||
var rows = getTableBody().querySelectorAll('tr'),
|
||||
i;
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
rows[i].data = loadRowData(rows[i]);
|
||||
}
|
||||
}
|
||||
// sorts the table using the data for the ith column
|
||||
function sortByIndex(index, desc) {
|
||||
var key = cols[index].key,
|
||||
sorter = function(a, b) {
|
||||
a = a.data[key];
|
||||
b = b.data[key];
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
},
|
||||
finalSorter = sorter,
|
||||
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||
rowNodes = tableBody.querySelectorAll('tr'),
|
||||
rows = [],
|
||||
i;
|
||||
|
||||
if (desc) {
|
||||
finalSorter = function(a, b) {
|
||||
return -1 * sorter(a, b);
|
||||
};
|
||||
}
|
||||
|
||||
for (i = 0; i < rowNodes.length; i += 1) {
|
||||
rows.push(rowNodes[i]);
|
||||
tableBody.removeChild(rowNodes[i]);
|
||||
}
|
||||
|
||||
rows.sort(finalSorter);
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
tableBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
// removes sort indicators for current column being sorted
|
||||
function removeSortIndicators() {
|
||||
var col = getNthColumn(currentSort.index),
|
||||
cls = col.className;
|
||||
|
||||
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||
col.className = cls;
|
||||
}
|
||||
// adds sort indicators for current column being sorted
|
||||
function addSortIndicators() {
|
||||
getNthColumn(currentSort.index).className += currentSort.desc
|
||||
? ' sorted-desc'
|
||||
: ' sorted';
|
||||
}
|
||||
// adds event listeners for all sorter widgets
|
||||
function enableUI() {
|
||||
var i,
|
||||
el,
|
||||
ithSorter = function ithSorter(i) {
|
||||
var col = cols[i];
|
||||
|
||||
return function() {
|
||||
var desc = col.defaultDescSort;
|
||||
|
||||
if (currentSort.index === i) {
|
||||
desc = !currentSort.desc;
|
||||
}
|
||||
sortByIndex(i, desc);
|
||||
removeSortIndicators();
|
||||
currentSort.index = i;
|
||||
currentSort.desc = desc;
|
||||
addSortIndicators();
|
||||
};
|
||||
};
|
||||
for (i = 0; i < cols.length; i += 1) {
|
||||
if (cols[i].sortable) {
|
||||
// add the click event handler on the th so users
|
||||
// dont have to click on those tiny arrows
|
||||
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener('click', ithSorter(i));
|
||||
} else {
|
||||
el.attachEvent('onclick', ithSorter(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// adds sorting functionality to the UI
|
||||
return function() {
|
||||
if (!getTable()) {
|
||||
return;
|
||||
}
|
||||
cols = loadColumns();
|
||||
loadData();
|
||||
addSearchBox();
|
||||
addSortIndicators();
|
||||
enableUI();
|
||||
};
|
||||
})();
|
||||
|
||||
window.addEventListener('load', addSorting);
|
||||
@@ -0,0 +1,38 @@
|
||||
TN:
|
||||
SF:src/features/reference-feature/adapters/reference-http-gateway.ts
|
||||
FN:37,mapProblem
|
||||
FN:64,createReferenceHttpGateway
|
||||
FN:68,list
|
||||
FN:71,create
|
||||
FN:74,get
|
||||
FNF:5
|
||||
FNH:5
|
||||
FNDA:5,mapProblem
|
||||
FNDA:8,createReferenceHttpGateway
|
||||
FNDA:1,list
|
||||
FNDA:6,create
|
||||
FNDA:3,get
|
||||
DA:25,2
|
||||
DA:27,2
|
||||
DA:38,5
|
||||
DA:39,2
|
||||
DA:44,3
|
||||
DA:45,2
|
||||
DA:50,1
|
||||
DA:67,8
|
||||
DA:69,1
|
||||
DA:72,6
|
||||
DA:75,3
|
||||
LF:11
|
||||
LH:11
|
||||
BRDA:38,0,0,2
|
||||
BRDA:38,0,1,3
|
||||
BRDA:41,1,0,2
|
||||
BRDA:41,1,1,1
|
||||
BRDA:44,2,0,2
|
||||
BRDA:44,2,1,1
|
||||
BRDA:47,3,0,2
|
||||
BRDA:47,3,1,1
|
||||
BRF:8
|
||||
BRH:8
|
||||
end_of_record
|
||||
+2837
-431
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"snapshotDigest": "53468eada148a7afc821b0885b8a53bf47978bfd17adf26b424054beedcce608",
|
||||
"snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8",
|
||||
"owner": "frontend-platform",
|
||||
"reason": "Make the reference feature route access policy explicitly session-required",
|
||||
"approvedAt": "2026-08-01T07:18:30.000Z"
|
||||
"reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
|
||||
"approvedAt": "2026-08-01T15:15:45.537Z"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"registryId": "FE-REG-ROUTE",
|
||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||
"source": "src/features/installed-feature-contracts.ts",
|
||||
"rowCount": 9,
|
||||
"rowCount": 10,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"routeId",
|
||||
@@ -121,6 +121,19 @@
|
||||
"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",
|
||||
@@ -218,7 +231,7 @@
|
||||
"registryId": "FE-REG-ROUTE-RUNTIME",
|
||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||
"source": "src/features/installed-feature-contracts.ts",
|
||||
"rowCount": 9,
|
||||
"rowCount": 10,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"routeId",
|
||||
@@ -275,6 +288,12 @@
|
||||
"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",
|
||||
@@ -408,41 +427,101 @@
|
||||
"rows": {
|
||||
"CREATE_REFERENCE_RESOURCE": {
|
||||
"auth": "external-session",
|
||||
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
|
||||
"contractVersion": 2,
|
||||
"csrfProfileId": "NO_CSRF_BEARER",
|
||||
"idempotency": "keyed",
|
||||
"idempotencyKeyPolicy": "REQUIRED",
|
||||
"mapperId": "ReferenceResourceMapper",
|
||||
"maxEncodedSearchBytes": 0,
|
||||
"maxResponseBytes": 32768,
|
||||
"method": "POST",
|
||||
"operationId": "CREATE_REFERENCE_RESOURCE",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"path": "/api/reference-resources",
|
||||
"pathParameterNames": [],
|
||||
"pathSchema": "NoRequest",
|
||||
"protocol": "REST",
|
||||
"providerId": "PRIMARY_API",
|
||||
"replayPolicy": "KEYED_COMMAND",
|
||||
"requestSchema": "CreateReferenceResourceCommand",
|
||||
"requestSource": "body",
|
||||
"responseMediaTypes": [
|
||||
"application/json"
|
||||
],
|
||||
"responseSchema": "ReferenceResourcePayload",
|
||||
"retry": "runtime",
|
||||
"semantics": "COMMAND",
|
||||
"successStatuses": [
|
||||
200,
|
||||
201
|
||||
],
|
||||
"timeoutMs": null
|
||||
},
|
||||
"GET_REFERENCE_RESOURCE": {
|
||||
"auth": "external-session",
|
||||
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
|
||||
"contractVersion": 2,
|
||||
"csrfProfileId": "NO_CSRF_BEARER",
|
||||
"idempotency": "safe",
|
||||
"idempotencyKeyPolicy": "NONE",
|
||||
"mapperId": "ReferenceResourceMapper",
|
||||
"maxEncodedSearchBytes": 0,
|
||||
"maxResponseBytes": 32768,
|
||||
"method": "GET",
|
||||
"operationId": "GET_REFERENCE_RESOURCE",
|
||||
"owner": "feature-frontend-form-page-platform",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"path": "/api/reference-resources/{resourceId}",
|
||||
"pathParameterNames": [
|
||||
"resourceId"
|
||||
],
|
||||
"pathSchema": "ReferenceResourceParams",
|
||||
"protocol": "REST",
|
||||
"providerId": "PRIMARY_API",
|
||||
"replayPolicy": "SAFE",
|
||||
"requestSchema": "NoRequest",
|
||||
"requestSource": "none",
|
||||
"responseMediaTypes": [
|
||||
"application/json"
|
||||
],
|
||||
"responseSchema": "ReferenceResourcePayload",
|
||||
"retry": "runtime",
|
||||
"semantics": "QUERY",
|
||||
"successStatuses": [
|
||||
200
|
||||
],
|
||||
"timeoutMs": null
|
||||
},
|
||||
"LIST_REFERENCE_RESOURCES": {
|
||||
"auth": "external-session",
|
||||
"authProfileId": "REFERENCE_EXTERNAL_BEARER",
|
||||
"contractVersion": 2,
|
||||
"csrfProfileId": "NO_CSRF_BEARER",
|
||||
"idempotency": "safe",
|
||||
"idempotencyKeyPolicy": "NONE",
|
||||
"mapperId": "ReferenceResourceListMapper",
|
||||
"maxEncodedSearchBytes": 4096,
|
||||
"maxResponseBytes": 262144,
|
||||
"method": "GET",
|
||||
"operationId": "LIST_REFERENCE_RESOURCES",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"path": "/api/reference-resources",
|
||||
"pathParameterNames": [],
|
||||
"pathSchema": "NoRequest",
|
||||
"protocol": "REST",
|
||||
"providerId": "PRIMARY_API",
|
||||
"replayPolicy": "SAFE",
|
||||
"requestSchema": "ReferenceResourceListQuery",
|
||||
"requestSource": "search",
|
||||
"responseMediaTypes": [
|
||||
"application/json"
|
||||
],
|
||||
"responseSchema": "ReferenceResourceListPayload",
|
||||
"retry": "runtime",
|
||||
"semantics": "QUERY",
|
||||
"successStatuses": [
|
||||
200
|
||||
],
|
||||
"timeoutMs": null
|
||||
}
|
||||
}
|
||||
@@ -491,9 +570,12 @@
|
||||
"rows": {
|
||||
"CreateReferenceResourceCommand": {
|
||||
"boundary": "api-request",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"runtime": "zod",
|
||||
"schemaId": "CreateReferenceResourceCommand"
|
||||
"schemaId": "CreateReferenceResourceCommand",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"none": {
|
||||
"boundary": "route-params",
|
||||
@@ -503,9 +585,12 @@
|
||||
},
|
||||
"NoRequest": {
|
||||
"boundary": "api-request",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"runtime": "zod",
|
||||
"schemaId": "NoRequest"
|
||||
"schemaId": "NoRequest",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"NotFoundSplat": {
|
||||
"boundary": "route-params",
|
||||
@@ -515,27 +600,39 @@
|
||||
},
|
||||
"ReferenceResourceListPayload": {
|
||||
"boundary": "api-response",
|
||||
"direction": "RESPONSE",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"runtime": "zod",
|
||||
"schemaId": "ReferenceResourceListPayload"
|
||||
"schemaId": "ReferenceResourceListPayload",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
},
|
||||
"ReferenceResourceListQuery": {
|
||||
"boundary": "route-search-api-request",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"runtime": "zod",
|
||||
"schemaId": "ReferenceResourceListQuery"
|
||||
"schemaId": "ReferenceResourceListQuery",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"ReferenceResourceParams": {
|
||||
"boundary": "route-params",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"runtime": "zod",
|
||||
"schemaId": "ReferenceResourceParams"
|
||||
"schemaId": "ReferenceResourceParams",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"ReferenceResourcePayload": {
|
||||
"boundary": "api-response",
|
||||
"direction": "RESPONSE",
|
||||
"owner": "feature-frontend-reference-feature-vertical-slice",
|
||||
"runtime": "zod",
|
||||
"schemaId": "ReferenceResourcePayload"
|
||||
"schemaId": "ReferenceResourcePayload",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -585,12 +682,6 @@
|
||||
"phase": "runtime",
|
||||
"required": true
|
||||
},
|
||||
"API_CONTRACT_VERSION": {
|
||||
"classification": "public",
|
||||
"defaultValue": null,
|
||||
"phase": "runtime",
|
||||
"required": true
|
||||
},
|
||||
"APP_ENV": {
|
||||
"classification": "public",
|
||||
"defaultValue": null,
|
||||
@@ -603,6 +694,12 @@
|
||||
"phase": "runtime",
|
||||
"required": true
|
||||
},
|
||||
"CAPABILITY_OVERRIDES": {
|
||||
"classification": "public",
|
||||
"defaultValue": null,
|
||||
"phase": "runtime",
|
||||
"required": false
|
||||
},
|
||||
"CONFIG_SCHEMA_VERSION": {
|
||||
"classification": "public",
|
||||
"defaultValue": null,
|
||||
@@ -795,7 +892,7 @@
|
||||
"registryId": "FE-REG-ERROR",
|
||||
"owner": "feature-frontend-error-classification-boundary-contract",
|
||||
"source": "src/contracts/errors.ts",
|
||||
"rowCount": 31,
|
||||
"rowCount": 38,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"kind",
|
||||
@@ -1041,6 +1138,24 @@
|
||||
"telemetryEvent": "release.mismatch.detected",
|
||||
"userMessageKey": "error.deploy_mismatch"
|
||||
},
|
||||
"DUPLICATE_IN_FLIGHT": {
|
||||
"action": "none",
|
||||
"defaultRetryable": false,
|
||||
"kind": "DUPLICATE_IN_FLIGHT",
|
||||
"redaction": [
|
||||
"authorization",
|
||||
"body",
|
||||
"cause",
|
||||
"headers",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
"url"
|
||||
],
|
||||
"severity": "info",
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.duplicate_in_flight"
|
||||
},
|
||||
"ENVELOPE_MISMATCH": {
|
||||
"action": "contact-support",
|
||||
"defaultRetryable": false,
|
||||
@@ -1077,6 +1192,24 @@
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.forbidden"
|
||||
},
|
||||
"IDENTITY_INTERN_LIMIT_EXCEEDED": {
|
||||
"action": "retry",
|
||||
"defaultRetryable": false,
|
||||
"kind": "IDENTITY_INTERN_LIMIT_EXCEEDED",
|
||||
"redaction": [
|
||||
"authorization",
|
||||
"body",
|
||||
"cause",
|
||||
"headers",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
"url"
|
||||
],
|
||||
"severity": "warning",
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.identity_intern_limit_exceeded"
|
||||
},
|
||||
"MALFORMED_JSON": {
|
||||
"action": "contact-support",
|
||||
"defaultRetryable": false,
|
||||
@@ -1095,6 +1228,24 @@
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.malformed_json"
|
||||
},
|
||||
"MAPPING_CONTRACT_VIOLATION": {
|
||||
"action": "contact-support",
|
||||
"defaultRetryable": false,
|
||||
"kind": "MAPPING_CONTRACT_VIOLATION",
|
||||
"redaction": [
|
||||
"authorization",
|
||||
"body",
|
||||
"cause",
|
||||
"headers",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
"url"
|
||||
],
|
||||
"severity": "error",
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.mapping_contract_violation"
|
||||
},
|
||||
"NETWORK_UNREACHABLE": {
|
||||
"action": "retry",
|
||||
"defaultRetryable": true,
|
||||
@@ -1131,6 +1282,24 @@
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.not_found"
|
||||
},
|
||||
"PAGINATION_CONTRACT_VIOLATION": {
|
||||
"action": "contact-support",
|
||||
"defaultRetryable": false,
|
||||
"kind": "PAGINATION_CONTRACT_VIOLATION",
|
||||
"redaction": [
|
||||
"authorization",
|
||||
"body",
|
||||
"cause",
|
||||
"headers",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
"url"
|
||||
],
|
||||
"severity": "error",
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.pagination_contract_violation"
|
||||
},
|
||||
"QUERY_CACHE_FAILURE": {
|
||||
"action": "retry",
|
||||
"defaultRetryable": false,
|
||||
@@ -1257,6 +1426,42 @@
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.request_timeout"
|
||||
},
|
||||
"RESPONSE_BODY_LIMIT": {
|
||||
"action": "contact-support",
|
||||
"defaultRetryable": false,
|
||||
"kind": "RESPONSE_BODY_LIMIT",
|
||||
"redaction": [
|
||||
"authorization",
|
||||
"body",
|
||||
"cause",
|
||||
"headers",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
"url"
|
||||
],
|
||||
"severity": "error",
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.response_body_limit"
|
||||
},
|
||||
"RESULT_LIMIT_EXCEEDED": {
|
||||
"action": "contact-support",
|
||||
"defaultRetryable": false,
|
||||
"kind": "RESULT_LIMIT_EXCEEDED",
|
||||
"redaction": [
|
||||
"authorization",
|
||||
"body",
|
||||
"cause",
|
||||
"headers",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
"url"
|
||||
],
|
||||
"severity": "error",
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.result_limit_exceeded"
|
||||
},
|
||||
"SCHEMA_MISMATCH": {
|
||||
"action": "contact-support",
|
||||
"defaultRetryable": false,
|
||||
@@ -1275,6 +1480,24 @@
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.schema_mismatch"
|
||||
},
|
||||
"SCOPE_GENERATION_CHANGED": {
|
||||
"action": "none",
|
||||
"defaultRetryable": false,
|
||||
"kind": "SCOPE_GENERATION_CHANGED",
|
||||
"redaction": [
|
||||
"authorization",
|
||||
"body",
|
||||
"cause",
|
||||
"headers",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
"url"
|
||||
],
|
||||
"severity": "info",
|
||||
"telemetryEvent": "api.request.failed",
|
||||
"userMessageKey": "error.scope_generation_changed"
|
||||
},
|
||||
"SERVER_FAILURE": {
|
||||
"action": "retry",
|
||||
"defaultRetryable": true,
|
||||
@@ -1404,68 +1627,116 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-QUERY",
|
||||
"registryId": "FE-REG-QUERY-INVALIDATION",
|
||||
"owner": "feature-frontend-server-state-caching-contract",
|
||||
"source": "src/features/installed-feature-contracts.ts",
|
||||
"rowCount": 1,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"namespace",
|
||||
"serialization",
|
||||
"identity",
|
||||
"invalidation",
|
||||
"invalidationTopic",
|
||||
"crossContext",
|
||||
"version",
|
||||
"persistence"
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"namespace": "array",
|
||||
"serialization": "string",
|
||||
"identity": "string",
|
||||
"invalidation": "string",
|
||||
"invalidationTopic": "string",
|
||||
"crossContext": "string",
|
||||
"version": "integer",
|
||||
"persistence": "string"
|
||||
},
|
||||
"uniqueFields": [
|
||||
"namespace",
|
||||
"invalidationTopic"
|
||||
],
|
||||
"allowedValues": {
|
||||
"crossContext": [
|
||||
"invalidate-only"
|
||||
],
|
||||
"persistence": [
|
||||
"disabled"
|
||||
]
|
||||
"topicId": "string",
|
||||
"namespace.namespaceId": "string",
|
||||
"namespace.namespaceVersion": "integer"
|
||||
},
|
||||
"uniqueFields": [],
|
||||
"allowedValues": {},
|
||||
"references": [],
|
||||
"keyField": null,
|
||||
"breakingFields": [
|
||||
"namespace",
|
||||
"serialization",
|
||||
"identity",
|
||||
"invalidationTopic",
|
||||
"crossContext",
|
||||
"version",
|
||||
"persistence"
|
||||
"topics",
|
||||
"namespaces",
|
||||
"edges"
|
||||
],
|
||||
"rowsPath": "edges",
|
||||
"rowKeyFields": [
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
],
|
||||
"uniqueFieldSets": [
|
||||
[
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
]
|
||||
],
|
||||
"snapshotProjection": {
|
||||
"singletonRowKey": "invalidation-graph",
|
||||
"canonicalArrayKeyFields": {
|
||||
"topics": [
|
||||
"$value"
|
||||
],
|
||||
"namespaces": [
|
||||
"namespaceId",
|
||||
"namespaceVersion"
|
||||
],
|
||||
"edges": [
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rows": {
|
||||
"invalidation-graph": {
|
||||
"edges": [
|
||||
{
|
||||
"namespace": {
|
||||
"namespaceId": "reference-resource",
|
||||
"namespaceVersion": 1
|
||||
},
|
||||
"topicId": "qinv.01k10f7m3w9p6r2c8v5n4x"
|
||||
}
|
||||
],
|
||||
"namespaces": [
|
||||
{
|
||||
"namespaceId": "reference-resource",
|
||||
"namespaceVersion": 1
|
||||
}
|
||||
],
|
||||
"topics": [
|
||||
"qinv.01k10f7m3w9p6r2c8v5n4x"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
|
||||
"owner": "feature-frontend-server-state-caching-contract",
|
||||
"source": "src/features/installed-feature-contracts.ts",
|
||||
"rowCount": 1,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"topicId",
|
||||
"topicVersion"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"topicId": "string",
|
||||
"topicVersion": "integer"
|
||||
},
|
||||
"uniqueFields": [
|
||||
"topicId"
|
||||
],
|
||||
"allowedValues": {},
|
||||
"references": [],
|
||||
"keyField": null,
|
||||
"breakingFields": [
|
||||
"topicId",
|
||||
"topicVersion"
|
||||
],
|
||||
"rowKeyFields": [
|
||||
"topicId"
|
||||
]
|
||||
},
|
||||
"rows": {
|
||||
"REFERENCE_RESOURCE": {
|
||||
"crossContext": "invalidate-only",
|
||||
"identity": "no-pii-token-or-raw-url",
|
||||
"invalidation": "reference resource namespace after successful mutation",
|
||||
"invalidationTopic": "qinv.01k10f7m3w9p6r2c8v5n4x",
|
||||
"namespace": [
|
||||
"reference-resource",
|
||||
1
|
||||
],
|
||||
"persistence": "disabled",
|
||||
"serialization": "canonical-object-order",
|
||||
"version": 1
|
||||
"[\"qinv.01k10f7m3w9p6r2c8v5n4x\"]": {
|
||||
"topicId": "qinv.01k10f7m3w9p6r2c8v5n4x",
|
||||
"topicVersion": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1655,7 +1926,7 @@
|
||||
"registryId": "FE-REG-RELEASE",
|
||||
"owner": "feature-frontend-release-cache-rollback-contract",
|
||||
"source": "src/contracts/release-tokens.ts",
|
||||
"rowCount": 8,
|
||||
"rowCount": 9,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"token",
|
||||
@@ -1681,7 +1952,7 @@
|
||||
},
|
||||
"rows": {
|
||||
"apiContractVersion": {
|
||||
"compatibilityRole": "schema compatibility",
|
||||
"compatibilityRole": "legacy V1 scalar; superseded by contractSetDigest",
|
||||
"source": "frontend/backend agreement",
|
||||
"token": "apiContractVersion"
|
||||
},
|
||||
@@ -1715,6 +1986,11 @@
|
||||
"source": "runtime config schema",
|
||||
"token": "configSchemaVersion"
|
||||
},
|
||||
"contractSetDigest": {
|
||||
"compatibilityRole": "release coherence for multi-package contracts",
|
||||
"source": "compiled external contract package set",
|
||||
"token": "contractSetDigest"
|
||||
},
|
||||
"releaseId": {
|
||||
"compatibilityRole": "rollback target",
|
||||
"source": "deploy system",
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
"rollback": "Restore the V1 writer in scripts/generate-build-manifest.ts and the scalar key in public/config.json; the V1 reader is still present.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-QUERY:*:*:removed",
|
||||
"versionBump": "Query invalidation composition moves from the legacy flat query registry to the bounded many-to-many invalidation graph.",
|
||||
"migration": "Installed feature contracts now contribute topics, namespace identities, edges, and separate wire versions; bootstrap validates and indexes those contributions before constructing coordinators.",
|
||||
"compatibilityWindow": "Cross-context envelopes remain opaque topic/version pairs and release cache epochs isolate mixed releases; no query keys or cached values cross contexts.",
|
||||
"rollback": "Restore the flat QUERY_REGISTRY composition and its coordinator adapter together with the prior governance entry.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-QUERY:$contract:allowedValues:contract-field-changed",
|
||||
"versionBump": "Cross-context invalidation wire protocol starts at version 1.",
|
||||
|
||||
@@ -380,50 +380,72 @@
|
||||
"breakingFields": ["kind", "userMessageKey", "action", "telemetryEvent"]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-QUERY",
|
||||
"registryId": "FE-REG-QUERY-INVALIDATION",
|
||||
"path": "src/features/installed-feature-contracts.ts",
|
||||
"exportName": "QUERY_REGISTRY",
|
||||
"exportName": "INVALIDATION_REGISTRY",
|
||||
"rowsPath": "edges",
|
||||
"rowKeyFields": [
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
],
|
||||
"owner": "feature-frontend-server-state-caching-contract",
|
||||
"requiredFields": [
|
||||
"namespace",
|
||||
"serialization",
|
||||
"identity",
|
||||
"invalidation",
|
||||
"invalidationTopic",
|
||||
"crossContext",
|
||||
"version",
|
||||
"persistence"
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"namespace": "array",
|
||||
"serialization": "string",
|
||||
"identity": "string",
|
||||
"invalidation": "string",
|
||||
"invalidationTopic": "string",
|
||||
"crossContext": "string",
|
||||
"version": "integer",
|
||||
"persistence": "string"
|
||||
"topicId": "string",
|
||||
"namespace.namespaceId": "string",
|
||||
"namespace.namespaceVersion": "integer"
|
||||
},
|
||||
"uniqueFields": ["namespace", "invalidationTopic"],
|
||||
"allowedValues": {
|
||||
"crossContext": ["invalidate-only"],
|
||||
"persistence": ["disabled"]
|
||||
"uniqueFieldSets": [
|
||||
[
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
]
|
||||
],
|
||||
"snapshotProjection": {
|
||||
"singletonRowKey": "invalidation-graph",
|
||||
"canonicalArrayKeyFields": {
|
||||
"topics": ["$value"],
|
||||
"namespaces": ["namespaceId", "namespaceVersion"],
|
||||
"edges": [
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
]
|
||||
}
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"path": "src/features/reference-feature/contracts/reference-feature-contract.ts",
|
||||
"token": "referenceQueryKeys"
|
||||
"path": "src/bootstrap/runtime-adapters.ts",
|
||||
"token": "indexInvalidationRegistry(INVALIDATION_REGISTRY)"
|
||||
}
|
||||
],
|
||||
"breakingFields": [
|
||||
"namespace",
|
||||
"serialization",
|
||||
"identity",
|
||||
"invalidationTopic",
|
||||
"crossContext",
|
||||
"version",
|
||||
"persistence"
|
||||
]
|
||||
"breakingFields": ["topics", "namespaces", "edges"]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
|
||||
"path": "src/features/installed-feature-contracts.ts",
|
||||
"exportName": "INVALIDATION_TOPIC_VERSIONS",
|
||||
"rowKeyFields": ["topicId"],
|
||||
"owner": "feature-frontend-server-state-caching-contract",
|
||||
"requiredFields": ["topicId", "topicVersion"],
|
||||
"fieldTypes": {
|
||||
"topicId": "string",
|
||||
"topicVersion": "integer"
|
||||
},
|
||||
"uniqueFields": ["topicId"],
|
||||
"consumers": [
|
||||
{
|
||||
"path": "src/bootstrap/runtime-adapters.ts",
|
||||
"token": "indexInvalidationTopicVersions("
|
||||
}
|
||||
],
|
||||
"breakingFields": ["topicId", "topicVersion"]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-TELEMETRY",
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
"lifecycleMethods": ["release-file-ref", "release-or-dispose-preview-leases", "cancel-via-AbortSignal", "reconcile-or-explicitly-abort-upload", "close-checkpoint-store", "dispose-capability-and-image-runtime"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Treat file name, extension, MIME and lastModified as untrusted metadata.", "Resolve only exact composition-issued file and image policy object identities; callers cannot raise byte, candidate, pixel, quality, format, lifetime or origin ceilings.", "Use opaque file references and verification receipts bound to an inspected immutable file snapshot and the exact registered profile; reject replay through another profile even when an inspection rule ID matches.", "Treat presigned URLs as bearer capabilities; bind exact method, resource or upload part, offset, length, media type, checksum, origin, path, query, headers and expiry in an in-memory identity vault.", "Use credentials omit, redirect error, no-referrer and no-store for direct data-plane fetch; never persist or observe URL, query, signed header, capability, file name or raw backend message, and never emit digest, raw ETag or receipt values to diagnostics or telemetry.", "A strict account-partitioned upload checkpoint may persist only the protocol-defined SHA-256 file fingerprint, per-part checksum and bounded opaque non-authorizing part receipt token required for server reconciliation; no bearer token or raw signed capability is allowed.", "Persist only strict non-authorizing upload checkpoints and reconcile them with server-authoritative status and re-hashed local parts before completion.", "Require a synchronous server-issued browser-managed download capability whose receipt exactly equals the caller's branded capability receipt and whose resource, media type, safe extension, maximum bytes, optional digest and expiry all match before handoff.", "Expose File, OPFS, Cache and transfer byte streams only as chunk-level closed Results; stop after the first failure, cancel native readers and never throw a raw native exception across the port.", "Accept Image CDN assets only through immutable allowlisted or signature-verified descriptors and registered preset identities; reject active formats, arbitrary transforms, pixel/decode-budget overflow and unsafe cache policy.", "Upload completion remains QUARANTINED until backend scan and promotion; client capability checks are not an authorization boundary.", "Active content preview requires isolation or download-only treatment."],
|
||||
"bundleBudgetGzipBytes": 52000,
|
||||
"bundleBudgetGzipBytes": 54600,
|
||||
"fallback": "Accessible native file input, same-origin authorized server upload/download and a single bounded server-selected image rendition; generated artifacts above the buffer budget move to server-side generation.",
|
||||
"removal": ["Stop new capability and upload-session issuance, then cancel active reads and transfers.", "Reconcile or explicitly abort active multipart sessions and let backend TTL cleanup remove ambiguous orphans.", "Remove non-secret checkpoints according to account and retention policy.", "Release file references, revoke preview object-URL leases and dispose file, capability and image runtimes.", "Remove transfer/image feature facades and composition, then prove browser-transfer sources are absent from the production module inventory."],
|
||||
"serverStatePolicy": "query-cache-metadata-only"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,34 @@
|
||||
"public",
|
||||
"schemas",
|
||||
".storybook",
|
||||
".gitea/workflows/quality-gates.yml",
|
||||
".dependency-cruiser.json",
|
||||
".nvmrc",
|
||||
".npmrc",
|
||||
"eslint.config.ts",
|
||||
"index.html",
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"pnpm-workspace.yaml",
|
||||
"tsconfig.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.base.json",
|
||||
"tsconfig.node.json",
|
||||
"tsconfig.recipes.json",
|
||||
"tsconfig.service-worker.json",
|
||||
"tsconfig.test.json",
|
||||
"tsconfig.web-worker.json",
|
||||
"vite.config.ts",
|
||||
"vite.service-worker.config.ts",
|
||||
"vitest.config.ts",
|
||||
"playwright.config.ts"
|
||||
"playwright.config.ts",
|
||||
"playwright.capabilities.config.ts",
|
||||
"playwright.dev.config.ts",
|
||||
"playwright.storybook.config.ts",
|
||||
"playwright.visual.config.ts"
|
||||
],
|
||||
"generatedRoots": ["dist", "artifacts/release"],
|
||||
"optionalRoots": ["artifacts/release"],
|
||||
"excludedPaths": [
|
||||
"tests/fixtures/security/secret-detection/forbidden"
|
||||
],
|
||||
|
||||
@@ -1,14 +1,57 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"schemaVersion": 2,
|
||||
"repositoryBaseline": 285,
|
||||
"generatedPaths": [],
|
||||
"summary": {
|
||||
"lines": 80,
|
||||
"statements": 78,
|
||||
"functions": 85,
|
||||
"lines": 75,
|
||||
"statements": 73,
|
||||
"functions": 80,
|
||||
"branches": 68
|
||||
},
|
||||
"criticalModules": [
|
||||
{
|
||||
"path": "src/adapters/http/bounded-body-reader.ts",
|
||||
"owner": "http-runtime",
|
||||
"minimum": {
|
||||
"lines": 95,
|
||||
"statements": 95,
|
||||
"functions": 95,
|
||||
"branches": 90
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/http/bounded-json.ts",
|
||||
"owner": "http-runtime",
|
||||
"minimum": {
|
||||
"lines": 85,
|
||||
"statements": 84,
|
||||
"functions": 95,
|
||||
"branches": 78
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/http/http-execution-v3.ts",
|
||||
"owner": "http-runtime",
|
||||
"minimum": {
|
||||
"lines": 75,
|
||||
"statements": 73,
|
||||
"functions": 70,
|
||||
"branches": 52
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/http/request-builder.ts",
|
||||
"owner": "http-runtime",
|
||||
"minimum": {
|
||||
"lines": 85,
|
||||
"statements": 85,
|
||||
"functions": 95,
|
||||
"branches": 82
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/http/retry-policy.ts",
|
||||
"owner": "http-runtime",
|
||||
"minimum": {
|
||||
"lines": 80,
|
||||
"statements": 78,
|
||||
@@ -16,8 +59,29 @@
|
||||
"branches": 78
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/query-cache/server-state-scope-runtime.ts",
|
||||
"owner": "server-state-runtime",
|
||||
"minimum": {
|
||||
"lines": 85,
|
||||
"statements": 85,
|
||||
"functions": 85,
|
||||
"branches": 75
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/service-worker/service-worker-lifecycle.ts",
|
||||
"owner": "service-worker-runtime",
|
||||
"minimum": {
|
||||
"lines": 64,
|
||||
"statements": 60,
|
||||
"functions": 65,
|
||||
"branches": 43
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/storage/browser-storage-adapter.ts",
|
||||
"owner": "storage-runtime",
|
||||
"minimum": {
|
||||
"lines": 60,
|
||||
"statements": 60,
|
||||
@@ -27,6 +91,7 @@
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/telemetry/best-effort-telemetry.ts",
|
||||
"owner": "telemetry-runtime",
|
||||
"minimum": {
|
||||
"lines": 85,
|
||||
"statements": 85,
|
||||
@@ -36,6 +101,7 @@
|
||||
},
|
||||
{
|
||||
"path": "src/application/create-application.ts",
|
||||
"owner": "application-runtime",
|
||||
"minimum": {
|
||||
"lines": 90,
|
||||
"statements": 90,
|
||||
@@ -44,7 +110,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/application/policies/compatibility.ts",
|
||||
"path": "src/contracts/compatibility.ts",
|
||||
"owner": "compatibility-contracts",
|
||||
"minimum": {
|
||||
"lines": 95,
|
||||
"statements": 95,
|
||||
@@ -54,6 +121,7 @@
|
||||
},
|
||||
{
|
||||
"path": "src/application/policies/performance-budgets.ts",
|
||||
"owner": "application-policy",
|
||||
"minimum": {
|
||||
"lines": 80,
|
||||
"statements": 80,
|
||||
@@ -63,6 +131,7 @@
|
||||
},
|
||||
{
|
||||
"path": "src/application/policies/promotion-readiness.ts",
|
||||
"owner": "release-runtime",
|
||||
"minimum": {
|
||||
"lines": 95,
|
||||
"statements": 95,
|
||||
@@ -72,6 +141,7 @@
|
||||
},
|
||||
{
|
||||
"path": "src/application/use-cases/decide-chunk-recovery.ts",
|
||||
"owner": "application-runtime",
|
||||
"minimum": {
|
||||
"lines": 90,
|
||||
"statements": 90,
|
||||
@@ -79,8 +149,29 @@
|
||||
"branches": 85
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/bootstrap/load-release-manifest.ts",
|
||||
"owner": "release-runtime",
|
||||
"minimum": {
|
||||
"lines": 90,
|
||||
"statements": 90,
|
||||
"functions": 90,
|
||||
"branches": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/bootstrap/read-bounded-boot-json.ts",
|
||||
"owner": "bootstrap-runtime",
|
||||
"minimum": {
|
||||
"lines": 71,
|
||||
"statements": 66,
|
||||
"functions": 48,
|
||||
"branches": 57
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "src/contracts/diagnostics.ts",
|
||||
"owner": "diagnostics-contracts",
|
||||
"minimum": {
|
||||
"lines": 68,
|
||||
"statements": 68,
|
||||
@@ -90,6 +181,7 @@
|
||||
},
|
||||
{
|
||||
"path": "src/features/reference-feature/adapters/reference-http-gateway.ts",
|
||||
"owner": "reference-feature",
|
||||
"minimum": {
|
||||
"lines": 90,
|
||||
"statements": 90,
|
||||
@@ -99,21 +191,35 @@
|
||||
},
|
||||
{
|
||||
"path": "src/presentation/adapters/query/application-query.ts",
|
||||
"owner": "presentation-runtime",
|
||||
"minimum": {
|
||||
"lines": 90,
|
||||
"statements": 90,
|
||||
"functions": 90,
|
||||
"branches": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "scripts/lib/registry-compatibility.ts",
|
||||
"minimum": {
|
||||
"lines": 80,
|
||||
"statements": 80,
|
||||
"functions": 85,
|
||||
"branches": 60
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"highRiskPaths": [
|
||||
"src/adapters/http/bounded-body-reader.ts",
|
||||
"src/adapters/http/bounded-json.ts",
|
||||
"src/adapters/http/http-execution-v3.ts",
|
||||
"src/adapters/http/request-builder.ts",
|
||||
"src/adapters/http/retry-policy.ts",
|
||||
"src/adapters/query-cache/server-state-scope-runtime.ts",
|
||||
"src/adapters/service-worker/service-worker-lifecycle.ts",
|
||||
"src/adapters/storage/browser-storage-adapter.ts",
|
||||
"src/adapters/telemetry/best-effort-telemetry.ts",
|
||||
"src/application/create-application.ts",
|
||||
"src/contracts/compatibility.ts",
|
||||
"src/application/policies/performance-budgets.ts",
|
||||
"src/application/policies/promotion-readiness.ts",
|
||||
"src/application/use-cases/decide-chunk-recovery.ts",
|
||||
"src/bootstrap/load-release-manifest.ts",
|
||||
"src/bootstrap/read-bounded-boot-json.ts",
|
||||
"src/contracts/diagnostics.ts",
|
||||
"src/features/reference-feature/adapters/reference-http-gateway.ts",
|
||||
"src/presentation/adapters/query/application-query.ts"
|
||||
],
|
||||
"waivers": []
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"schemaVersion": 2,
|
||||
"scenarioCatalogs": [
|
||||
{
|
||||
"owner": "reference-feature",
|
||||
"path": "tests/mocks/scenarios/catalog.ts",
|
||||
"arrayExport": "HTTP_SCENARIO_IDS",
|
||||
"minimumEntries": 19
|
||||
"expectationExport": "HTTP_SCENARIO_EXPECTATIONS",
|
||||
"receiptPath": "artifacts/tests/http-scenario-executions.json",
|
||||
"receiptSchemaVersion": 1
|
||||
}
|
||||
],
|
||||
"sourceContracts": [
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
# Manual accessibility review checklist
|
||||
|
||||
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
||||
review all six route records in `artifacts/tests/a11y-manual/` against one
|
||||
review all ten route records in `artifacts/tests/a11y-manual/` against one
|
||||
release candidate and sign them. The required scope is derived from the route
|
||||
registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`,
|
||||
`REFERENCE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the
|
||||
gate rejects blank identity/timestamp/signature fields, pending verdicts,
|
||||
mismatched release IDs, or missing routes.
|
||||
registry: `APP_HOME`, `EXAMPLES_PLATFORM`, `EXAMPLES_UI`, `EXAMPLES_STATES`,
|
||||
`EXAMPLES_AUTH`, `NOT_FOUND`, `REFERENCE_RESOURCE_LIST`,
|
||||
`REFERENCE_RESOURCE_DETAIL`, `REFERENCE_RESOURCE_FORM` and
|
||||
`REFERENCE_RESOURCE_STATUS`. Copy the template fields exactly; the gate rejects
|
||||
blank identity/timestamp/signature fields, pending verdicts, mismatched release
|
||||
IDs, or missing routes.
|
||||
|
||||
This list is not maintained by hand: `verify:documentation` compares it against
|
||||
the installed route registry and fails when a registered route is absent. It
|
||||
said six routes while ten were registered, which put the platform overview and
|
||||
the three reference-resource screens outside the declared manual review scope
|
||||
without anyone deciding they should be.
|
||||
|
||||
Allowed item verdicts:
|
||||
|
||||
|
||||
@@ -247,6 +247,8 @@ domain, application state, query cache, global store, diagnostics에 넣지 않
|
||||
### 3.2 picker baseline과 enhancement
|
||||
|
||||
- 접근 가능한 `<input type="file">`가 모든 browser의 canonical baseline이다.
|
||||
baseline adapter는 `input.click()`으로 이 경로를 활성화한다. `showPicker()`
|
||||
존재 여부는 portable baseline capability의 판정 기준으로 사용하지 않는다.
|
||||
- `showOpenFilePicker()`와 `showSaveFilePicker()`는 runtime method별 feature
|
||||
detection을 거친 progressive enhancement다. UA sniffing을 사용하지 않는다.
|
||||
- picker 호출은 click/keyboard handler의 첫 browser action이어야 한다. 그 전에
|
||||
@@ -690,11 +692,20 @@ default physical layout은 구현과 동일하게 다음과 같다.
|
||||
```text
|
||||
/ca-frontend-opfs-v1/
|
||||
authorities/<authorityToken>/<namespaceToken>/<partitionToken>/
|
||||
objects/<object-id-prefix>/<opaque-object-id>/<generation>/manifest.json
|
||||
objects/<object-id-prefix>/<opaque-object-id>/<generation>/manifest.json # physical v1 (read-only)
|
||||
objects/<object-id-prefix>/<opaque-object-id>/g<generation>-<token>/manifest.json # physical v2 (new writes)
|
||||
chunks/sha256/<digest-prefix>/<digest>.bin
|
||||
staging/<transaction-id>/receipt.json
|
||||
```
|
||||
|
||||
physical v2는 STO-01 수정의 일부다. logical `generation`은 설계상 transaction 간에
|
||||
재사용되므로, 늦게 도착한 T1 보상이 같은 logical generation을 쓰는 T2의 디렉터리를
|
||||
지울 수 있었다. v2는 transaction-unique `physicalGenerationId` fencing token을
|
||||
경로, staging receipt, prepared object에 함께 기록해 보상이 자기 transaction의
|
||||
디렉터리만 삭제하도록 만든다. expand 단계에서는 v1 경로/receipt/prepared object를
|
||||
계속 읽고 새 write만 v2로 쓴다. rollback window가 끝나기 전에 v1 physical
|
||||
generation을 일괄 삭제하지 않는다.
|
||||
|
||||
구조화 metadata, query, revision, refcount와 operation journal은 IndexedDB가
|
||||
소유한다. OPFS에는 immutable chunk와 bounded runtime-schema-validated manifest만
|
||||
둔다. readable `scope.namespace`는 경로에 쓰지 않는다.
|
||||
@@ -736,6 +747,23 @@ COMMITTED <- 사용자에게 보이는 유일한 commit point
|
||||
CLEANED -> journal 제거
|
||||
```
|
||||
|
||||
보상(compensation)은 saga의 반쪽이며 다음 규칙을 따른다.
|
||||
|
||||
- journal row와 budget reservation은 physical cleanup effect가
|
||||
`CLEANED` 또는 `ALREADY_CLEAN`으로 확인된 뒤에만 해제한다. timeout, crash,
|
||||
malformed response, `EFFECT_UNKNOWN`은 성공이 아니며 `PREPARING`/`FILES_READY`를
|
||||
그대로 남기고 `OBJECT_RECONCILE`로 반환한다.
|
||||
- coordinator가 `abortPreparedPut()` 하나만 소유한다. worker client는 prepare 실패
|
||||
시 별도의 fire-and-forget abort를 발행하지 않는다. 중복 보상은 아직 남아 있어야
|
||||
할 journal row를 조기에 지우는 경로였다.
|
||||
- 보상은 caller signal을 상속하지 않는다. composition이 소유한 bounded
|
||||
`compensationSignal`을 사용하므로 이미 abort된 caller가 cleanup RPC 자체를
|
||||
시작조차 못 하게 만들 수 없다.
|
||||
- abort/cleanup은 origin mutation Web Lock을 physical 삭제와 staging 제거가 끝날
|
||||
때까지 계속 보유한다. lease를 먼저 release하지 않는다. 단, staging이 아직 없는
|
||||
transaction은 삭제할 것이 없으므로 lock을 기다리지 않고 `ALREADY_CLEAN`을
|
||||
반환한다. 이는 자기 자신이 취소하는 BEGIN과의 deadlock을 막는다.
|
||||
|
||||
- `PREPARING` crash: partial staging을 검증 후 resume하거나 purge한다.
|
||||
- `FILES_READY` crash: expected generation과 digest가 맞으면 idempotent logical
|
||||
commit, 아니면 quarantine한다.
|
||||
@@ -961,7 +989,7 @@ recipe의 공통 code는 UI/telemetry에 raw DOMException을 노출하지 않기
|
||||
| `PERMISSION_DENIED` | picker/save permission | baseline/manual fallback |
|
||||
| `LIMIT_EXCEEDED` | count/bytes/page/buffer budget | 입력 축소 |
|
||||
| `POLICY_REJECTED` | type/cache/data policy | 저장·전송 금지 |
|
||||
| `BLOCKED` | IndexedDB older context | 다른 탭 close/retry UI |
|
||||
| `BLOCKED` | IndexedDB older context가 live open/upgrade를 막는 중 | 다른 탭 close/retry UI |
|
||||
| `CONFLICT` | revision/generation/idempotency conflict | authoritative re-read |
|
||||
| `MIGRATION_FAILED` | schema/data migration | read-only/online-only |
|
||||
| `QUOTA_EXCEEDED` | actual write failure | rollback, reconstructable GC, bounded retry |
|
||||
@@ -973,6 +1001,12 @@ recipe의 공통 code는 UI/telemetry에 raw DOMException을 노출하지 않기
|
||||
| `UNAVAILABLE` | browser/worker/storage temporarily unavailable | documented fallback |
|
||||
| `UNSUPPORTED` | capability absence | baseline/online-only |
|
||||
|
||||
`IndexedDbConnectionStatus.BLOCKED`는 대기 중인 live attempt의 observable
|
||||
상태다. blocked deadline이 끝나 `open()`이 `BLOCKED` failure로 settle되면
|
||||
그 attempt는 더 이상 열리는 중이 아니므로 status는
|
||||
`CLOSED / NOT_OPENED`로 전이한다. 늦게 성공한 native connection은 즉시 닫고
|
||||
settled status를 되살리지 않는다.
|
||||
|
||||
user dismissal은 failure가 아니라 outcome이다. browser DOMException name은
|
||||
adapter에서 이 vocabulary로 mapping하고 raw message/stack은 local bounded
|
||||
diagnostic에도 기본 저장하지 않는다.
|
||||
@@ -1059,18 +1093,17 @@ Playwright Chromium, Firefox, WebKit에서 실제 secure-origin API를 검사한
|
||||
`test:browser-capabilities`의 JUnit을
|
||||
`verify:browser-capability-evidence`가 읽어 세 engine의 testcase 집합 동일성,
|
||||
양수 실행 수, zero failure/error/skipped와 skipped/failure node 부재를 강제한다.
|
||||
현재 checkout의 source suite는 engine마다 정확히 같은 14개 case(File 2,
|
||||
IndexedDB 4, OPFS/Cache/StorageManager 각 1, cross-context invalidation 2,
|
||||
presigned streaming download/multipart upload/Image CDN 각 1)를 정의한다.
|
||||
promotion artifact는 Chromium/Firefox/WebKit의 14개씩, 총 42개가 모두
|
||||
실행되어야 한다. 이 host의
|
||||
WebKit은 필수 native libraries(예:
|
||||
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
|
||||
`libavif.so.16`과 WPE 계열) 부재로 실행되지 않았고 현재 보존 artifact도
|
||||
Chromium/Firefox 14개씩 총 28개만 통과한 상태다. 따라서 promotion evidence를
|
||||
충족하지 않으며 verifier가 실패하는 것이 정상이다. `INSTALLED` 전에는 필요한
|
||||
system dependency가 있는 CI/device에서 세 engine 전체 evidence를 새로 생성해야
|
||||
한다.
|
||||
browser capability promotion은 source suite에서 관찰된 동일 testcase set을
|
||||
Chromium/Firefox/WebKit에서 모두 실행하는 규칙으로 관리한다. case 수를 문서
|
||||
상수로 복제하지 않고 `verify:browser-capability-evidence`가 Chromium 결과를
|
||||
baseline set으로 계산해 Firefox/WebKit의 set 동일성, 양수 실행 수,
|
||||
failure/error/skipped 0과 failure/skipped node 부재를 검증한다.
|
||||
|
||||
capability absence도 skip 사유가 아니다. 예를 들어 어떤 engine에서
|
||||
`navigator.storage`가 없다면 동일 testcase 안에서 adapter의
|
||||
`UNSUPPORTED / STORAGE_ESTIMATE / ONLINE_ONLY` 결과를 browser truth로
|
||||
검증한다. 세 engine을 실행할 수 없는 host의 artifact는 promotion evidence로
|
||||
사용하지 않는다.
|
||||
|
||||
- native input keyboard/focus/same-file reselection/multiple/dismissal
|
||||
- Chromium conditional picker/save enhancement와 다른 engine fallback
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# Capability consumer experience baseline
|
||||
|
||||
Correctness inside a reusable capability is not sufficient. The platform is
|
||||
also evaluated by how much of that correctness a normal feature developer must
|
||||
understand.
|
||||
|
||||
These measurements are baselines, not score targets. A lower line count is not
|
||||
automatically better if it hides business semantics or creates a universal
|
||||
repository abstraction.
|
||||
|
||||
## Scenario A — reference REST feature
|
||||
|
||||
The existing reference feature is the executable REST consumer.
|
||||
|
||||
It covers list/detail/create behavior, mapping, typed operation inputs,
|
||||
application ports and an optimistic mutation path.
|
||||
|
||||
Current source size:
|
||||
|
||||
| Feature-owned area | LOC |
|
||||
| --- | ---: |
|
||||
| `application/reference-feature-api.ts` | 91 |
|
||||
| `contracts/reference-mapper.ts` | 109 |
|
||||
| `adapters/reference-http-gateway.ts` | 65 |
|
||||
| `adapters/create-reference-feature-input.ts` | 41 |
|
||||
| Total measured boundary/application source | 306 |
|
||||
|
||||
The important boundary metric is not the raw total. It is what those files need
|
||||
to know about the platform.
|
||||
|
||||
Current result:
|
||||
|
||||
- HTTP platform imports in the feature adapter layer: 2 files,
|
||||
- canonical import path used by both:
|
||||
`src/adapters/http/index.ts`,
|
||||
- direct imports of `http-execution-v3.ts`, retry scheduler, response reader,
|
||||
auth admission or effect-certainty internals: 0,
|
||||
- feature gateway owns transport failure projection: 0,
|
||||
- feature gateway chooses operation ID, route ID, exact input/value type and
|
||||
mapper: yes,
|
||||
- central installed files own feature-specific adapter/runtime wiring: no;
|
||||
feature-owned contributions are aggregated centrally.
|
||||
|
||||
The reusable HTTP capability now owns execution-outcome normalization through
|
||||
`createFeatureHttpBinding`. A feature may still implement a custom outbound
|
||||
adapter behind its application port when the reusable contract does not match
|
||||
its business requirement.
|
||||
|
||||
## Scenario B — IndexedDB local draft
|
||||
|
||||
The original executable consumer probe remains at:
|
||||
|
||||
`tests/contract/consumer-experience/indexeddb-local-draft.test.ts`
|
||||
|
||||
with its isolated fixture:
|
||||
|
||||
`tests/contract/consumer-experience/fixtures/local-draft-feature.ts`
|
||||
|
||||
The architecture is now also exercised by a real vertical slice under:
|
||||
|
||||
`src/features/local-draft-feature`
|
||||
|
||||
It owns the Local Draft domain/application API and binds an
|
||||
`IndexedDbRepositoryPort<LocalDraft, never>` in its feature adapter. The
|
||||
cross-capability composition contract lives at:
|
||||
|
||||
`tests/contract/reusable-capability/feature-adapter-composition.test.ts`
|
||||
|
||||
That test composes one HTTP-only contribution and the IndexedDB-only Local Draft
|
||||
contribution through the same generic catalog path.
|
||||
|
||||
The browser-level composition proof lives at:
|
||||
|
||||
`tests/browser-capabilities/local-draft-composition.spec.ts`
|
||||
|
||||
It runs against native browser IndexedDB. The test creates the platform
|
||||
`createIndexedDbRuntime`, exposes that runtime through the typed
|
||||
`createIndexedDbRepositoryProvider`, composes
|
||||
`LOCAL_DRAFT_FEATURE_ADAPTER_CONTRIBUTION`, then executes Local Draft
|
||||
save/find/remove through the feature API. The Local Draft feature still does
|
||||
not import the IndexedDB runtime or native browser API.
|
||||
|
||||
Measured probe result:
|
||||
|
||||
| Metric | Result |
|
||||
| --- | ---: |
|
||||
| feature-owned fixture LOC | 97 |
|
||||
| platform import statements | 1 |
|
||||
| native IndexedDB API references | 0 |
|
||||
| `src/adapters/storage/indexeddb/**` imports | 0 |
|
||||
| runtime-internal IndexedDB types imported by feature | 0 |
|
||||
|
||||
The feature depends only on the public application boundary:
|
||||
|
||||
`src/application/ports/browser-file-storage/index.ts`
|
||||
|
||||
and specifically `IndexedDbRepositoryPort<LocalDraft, never>`.
|
||||
|
||||
The contract test rejects feature source that reaches for
|
||||
`globalThis.indexedDB`, `IDBFactory`, `IDBDatabase`, `IDBTransaction`,
|
||||
`IDBObjectStore` or the concrete IndexedDB adapter directory.
|
||||
|
||||
## What this does and does not prove
|
||||
|
||||
The contract probe confirms that **feature business code does not need native
|
||||
IndexedDB knowledge** once an `IndexedDbRepositoryPort` has been composed.
|
||||
The browser-capability proof additionally confirms that the same feature
|
||||
contribution works when that port is backed by the repository's real
|
||||
`createIndexedDbRuntime` and native IndexedDB implementation.
|
||||
|
||||
It does not prove that composition of `createIndexedDbRuntime` is cheap.
|
||||
That constructor still owns substantial infrastructure policy:
|
||||
|
||||
- dataset scope and storage policy,
|
||||
- physical store governance,
|
||||
- retention/idempotency stores,
|
||||
- schema migrations,
|
||||
- codec and query policy,
|
||||
- lifecycle authority,
|
||||
- durability, scheduling and observation.
|
||||
|
||||
That complexity belongs at the composition/platform boundary, not in the
|
||||
feature. The Local Draft vertical slice is now the second concrete consumer, and
|
||||
it confirms that the stable seam is the typed repository provider plus a
|
||||
feature-owned repository identity. It does **not** show that dataset scope,
|
||||
retention, migration, codec or lifecycle-authority configuration can be safely
|
||||
collapsed into one universal `create...Repository<T>` factory.
|
||||
|
||||
A convenience profile should therefore be introduced only after another
|
||||
IndexedDB-backed product feature repeats the same infrastructure policy, not
|
||||
merely because two features consume the same repository port.
|
||||
|
||||
## Consumer-quality review checklist
|
||||
|
||||
For each new product feature, record:
|
||||
|
||||
- feature-owned adapter LOC,
|
||||
- platform glue LOC,
|
||||
- files changed,
|
||||
- central catalog edits,
|
||||
- direct imports from capability-internal modules,
|
||||
- native browser/network API references,
|
||||
- duplicated failure/retry/lifecycle policy.
|
||||
|
||||
A healthy feature path should look like:
|
||||
|
||||
1. domain type and invariant,
|
||||
2. use case,
|
||||
3. business port,
|
||||
4. feature-owned mapper/codec and policy,
|
||||
5. capability-specific binding,
|
||||
6. presentation controller/page.
|
||||
|
||||
The feature should not need the retry scheduler, abort ownership,
|
||||
effect-certainty machinery, IndexedDB transaction lifecycle, OPFS journal,
|
||||
reconnect coordinator or provider process model.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Contract ownership
|
||||
|
||||
`src/contracts` is not a default destination for every shared-looking type.
|
||||
A contract belongs there only when the change authority is genuinely shared
|
||||
across layers, capabilities, build tooling, or runtime composition.
|
||||
|
||||
## Decision rule
|
||||
|
||||
For every proposed contract, ask:
|
||||
|
||||
1. Which requirement can cause this type or policy to change?
|
||||
2. Is there one clear capability or feature owner?
|
||||
3. Does another runtime/tooling boundary consume the same semantic contract?
|
||||
4. Is the contract a wire/artifact authority shared by browser runtime and
|
||||
build/release tooling?
|
||||
|
||||
The placement rule is:
|
||||
|
||||
- one feature owner -> keep it under that feature,
|
||||
- one reusable capability owner -> keep it under that adapter/capability and
|
||||
export it through the capability public entry point,
|
||||
- multiple independent capability/layer owners -> `src/contracts`,
|
||||
- browser/build/release wire or artifact authority -> `src/contracts` even
|
||||
when the browser source graph alone looks small.
|
||||
|
||||
Consumer count alone is not sufficient. Scripts, generated artifacts and
|
||||
release gates are semantic consumers too.
|
||||
|
||||
## Audit result
|
||||
|
||||
The September 2026 architecture review triggered an import-graph audit of all
|
||||
39 contract files.
|
||||
|
||||
### Capability-owned contract moved
|
||||
|
||||
`cursor-pagination.ts` had one production owner:
|
||||
`src/adapters/query-cache/cursor-pagination-runtime.ts`.
|
||||
|
||||
It moved to:
|
||||
|
||||
`src/adapters/query-cache/cursor-pagination-contract.ts`
|
||||
|
||||
and is exported through:
|
||||
|
||||
`src/adapters/query-cache/index.ts`
|
||||
|
||||
Tests use that public capability entry point. Pagination vocabulary no longer
|
||||
occupies the global contract bucket merely because it is reusable inside one
|
||||
adapter.
|
||||
|
||||
### Contracts intentionally kept global
|
||||
|
||||
The following examples have multiple semantic owners and remain global:
|
||||
|
||||
- `errors.ts` — application, presentation and several adapters,
|
||||
- `result.ts` — common success/failure carrier below application,
|
||||
- `boundary-mapper.ts` — HTTP, browser RPC, realtime and feature registries,
|
||||
- `mutation-intent.ts` — application, presentation, HTTP, platform and
|
||||
bootstrap,
|
||||
- `exact-snapshot.ts` — HTTP, query-cache and browser-transfer,
|
||||
- `rest-profiles.ts` — auth, HTTP, bootstrap and feature contract validation,
|
||||
- `query-invalidation.ts` / `query-keys.ts` — query-cache, presentation,
|
||||
bootstrap and features,
|
||||
- `cache-invalidation.ts` — cross-context wire protocol plus query
|
||||
invalidation policy,
|
||||
- `storage-keys.ts` — browser storage and cross-context invalidation,
|
||||
- `telemetry.ts` / `diagnostics.ts` — runtime adapters, application and
|
||||
bootstrap.
|
||||
|
||||
Some contracts appear to have few browser-source consumers but are still shared
|
||||
authorities:
|
||||
|
||||
- `env.ts` is consumed by bootstrap, runtime-schema/security tests and
|
||||
registry governance,
|
||||
- `deployment-admission.ts` is shared by runtime-config generation and release
|
||||
admission,
|
||||
- `release-tokens.ts` is shared by runtime coherence tooling and tests,
|
||||
- `service-worker-static-manifest.ts` is a runtime-neutral canonical format
|
||||
shared by build generation, validation and service-worker evidence.
|
||||
|
||||
Moving those based only on `src/**` import counts would split one semantic
|
||||
authority across processes.
|
||||
|
||||
## Feature contracts
|
||||
|
||||
Feature-specific contracts stay inside the vertical slice:
|
||||
|
||||
`features/<feature>/contracts`
|
||||
|
||||
The reference feature owns routes, schemas, mapper definitions, message
|
||||
catalogs and contribution identities. Central installed files aggregate those
|
||||
contributions; they do not own their semantics.
|
||||
|
||||
The message catalog is a deliberate special case: the compiled catalog remains
|
||||
total even when a feature is build-time disabled so the typed message lookup
|
||||
does not become partial. The central message file therefore aggregates compiled
|
||||
message keys rather than treating runtime installation as message ownership.
|
||||
|
||||
## Review rule for future additions
|
||||
|
||||
A new file under `src/contracts` should be rejected during review when all of
|
||||
the following are true:
|
||||
|
||||
- one feature or one capability is the only semantic owner,
|
||||
- no build/release/runtime wire authority needs the same definition,
|
||||
- moving the definition to that owner does not create an inward dependency
|
||||
violation.
|
||||
|
||||
Do not move a contract merely to reduce the number 39. The goal is explicit
|
||||
ownership, not a smaller directory.
|
||||
@@ -37,12 +37,41 @@ vendor 결정 전에는 안전한 기본값이 아니다.
|
||||
abort마다 `http.request.completed` diagnostics를 정확히 한 번 남긴다.
|
||||
`api.request.failed` telemetry는 retry가 끝난 terminal non-abort failure에만
|
||||
정확히 한 번 발행한다.
|
||||
5-1. V2 client와 V3 contract executor는 각각 자신의 logical execution에 대해
|
||||
이 규칙을 만족한다. V3에서는 execution site가 `HttpExecutionObservation`
|
||||
typed record 하나만 만들고, composition root의
|
||||
`createHttpObservationProjector`가 유일한 projection authority다. observation은
|
||||
arbitrary context map이 아니며 projector는 `route_id`, `operation_id`,
|
||||
`operation`, `outcome`, `error_kind`, `http_status_group`,
|
||||
`attempt_count_bucket`, `duration_bucket`만 사용한다. raw attempt count,
|
||||
duration, status, URL, intent, key, input identity와 내부 `terminalReason`은
|
||||
sink로 나가지 않는다. effect certainty가 운영상 필요해지면 `effect_certainty`
|
||||
key와 닫힌 value policy를 contract·fixture·이 ADR에 동시에 추가한 뒤에만
|
||||
전달한다.
|
||||
5-2. caller cancellation과 scope fence는 API failure가 아니다. diagnostics는 한
|
||||
번 남기고 `api.request.failed`는 발행하지 않는다.
|
||||
5-3. `routeId`는 installed operation-executor 경계의 필수 입력이다. feature
|
||||
gateway가 소유한 low-cardinality route identity를 URL에서 재구성하지 않는다.
|
||||
6. `app.boot.failed`, `ui.render.failed`, `release.mismatch.detected`,
|
||||
`telemetry.delivery.dropped`를 production path에 연결한다. cache와 storage
|
||||
실패는 diagnostics로 기록하되 raw key/value를 기록하지 않는다.
|
||||
7. queue full, invalid event/context, serialization과 sink failure는 제한된
|
||||
reason bucket으로 집계한다. drop observer의 failure는 다시 telemetry를
|
||||
발행하지 않는 nonrecursive 경계다.
|
||||
7-1. telemetry adapter lifecycle은 `ACTIVE | DISPOSED` 둘뿐이다. `dispose()`는
|
||||
한 번만 전이하고 `pagehide` listener 제거, queue 비우기, scheduled callback
|
||||
generation 무효화, in-flight sink `AbortController` abort를 모두 수행한다.
|
||||
dispose 뒤 `emit()`은 no-op이고 새 flush는 스케줄되지 않으며, abort를 무시한
|
||||
sink가 늦게 settle해도 post-dispose delivery state를 갱신하거나 재스케줄하지
|
||||
못한다. 종료 중 drop telemetry를 재귀적으로 발행하지 않는다.
|
||||
7-2. `flush()`는 active delivery promise를 join한다. 이미 진행 중인 flush가
|
||||
있으면 같은 promise를 반환하므로 `await flush()`는 실제 settle을 뜻한다.
|
||||
7-3. runtime `infrastructure.dispose()`는 diagnostics/state dependency를 파괴하기
|
||||
전에 `telemetry.dispose()`를 먼저 호출한다.
|
||||
7-4. queue/entry capacity는 construction-time 계약이다. `Number.isSafeInteger`가
|
||||
아니거나 1 미만이거나 문서화된 ceiling(각각 `MAX_TELEMETRY_QUEUE`,
|
||||
`MAX_DIAGNOSTIC_ENTRIES` = 10,000)을 넘으면 `TypeError`로 거절한다. NaN/Infinity가
|
||||
조용히 eviction을 비활성화하는 경로를 남기지 않는다.
|
||||
8. diagnostics와 telemetry failure는 제품 흐름, HTTP 결과, route transition,
|
||||
storage/cache fallback과 React error surface를 바꾸지 않는다.
|
||||
9. mount 전 bootstrap failure는 안전한 build/config/error kind만 별도 evidence로
|
||||
@@ -79,6 +108,11 @@ route/application/HTTP/cache/storage/bootstrap
|
||||
queue full, sink/observer failure와 pre-mount boot evidence를 검증한다.
|
||||
- HTTP integration은 success, retry recovery, terminal failure와 abort의 producer
|
||||
횟수, route/operation/correlation context와 요청 값 비노출을 검증한다.
|
||||
- `tests/integration/http-execution-v3-observability.test.ts`는 V3 terminal
|
||||
outcome이 실제로 closed allowlist를 통과하는지, terminal non-abort failure가
|
||||
`api.request.failed`를 정확히 한 번 발행하는지, cancellation/scope fence가
|
||||
발행하지 않는지, feature route ID가 executor 경계까지 보존되는지, sink 예외가
|
||||
HTTP 결과를 바꾸지 못하는지를 검증한다.
|
||||
- cache/storage/release/application/runtime test는 각 production wiring과
|
||||
diagnostics failure isolation을 검증한다.
|
||||
|
||||
|
||||
@@ -36,19 +36,26 @@ finding과 서명 성공으로 표현하면 local 검증과 release promotion을
|
||||
5. inventory를 CycloneDX 1.6 SBOM으로 투영한다. component 수, lockfile digest,
|
||||
SRI, license와 dependency edge가 inventory와 일치해야 한다.
|
||||
6. local in-toto/SLSA 형태 provenance statement는 source set, lockfile, SBOM과
|
||||
`dist` digest를 연결하되 `LOCAL_UNSIGNED`로 표시한다. 외부 attestation은
|
||||
provider, signer와 동일 dist subject digest가 있어야 한다.
|
||||
7. vulnerability adapter는 `VULNERABILITY_REPORT_PATH`가 가리키는
|
||||
machine-readable provider report를 검증한다. report의 lock digest, provider,
|
||||
severity와 exception owner/reviewer/reason/expiry가 유효해야 한다.
|
||||
8. provider report가 없으면 local inventory/license/SBOM/coherence는 `PASS`,
|
||||
promotion은 `FAIL_UNVERIFIED`다. 빈 finding을 만들어 vulnerability PASS로
|
||||
표시하지 않는다.
|
||||
9. secret scan은 source, scripts, tests, tracked config/schema, public, `dist`와
|
||||
`dist` digest를 연결하되 `LOCAL_UNSIGNED`로 표시한다. 이 문서는 외부
|
||||
provenance를 대신할 수 없다.
|
||||
7. `immutable_build`는 raw `pnpm-lock.yaml`, `dist`, build/module inventory와
|
||||
모든 local verification evidence를 한 번만 archive한다. Candidate manifest는
|
||||
raw lock bytes SHA-256, dependency inventory lock digest와 manifest
|
||||
`lockfileSha256`의 exact 일치를 요구한다.
|
||||
8. 두 provider job은 동일 archive를 각각 받아 외부 command를 실행한다.
|
||||
Vulnerability report는 raw lock digest와 `distSha256`, provenance attestation은
|
||||
`{name: "dist", digest.sha256}`를 포함한다. 두 문서 모두 strict schema와
|
||||
별도 trust path/key ID로 선택한 실제 Ed25519 public key 서명을 통과해야 한다.
|
||||
9. provider report나 trusted key가 없으면 local
|
||||
inventory/license/SBOM/coherence는 `PASS`, promotion은
|
||||
`FAIL_UNVERIFIED`다. 저장소 generator나 fixture가 production용 빈 finding 또는
|
||||
signed PASS를 만들지 않는다.
|
||||
10. secret scan은 source, scripts, tests, tracked config/schema, public, `dist`와
|
||||
generated release metadata를 검사한다. allowlist는 test path에만 허용하며
|
||||
owner, reason과 expiry가 필요하다. 발견한 secret 원문은 artifact에 쓰지 않고
|
||||
rule, path, line과 fingerprint만 남긴다.
|
||||
10. `SOURCE_DATE_EPOCH`를 지원하고 같은 source/lock/config의 production build를
|
||||
11. `SOURCE_DATE_EPOCH`를 지원하고 supply-chain timestamp도 build manifest의
|
||||
동일 epoch에 결합한다. 같은 source/lock/config의 production build를
|
||||
두 번 실행해 전체 dist digest 일치를 검증한 뒤 일반 build를 복원한다.
|
||||
|
||||
## 실행 경계와 증적
|
||||
@@ -61,8 +68,10 @@ package.json + frozen pnpm-lock.yaml + installed graph
|
||||
|
||||
source/config/lock + production dist
|
||||
-> local provenance statement
|
||||
-> optional vulnerability/attestation provider inputs
|
||||
-> LOCAL PASS | promotion PASS/FAIL_UNVERIFIED
|
||||
-> immutable archive + candidate manifest + distSha256
|
||||
-> external vulnerability provider + external provenance provider
|
||||
-> read-only local revalidation + signature/digest verification
|
||||
-> promotion PASS | FAIL_UNVERIFIED
|
||||
```
|
||||
|
||||
- policy: `config/security/`
|
||||
@@ -82,8 +91,9 @@ source/config/lock + production dist
|
||||
- ordering-only digest, removal, integrity tamper, baseline tamper, high-risk
|
||||
self approval, denied license, critical vulnerability와 만료 exception,
|
||||
provider/digest 오류, SBOM/provenance 불일치 fixture를 검사한다.
|
||||
- synthetic provider/attestation fixture는 promotion `PASS`를 증명한 후 기본
|
||||
`FAIL_UNVERIFIED` 상태를 복원한다.
|
||||
- isolated temporary candidate/PEM/report fixture는 실제 environment path wiring을
|
||||
통해 valid immutable 입력만 promotion `PASS`임을 증명한다. Production artifact를
|
||||
덮어쓰거나 generator를 provider 모드로 재실행하지 않는다.
|
||||
- frozen install은 manifest/lock mismatch fixture를 실제 pnpm으로 거절한다.
|
||||
- source/config/dist 각각의 synthetic secret fixture가 실제 scan을 실패시키고
|
||||
scoped test allowlist만 통과한다.
|
||||
@@ -97,7 +107,7 @@ attestation transparency/retention과 비밀 관리를 결정해야 한다.
|
||||
|
||||
## Rollback
|
||||
|
||||
외부 scanner/attestor adapter는 환경 입력을 제거하면 즉시
|
||||
외부 scanner/attestor command, report path 또는 trusted key 설정을 제거하면 즉시
|
||||
`FAIL_UNVERIFIED`로 돌아간다. local inventory, lock integrity, license, SBOM,
|
||||
secret, reproducibility와 actual diff gate는 유지한다. scanner 장애를 이유로
|
||||
promotion을 PASS로 변경하지 않는다.
|
||||
|
||||
@@ -189,17 +189,18 @@ chunk별 `CapabilityResult<Uint8Array>`를 반환한다. backend upload example
|
||||
실제 upload feature는 이 예시를 그대로 import하지 않고 purpose와 backend
|
||||
protocol에 맞게 contract를 더 좁힌다.
|
||||
|
||||
현재 checkout의 browser source suite는 engine마다 같은 14개 case(File 2,
|
||||
IndexedDB 4, OPFS/Cache/StorageManager 각 1, cross-context invalidation 2,
|
||||
presigned streaming download/multipart upload/Image CDN 각 1)를 정의한다.
|
||||
promotion artifact는 Chromium/Firefox/WebKit 각각 14개, 총 42개를 모두
|
||||
실행해야 한다. WebKit은 현재
|
||||
host의 필수 native libraries(예:
|
||||
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
|
||||
`libavif.so.16`과 WPE 계열) 부재로 실행되지 않았다. 보존 artifact는
|
||||
Chromium/Firefox 14개씩 총 28개만 통과했으므로
|
||||
`verify:browser-capability-evidence`가 실패하는 것이 정상이다. 세 engine
|
||||
evidence가 완성되기 전에는 product 상태를 `INSTALLED`로 올리지 않는다.
|
||||
browser capability promotion은 source suite가 정의한 동일 testcase set을
|
||||
Chromium/Firefox/WebKit에서 모두 실행해야 한다. 구체적인 case 개수는 이 결정
|
||||
문서에 고정하지 않는다. `verify:browser-capability-evidence`가 Chromium
|
||||
artifact에서 baseline set을 계산하고 Firefox/WebKit과의 set 동일성 및
|
||||
failure/error/skipped 0을 기계적으로 검증한다.
|
||||
|
||||
engine별 native capability 차이는 testcase를 skip하는 이유가 아니다. capability가
|
||||
없으면 adapter의 명시적 unsupported/degraded result를 같은 testcase에서
|
||||
검증한다. baseline `<input type="file">` activation은 portable한
|
||||
`input.click()` 경로를 사용하고, `showOpenFilePicker()` 같은 API는 별도
|
||||
progressive enhancement로 유지한다. 세 engine evidence가 완성되기 전에는
|
||||
product 상태를 `INSTALLED`로 올리지 않는다.
|
||||
|
||||
## 선택 이후 필요한 구현
|
||||
|
||||
|
||||
@@ -262,6 +262,55 @@ auth-required operation은 session state가 `authenticated`가 아니면 fetch
|
||||
`integration-failed`, `unauthenticated`와 credential attach rejection을 anonymous
|
||||
request로 downgrade하지 않는다.
|
||||
|
||||
#### 5-0. Logical effect certainty는 단조 증가한다
|
||||
|
||||
`PhysicalAttemptState`는 현재 attempt만 설명한다. logical execution 전체에는
|
||||
별도의 monotonic accumulator를 두고 `joinMutationEffectCertainty`로 join한다.
|
||||
join 순서는 보수적이다.
|
||||
|
||||
```text
|
||||
NOT_STARTED < NOT_APPLIED < MAYBE_APPLIED < APPLIED_CONFIRMED
|
||||
```
|
||||
|
||||
`fetch()` dispatch 시점에 command는 즉시 `MAYBE_APPLIED`를 기록한다. 이후 retry
|
||||
loop entry, pre-dispatch final invariant, scope fence, cancellation, timeout
|
||||
return은 모두 accumulator를 읽는다. 아직 보내지 않은 새 retry가 있다는 이유로
|
||||
전체 logical operation을 `NOT_STARTED`로 되돌리지 않는다. query operation은
|
||||
`NOT_APPLICABLE`로 남고 이 lattice를 쓰지 않는다.
|
||||
|
||||
#### 5-1. Installed auth profile registry (V3 집행)
|
||||
|
||||
`installRestAuthProfileRegistry()`가 composition 시점에 profile을 한 번 설치하고
|
||||
`INSTALLED_REST_AUTH_PROFILES`가 유일한 authority다. contract composition
|
||||
(`assertExecutionPolicy`)은 등록되지 않은 `authProfileId`를 거절하므로 executor는
|
||||
runtime에 profile을 발명하지 않는다. profile은 다음을 exact하게 소유한다.
|
||||
|
||||
- Fetch `credentials` (credential collaborator가 바꿀 수 없다)
|
||||
- `allowedCredentialHeaders`: 이 operation이 허용하는 정확한 proof header 집합
|
||||
- `requiredCredentialHeaders`: dispatch 전에 반드시 관찰되어야 하는 집합
|
||||
|
||||
`CredentialPatchOutcome.READY`는 proof header만 담는다. `credentials` field는
|
||||
제거되었다. credential owner가 transport-owned header(`accept`, `content-type`,
|
||||
`idempotency-key`)나 forbidden header를 넣거나, profile이 허용하지 않는 header를
|
||||
넣거나, required header를 빠뜨리면 `AUTH_INTEGRATION_FAILURE`이고 fetch 0회이며
|
||||
command effect는 `NOT_STARTED`다. `idempotency-key`는 contract-owned이므로 더
|
||||
구체적인 `UNEXPECTED_IDEMPOTENCY_KEY` request violation으로 남는다.
|
||||
`UNAUTHENTICATED`는 user/session state이지 integration failure가 아니다.
|
||||
|
||||
transport-owned header는 credential header 뒤에 기록되어 key ordering으로도
|
||||
shadow될 수 없고, final invariant가 `init.credentials`와 profile을 다시 대조하며
|
||||
allowed/required credential header 집합을 독립적으로 재검증한다.
|
||||
|
||||
`AUTH_MODE=demo`는 profile을 약화시키지 않는다. `createDemoSessionAdapter`가
|
||||
고정된 비밀 아닌 `DEMO_AUTHORIZATION_MARKER` proof header를 제공하여 strict
|
||||
`REFERENCE_EXTERNAL_BEARER`를 그대로 만족시킨다. 진짜 anonymous backend는 별도
|
||||
anonymous contract/profile을 composition에서 선택해야 한다.
|
||||
|
||||
credential collaborator는 `AuthOperationContext { signal, deadlineAtMonotonicMs }`를
|
||||
받는다. cooperative owner는 스스로 중단하고, non-cooperative owner도 executor가
|
||||
같은 lifetime signal과 race하므로 operation 수명을 넘기지 못하며 late completion은
|
||||
관찰되지 않는다.
|
||||
|
||||
### 6. Cookie auth, CSRF와 CORS
|
||||
|
||||
same-origin BFF cookie session을 기본 권장한다.
|
||||
|
||||
@@ -11,6 +11,26 @@
|
||||
첫 제품 stream/Web Push를 선택할 때, 또는 backend replay/hosting/provider
|
||||
protocol이 바뀔 때
|
||||
|
||||
|
||||
## 스트림 lifecycle은 freshness와 직교한다 (R-02, R-03)
|
||||
|
||||
`RealtimeStreamLifecycle = OPEN | DRAINING | CLOSED`는 freshness
|
||||
(`UNKNOWN/CURRENT/STALE/RESYNCING`)와 별개다.
|
||||
|
||||
- effect/recovery deadline에 도달하면 commit capability를 즉시 영구 무효화하고
|
||||
abort한다. caller에는 bounded `IDLE_TIMEOUT`(non-retryable, operation
|
||||
`APPLY`/`RECOVER`)을 반환하되 **실제 task는 버리지 않고 retain**한다.
|
||||
- retain된 task가 하나라도 있으면 stream은 `DRAINING`이고 새 event/recovery
|
||||
admission을 거절한다. 실제 settlement가 일어나야 `STALE`로 돌아가
|
||||
authoritative recovery를 요구하거나, close 요청이면 `CLOSED`가 된다.
|
||||
- `close()`는 `Promise<RealtimeResult<void>>`다. 모든 retain task가 실제로
|
||||
settle해야 success이고, drain bound를 넘기면 `IDLE_TIMEOUT/CLOSE`를 반환하며
|
||||
stream은 계속 `DRAINING`이다. teardown success가 곧 quiescence다.
|
||||
- LIVE↔POLL overflow fail-close는 active/probe/quiescing/transition lease를
|
||||
모두 abort한 뒤 **retired writer set**으로 옮기고 나서 reference를 지운다.
|
||||
`close()`는 current와 retired를 dedupe해 함께 기다리므로, 버려진
|
||||
non-cooperative writer가 아직 실행 중인데 close가 성공을 보고할 수 없다.
|
||||
|
||||
## 배경
|
||||
|
||||
현재 optional recipe catalog는 realtime capability에
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
# Frontend Application Foundation
|
||||
|
||||
This repository is not treated as a minimal React project template and it is not
|
||||
an independent general-purpose SDK. Its architectural role is:
|
||||
|
||||
> Frontend Application Foundation = Starter / Composition Skeleton + Reusable Capability Platform
|
||||
|
||||
The starter side owns bootstrap, routing, providers, project conventions and a
|
||||
removable reference feature. The capability side owns reusable technical
|
||||
problems such as HTTP execution, Server State, authentication boundaries,
|
||||
IndexedDB/OPFS, Cache Storage, realtime, browser RPC, transfer and diagnostics.
|
||||
|
||||
The cost of a sophisticated capability is acceptable only when product features
|
||||
do not have to understand that internal sophistication.
|
||||
|
||||
## Hybrid architecture
|
||||
|
||||
Platform code is horizontal:
|
||||
|
||||
- `application`: generic application inputs/policies/ports
|
||||
- `contracts`: genuinely cross-capability shared vocabulary and registries
|
||||
- `adapters`: reusable capability runtimes
|
||||
- `presentation`: generic UI/routing/query integration
|
||||
- `bootstrap`: concrete composition
|
||||
|
||||
Product business code is vertical:
|
||||
|
||||
- `features/<feature>/domain`
|
||||
- `features/<feature>/application`
|
||||
- `features/<feature>/contracts`
|
||||
- `features/<feature>/adapters`
|
||||
- `features/<feature>/presentation`
|
||||
|
||||
The dependency model is:
|
||||
|
||||
Domain / Use case
|
||||
|
|
||||
| owns
|
||||
v
|
||||
Business port
|
||||
^
|
||||
| implements / binds
|
||||
|
|
||||
Feature-owned adapter binding
|
||||
|
|
||||
| generic type + mapper/codec + policy
|
||||
v
|
||||
Reusable capability runtime
|
||||
|
|
||||
v
|
||||
Browser / network / native API
|
||||
|
||||
A use case never imports a platform adapter. Generic binding happens in the
|
||||
feature adapter/composition seam.
|
||||
|
||||
## Capability-specific typed bindings
|
||||
|
||||
Do not introduce one universal `Repository<TKey, TValue>` abstraction for HTTP,
|
||||
storage, realtime and transfer. Their lifecycle and failure semantics differ.
|
||||
|
||||
A reusable capability boundary is composed from:
|
||||
|
||||
- generic input/output types,
|
||||
- feature-owned mapper or codec,
|
||||
- feature-selected policy,
|
||||
- one capability-specific runtime.
|
||||
|
||||
The HTTP reference path is the first concrete example.
|
||||
`src/adapters/http/feature-http-binding.ts` owns transport/outcome
|
||||
normalization. The reference feature contributes only:
|
||||
|
||||
- operation ID,
|
||||
- route ID,
|
||||
- exact request input type,
|
||||
- exact success value type,
|
||||
- wire-to-domain mapper.
|
||||
|
||||
The feature gateway therefore does not reimplement timeout, cancellation,
|
||||
transport failure, authentication failure or contract-violation projection.
|
||||
|
||||
Storage, realtime and transfer may gain their own typed binders only after
|
||||
actual feature repetition demonstrates the need. They must not be forced
|
||||
through the HTTP abstraction.
|
||||
|
||||
## Custom adapter escape hatch
|
||||
|
||||
A product feature uses a reusable capability when the capability preserves the
|
||||
business requirement.
|
||||
|
||||
If a platform contract would require changing or weakening the business model,
|
||||
the feature implements a custom outbound adapter behind the same application
|
||||
port. The architecture boundary remains stable; platform reuse is optional.
|
||||
|
||||
## Feature installation
|
||||
|
||||
A feature owns its contract, runtime and adapter contributions.
|
||||
|
||||
Central installed catalogs are aggregation points only:
|
||||
|
||||
- `installed-product-manifest.ts`: which product features are compiled/selected
|
||||
- `installed-feature-contracts.ts`: contract aggregation
|
||||
- `installed-feature-runtimes.tsx`: runtime contribution aggregation
|
||||
- `installed-feature-adapters.ts`: application-input contribution aggregation
|
||||
|
||||
Feature-specific composition belongs under the feature itself. Central
|
||||
catalogs must not grow feature-specific branching logic.
|
||||
|
||||
Adapter contributions declare the platform capabilities they consume through
|
||||
`needs`. The generic contribution seam in
|
||||
`src/features/feature-adapter-contribution.ts` derives the context from that
|
||||
list, so an HTTP-only feature cannot reach IndexedDB and an IndexedDB-only
|
||||
feature does not receive the HTTP executor. It also binds
|
||||
`featureId -> ApplicationFeatureInputs[featureId]` at the contribution
|
||||
definition site instead of recovering that relationship with a final catalog
|
||||
cast.
|
||||
|
||||
A contribution becomes composable only through
|
||||
`defineFeatureAdapterContribution()`. That factory adds the private
|
||||
contribution brand required by `composeFeatureAdapterInputs()`; a raw object
|
||||
with the same visible fields is rejected by TypeScript and checked again at
|
||||
runtime. The negative type fixture
|
||||
`invalid-direct-feature-composition.ts` is part of FE-GATE-003 so this
|
||||
authority cannot be bypassed by calling the composer directly.
|
||||
|
||||
For IndexedDB-backed features, `createIndexedDbRepositoryProvider()` is the
|
||||
composition helper that maps feature-owned repository identities to typed
|
||||
`IndexedDbRepositoryPort` instances. It does not create a universal storage
|
||||
repository or move dataset/migration/lifecycle policy into the feature.
|
||||
|
||||
The repository now has two executable contribution shapes:
|
||||
|
||||
- Reference feature: `needs: ["http"]`
|
||||
- Local Draft feature: `needs: ["indexedDb"]`
|
||||
|
||||
Local Draft is a compiled architecture consumer used to prove the second
|
||||
capability shape; it is not added to the default product manifest. Its
|
||||
browser-capability test composes the feature over the real
|
||||
`createIndexedDbRuntime` and native IndexedDB, proving that this seam is not
|
||||
limited to an in-memory contract fixture.
|
||||
|
||||
## Presentation consumer surface
|
||||
|
||||
`ApplicationProvider` remains the composition root for presentation, but new
|
||||
consumers should not navigate a root `ApplicationApi` service locator.
|
||||
|
||||
Use the narrow hooks in
|
||||
`src/presentation/providers/application-provider.tsx`:
|
||||
|
||||
- `useApplicationSession`
|
||||
- `useApplicationPreferences`
|
||||
- `useApplicationDiagnostics`
|
||||
- `useApplicationRuntime`
|
||||
- `useApplicationRecovery`
|
||||
- `useApplicationFeature`
|
||||
|
||||
`useApplication` exists only as a deprecated compatibility escape hatch.
|
||||
|
||||
## Canonical imports
|
||||
|
||||
New feature code should use capability public entry points rather than deep
|
||||
runtime modules. For HTTP the canonical path is
|
||||
`src/adapters/http/index.ts`.
|
||||
|
||||
Compatibility re-exports may exist during a migration window, but they must be
|
||||
marked as compatibility/deprecated paths and should not expand into an
|
||||
unbounded public barrel.
|
||||
|
||||
## Policy ownership
|
||||
|
||||
Duplication is judged by ownership, not by syntax percentage.
|
||||
|
||||
Small local validators can remain duplicated when locality improves auditing.
|
||||
Business or concurrency policy must have one owner. For example the reference
|
||||
create mutation keeps definition ID, idempotency requirement, duplicate policy
|
||||
and invalidation policy in one feature-owned definition and binds only
|
||||
`scope` and `execute` per usage site.
|
||||
|
||||
## Runtime decomposition rule
|
||||
|
||||
Large runtime files are not split by line count.
|
||||
|
||||
Extract a boundary when it has its own state machine, lifecycle owner, failure
|
||||
model or compensation/recovery responsibility. Candidate seams include:
|
||||
|
||||
- connection/open/upgrade lifecycle,
|
||||
- transaction ownership,
|
||||
- migration state machine,
|
||||
- reconnect/backoff and heartbeat,
|
||||
- subscription ownership,
|
||||
- retry/deadline/cancellation ownership,
|
||||
- settlement/reconciliation/cleanup.
|
||||
|
||||
A cohesive 2,000-line state machine can remain together. A 300-line file with
|
||||
multiple lifecycle owners is a better extraction candidate.
|
||||
|
||||
### Current runtime boundary audit
|
||||
|
||||
The current large-runtime inventory was reviewed using that rule.
|
||||
|
||||
- IndexedDB remains large, but connection, transaction, migration, maintenance
|
||||
and failure translation already have separate owners/modules.
|
||||
- resumable upload already separates runtime policy, checkpoint persistence,
|
||||
HTTP control-plane transport, part execution, cancellation and mutation
|
||||
locking.
|
||||
- realtime already separates reconnect policy/coordinator, event codec/consumer
|
||||
and stream coordination.
|
||||
- OPFS is separated into browser runtime, journal, byte-store, policy and worker
|
||||
protocol/runtime responsibilities.
|
||||
- HTTP V3 still owned retry eligibility/backoff inside the execution state
|
||||
machine, so that responsibility moved to
|
||||
`src/adapters/http/http-retry-lifecycle.ts`.
|
||||
|
||||
No other runtime is split merely because of its line count.
|
||||
|
||||
## Consumer quality metrics
|
||||
|
||||
Before adding another abstraction, implement or model multiple real feature
|
||||
uses and measure:
|
||||
|
||||
- feature-owned adapter LOC,
|
||||
- repeated platform glue,
|
||||
- number of platform-internal types exposed to the feature,
|
||||
- central catalog edits,
|
||||
- files changed for one normal query/command,
|
||||
- whether native browser/network APIs leak into the feature.
|
||||
|
||||
The target feature-development path is:
|
||||
|
||||
1. domain type and invariant,
|
||||
2. use case,
|
||||
3. port,
|
||||
4. transport/storage schema plus mapper/codec,
|
||||
5. capability binding,
|
||||
6. presentation controller/page.
|
||||
|
||||
A product feature should not need to know the retry scheduler, abort ownership,
|
||||
effect-certainty machinery, transaction leases, reconnect coordinator, OPFS
|
||||
journal or provider lifecycle.
|
||||
|
||||
The executable REST and IndexedDB consumer baselines are recorded in
|
||||
[`capability-consumer-experience.md`](./capability-consumer-experience.md).
|
||||
Contract placement and the global-vs-owner-local audit are recorded in
|
||||
[`contract-ownership.md`](./contract-ownership.md).
|
||||
|
||||
## Verification paths
|
||||
|
||||
Product-development verification, capability verification and release assurance
|
||||
are intentionally separate. See
|
||||
[`docs/testing/taxonomy.md`](../testing/taxonomy.md).
|
||||
|
||||
Host-level CI-runner tests belong to `tests/system`, not `tests/unit`.
|
||||
Reusable capability consumer contracts belong to `tests/contract`.
|
||||
@@ -17,11 +17,63 @@ The following edges are forbidden:
|
||||
|
||||
- domain to application, presentation, adapters, bootstrap, React, or browser globals
|
||||
- application to presentation, concrete adapters, bootstrap, React, or browser globals
|
||||
- `contracts` to application or features: contracts is the lower package and
|
||||
owns the shared vocabulary both of them read
|
||||
- presentation to concrete adapters, raw DTO schemas, or storage implementations
|
||||
- generic presentation to the installed-feature registries: which features exist
|
||||
is a product decision owned by `bootstrap`
|
||||
- an adapter to presentation, bootstrap internals, or another concrete adapter
|
||||
- feature domain/application to its presentation or outbound adapter, and
|
||||
feature presentation to its outbound adapter
|
||||
|
||||
## The adapter kernel
|
||||
|
||||
"Another concrete adapter" excludes the adapter kernel, which is shared on
|
||||
purpose and is the only adapter code an adapter may reach across a group for:
|
||||
|
||||
- `src/adapters/platform/**` — the system clock, the shared abort primitive and
|
||||
the bounded-capacity guard
|
||||
- `src/adapters/browser-file-storage/result.ts` — the browser-data result and
|
||||
failure constructors
|
||||
|
||||
Each rule above is enforced by `check:architecture`, including the kernel
|
||||
carve-out, so this table and the executable rules cannot drift apart. Two edges
|
||||
are still open and are named explicitly in `.dependency-cruiser.json` rather
|
||||
than left silent: the generic presentation modules that read the installed
|
||||
registries today, and the two collaborator types `query-cache` reads from
|
||||
`cross-context-invalidation`. Both lists are frozen — a new edge of either kind
|
||||
fails the gate.
|
||||
|
||||
## 어댑터 그룹의 공개 경계
|
||||
|
||||
각 어댑터 그룹의 공개 표면은 그 그룹의 `index.ts`다. 그룹 바깥
|
||||
(`bootstrap`, `features`, `presentation`)은 배럴만 import한다.
|
||||
`adapter-groups-are-reached-through-their-barrel` 규칙이 이를 강제하고,
|
||||
`tests/fixtures/architecture/dependency-graph/barrel`이 거부와 허용을
|
||||
각각 고정한다 — 규칙을 지우면 그 회귀 검사가 먼저 깨진다.
|
||||
|
||||
두 가지 예외가 있고 둘 다 의도된 것이다.
|
||||
|
||||
- **그룹 내부 파일끼리**는 파일 경로로 직접 import한다. 배럴은 바깥을 위한
|
||||
문이지 내부 규율이 아니다.
|
||||
- **어댑터 → 커널(`platform/**`)** 간선도 파일 경로를 유지한다. 커널은
|
||||
런타임 합성물이 아니라 프리미티브이고, `check:adapter-inventory`가 네
|
||||
소비자에게 `platform/abortable-operation.ts`로 해석되는 specifier를
|
||||
직접 요구한다. 커널 배럴(`platform/index.ts`)은 bootstrap과 테스트를
|
||||
위한 것이다.
|
||||
|
||||
`storage`는 최상위 배럴이 `indexeddb/`·`opfs/` 서브배럴을 재수출하지
|
||||
않는다. 두 런타임이 각자 독립적으로 제거 가능하고
|
||||
(`test:browser-file-storage-removal`), 그래서 각 서브배럴이 곧 경계다.
|
||||
규칙의 도착점 정규식이 1단계 중첩 `index.ts`를 배럴로 인정하는 이유가
|
||||
이것이다.
|
||||
|
||||
`service-worker/index.ts`는 `tsconfig.service-worker.json`의 `exclude`에
|
||||
들어 있다. 그 설정이 그룹 폴더를 통째로 WebWorker lib로 컴파일하면서 페이지
|
||||
realm 파일만 빼는 구조라, 배럴이 그 파일을 다시 끌어들이면 워커 타입체크가
|
||||
`document`를 찾지 못한다. 배럴의 타입 커버리지는 `tsconfig.app.json`이
|
||||
담당하고, 워커 진입점은 배럴을 쓰지 않는다.
|
||||
|
||||
`bootstrap` contains composition only. Business rules and page-specific
|
||||
orchestration belong to domain/application.
|
||||
|
||||
|
||||
@@ -19,6 +19,37 @@
|
||||
- 운영 절차:
|
||||
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
|
||||
|
||||
|
||||
## Installed binding snapshot과 stream cleanup bound (R-01, R-04, R-05, R-06)
|
||||
|
||||
- `installBrowserRpcContractBindings()`가 registry를 **parse → validate →
|
||||
install** 순서로 처리한다. own data descriptor만 읽어 exact key set으로
|
||||
null-prototype frozen snapshot을 만들고, 그 snapshot을 검증한 뒤 설치한다.
|
||||
getter/accessor, extra key, symbol key, malformed descriptor, revoked proxy는
|
||||
composition-time `TypeError`이며 getter는 호출조차 되지 않는다. runtime과
|
||||
transport call은 이후 snapshot만 읽으므로 validation 이후 registry mutation이
|
||||
replay policy·deadline·byte ceiling·transport selection을 바꿀 수 없다.
|
||||
- server stream 종료는 transport iterator에 lifecycle authority를 위임하지
|
||||
않는다. commit/admission generation은 즉시 fence하고 listener는 바로 해제하며,
|
||||
`iterator.return()`은 cleanup **요청**으로서 bound 안에서만 기다린다. 끝나지
|
||||
않은 cleanup은 관찰만 유지되고(unhandled rejection 없음) application generator는
|
||||
bound 안에 종료된다. cleanup rejection은 이미 선택된 application failure를
|
||||
덮지 않는다.
|
||||
- WebSocket text frame은 allocation 전에 admission한다. UTF-16 code unit 길이가
|
||||
이미 cap을 넘으면 encoder를 만들지 않고 거절하고, 나머지는 early exit하는
|
||||
code-point 누적으로 센다. valid surrogate pair는 4 bytes, lone surrogate는
|
||||
`TextEncoder`와 동일하게 replacement 3 bytes다.
|
||||
- clock/fence collaborator 예외는 Result 경계를 벗어나지 않는다. clock 실패는
|
||||
`SERVER_FAILURE/RPC_RUNTIME_DEPENDENCY_FAILED`, capture 실패는
|
||||
`SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE`, `isCurrent` 실패는
|
||||
fail-closed로 canonicalize하며 listener/timer는 단일 exit path에서 정확히 한 번
|
||||
해제한다.
|
||||
|
||||
Browser RPC는 여전히 `AVAILABLE_NOT_COMPOSED`다. 선택된 Connect/gRPC-Web
|
||||
transport는 enqueue-time `maxBufferedBytes`, raw/decompressed ceiling,
|
||||
cancel/closed receipt, terminal framing, target browser와 load behavior를
|
||||
별도로 증명해야 조립할 수 있다 (R-07).
|
||||
|
||||
## 1. 먼저 축을 분리한다
|
||||
|
||||
네 이름은 같은 종류의 대안이 아니다.
|
||||
|
||||
@@ -34,6 +34,16 @@ capability가 설치됐거나 production-ready라는 뜻이 아니다.
|
||||
[Frontend ports, adapters, and boundaries](./frontend-ports-adapters-and-boundaries.md)
|
||||
를 따른다.
|
||||
|
||||
|
||||
## Bounded task lease와 DRAINING (R-02, R-03)
|
||||
|
||||
non-cooperative effect/recovery authority 하나가 stream tail 전체를 영구
|
||||
wedge하지 못하도록, common coordinator는 각 task를 deadline으로 감싼다. deadline
|
||||
초과 시 commit capability는 즉시 취소되지만 task 자체는 `retainedTasks`에 남아
|
||||
stream을 `DRAINING`으로 유지한다. `close()`는 이 retain 집합이 실제로 settle해야
|
||||
성공을 반환한다. handoff coordinator도 같은 원칙으로 fail-close된 writer를
|
||||
`retiredWriters`에 보존한다.
|
||||
|
||||
## 0. 현재 상태와 목표 delta
|
||||
|
||||
이 문서에서 설계 승인, reference source 존재, production 조합과 target browser의
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"standard": "rules/diagram-standards.md v2",
|
||||
"evidenceReport": {
|
||||
"repoPath": "docs/architecture/review-evidence.md",
|
||||
"canonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
|
||||
"upstreamCanonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
|
||||
"canonicalSha256": "b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29"
|
||||
},
|
||||
"reviews": {
|
||||
@@ -25,5 +25,6 @@
|
||||
"thresholdSatisfied": true,
|
||||
"scope": "immutable static assets and mutable /config.json delivery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"note": "`repoPath` is this repository's copy and must resolve. `upstreamCanonicalPath` and every `reviews[*].sourcePath` name the reviewing workspace, not this tree; they are provenance labels and are deliberately not resolvable here. `canonicalSha256` is what binds the two, and the gate checks it appears in `repoPath`."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"review": "third-review-2026-08-14",
|
||||
"note": "GOV-03. The machine-readable disposition of every finding the third re-review raised. `check:remediation-ledger` joins this file against the prose ledger and refuses a blanket closure claim while any row is not FIXED, so a summary sentence can never outrun the evidence.",
|
||||
"dispositions": [
|
||||
{
|
||||
"id": "NS-01",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A credential owner's answer is decoded once, inside the auth boundary, through own data descriptors.",
|
||||
"evidence": ["tests/integration/http-execution-v3-live-authority.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "NS-02",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "Contract composition snapshots first and validates the snapshot, so the installed row is the row that was checked.",
|
||||
"evidence": ["tests/unit/contract-registry-immutability.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "NS-03",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "The `responseBody: NONE` probe owns its reader: the operation lifetime reaches it, and the lock is released.",
|
||||
"evidence": ["tests/integration/http-execution-v3-live-authority.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "NS-04",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A journal transaction that cannot be completed is maintenance debt, not a settled write.",
|
||||
"evidence": ["tests/unit/opfs-byte-store.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "NS-05",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A bootstrap failure answers with the request kind it belongs to, so the real cause survives the gateway.",
|
||||
"evidence": ["tests/unit/opfs-worker-runtime.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "NS-06",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A reply is decoded before its pending row is released, and an uncorrelatable reply fails the channel closed.",
|
||||
"evidence": ["tests/unit/opfs-worker-runtime.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "NS-07",
|
||||
"previous": "NEW",
|
||||
"disposition": "FIXED",
|
||||
"summary": "Cursor caps and collaborators are captured at construction, so a later mutation cannot widen a validated cap.",
|
||||
"evidence": ["tests/unit/cursor-pagination-runtime.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "NS-08",
|
||||
"previous": "NEW",
|
||||
"disposition": "FIXED",
|
||||
"summary": "One terminal owner covers the whole public-cache staging body, so nothing writes after the abort.",
|
||||
"evidence": ["tests/unit/public-response-cache.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "RPC-01",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "Only a fulfilled, contract-shaped `waitClosed()` receipt prunes an active stream registration.",
|
||||
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "RPC-02",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "Iterator cleanup and the lease decoder read foreign state inside their own boundaries.",
|
||||
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "RPC-03",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "Every registry is snapshotted before any validation runs, and rows with hidden fields are refused.",
|
||||
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "RPC-04",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A transport result is an exact union: required own keys, no inherited extras, plain prototype.",
|
||||
"evidence": ["tests/unit/browser-rpc/browser-rpc-remediation.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "RT-01",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A tracked task is registered before the authority is invoked, closing the reentrant-close window.",
|
||||
"evidence": ["tests/unit/realtime/stream-coordinator.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "RT-02",
|
||||
"previous": "NEW",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A scheduler that cannot install a deadline fails closed inside the realtime result contract.",
|
||||
"evidence": ["tests/unit/realtime/stream-coordinator.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "TR-01",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "The vault snapshots a registration and everything nested in it before validating or storing it.",
|
||||
"evidence": ["tests/unit/presigned-transfer.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "TR-02",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "A source lease that arrives after the delivery ended is closed exactly once by a compensator.",
|
||||
"evidence": ["tests/unit/presigned-transfer.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "TR-03",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "The shared abort primitive settles once by observation order, and all four consumers use it with bound timer snapshots.",
|
||||
"markers": ["X-AUDIT-01", "X-AUDIT-02"],
|
||||
"evidence": [
|
||||
"tests/unit/abortable-operation.test.ts",
|
||||
"tests/unit/image-cdn-runtime.test.ts",
|
||||
"tests/unit/resumable-upload-fetch-transport.test.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "TR-04",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "Teardown proves quiescence of the raw provider registry, not only of the wrappers that bound it.",
|
||||
"evidence": ["tests/unit/resumable-upload-runtime.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "TR-05",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "The control-plane decoder validates an owned snapshot, so a stateful answer cannot swap a checked value.",
|
||||
"evidence": ["tests/unit/resumable-upload-http-control-plane.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "SW-01",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "The activation marker read is bounded in bytes, cancels what it refuses and releases its reader lock.",
|
||||
"evidence": ["tests/unit/service-worker-runtime.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "SW-02",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "The generator and the runtime decoder share one canonical asset-path predicate, and the generator self-validates.",
|
||||
"evidence": ["tests/unit/service-worker-web-push-remediation.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "WP-01",
|
||||
"previous": "PARTIAL",
|
||||
"disposition": "FIXED",
|
||||
"summary": "One observation authority per click; certainty is monotone and the late-effect tail is owned by `waitUntil`.",
|
||||
"evidence": ["tests/unit/web-push-worker-runtime.test.ts"]
|
||||
},
|
||||
{
|
||||
"id": "GOV-03",
|
||||
"previous": "OPEN",
|
||||
"disposition": "FIXED",
|
||||
"summary": "This file plus `check:remediation-ledger` bind each row to a disposition and a test path, and block a blanket closure claim while any row is open.",
|
||||
"evidence": ["scripts/check-remediation-ledger.ts"]
|
||||
},
|
||||
{
|
||||
"id": "GOV-04",
|
||||
"previous": "OPEN",
|
||||
"disposition": "FIXED",
|
||||
"summary": "The inventory gate requires the exact named consumer set to resolve its import to the shared primitive and prints the set.",
|
||||
"evidence": ["scripts/check-adapter-inventory.ts"]
|
||||
},
|
||||
{
|
||||
"id": "GOV-05",
|
||||
"previous": "OPEN",
|
||||
"disposition": "FIXED",
|
||||
"summary": "Duplicate abort mechanics were consolidated onto the shared primitive and the file-transfer budget was reset to cover the remaining correctness code.",
|
||||
"markers": [],
|
||||
"evidence": ["config/recipes/frontend-capability-recipes.json"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
# 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 | Historical baseline result. All three passed in a fresh process (78 passed together). |
|
||||
|
||||
> Current classification update (2026-09-18): the provider guardian transaction
|
||||
> suite moved to `tests/integration/provider-guardian-transaction.test.ts`.
|
||||
> It spawns child processes and exercises filesystem/IPC/process lifecycle, so it
|
||||
> is not part of the pure unit pool. Its READY/PUBLISHED wait uses a dedicated
|
||||
> test watchdog rather than treating a 1-second scheduler delay as a production
|
||||
> protocol deadline.
|
||||
|
||||
### 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` (historical path; now `tests/integration/provider-guardian-transaction.test.ts`) | 1 | Historical baseline: passed in isolation; current suite is classified as process integration. |
|
||||
|
||||
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
|
||||
primitive를 coordinator 완료 증거로 사용하지 않는다.
|
||||
|
||||
|
||||
## OPFS 보상 실패와 reconcile (STO-01)
|
||||
|
||||
`put()`이 실패했는데 보상 cleanup effect가 확인되지 않으면 runtime은 실패를
|
||||
`OBJECT_RECONCILE` / `CONFLICT`(recovery `RETRY`)로 보고하고 journal row를 남긴다.
|
||||
이는 결함이 아니라 설계된 상태다.
|
||||
|
||||
1. journal에 `PREPARING` 또는 `FILES_READY` row가 남아 있는지 확인한다. 남아
|
||||
있다면 staging bytes가 아직 존재할 수 있다는 뜻이다.
|
||||
2. `maintenance.reconcile()`을 실행한다. reconcile은 같은 exact physical
|
||||
generation token만 삭제하고, effect가 여전히 `EFFECT_UNKNOWN`이면 journal을
|
||||
유지한 채 다시 `OBJECT_RECONCILE`을 반환한다.
|
||||
3. journal row를 수동으로 삭제하지 않는다. row가 사라지면 stale staging을 추적할
|
||||
근거가 사라지고 quota만 누수된다.
|
||||
4. OPFS root나 journal database를 통째로 삭제하거나 schema를 downgrade하지
|
||||
않는다. rollback은 새 v2 write admission을 닫고 v1+v2 reader를 유지하는 것으로
|
||||
수행한다.
|
||||
|
||||
## 1. 공통 원칙
|
||||
|
||||
incident 중에도 다음 작업은 금지한다.
|
||||
@@ -130,6 +148,9 @@ read-only나 online-only가 사용자 작성 내용을 잃게 한다면 먼저 e
|
||||
### 신호
|
||||
|
||||
- `BLOCKED`, `UPGRADE_BLOCKED` 또는 blocked duration bucket 증가
|
||||
- live attempt가 기다리는 동안 status는 `BLOCKED`
|
||||
- blocked deadline settle 뒤에는 open result가 `BLOCKED` failure여도
|
||||
runtime status는 `CLOSED / NOT_OPENED`
|
||||
- `versionchange` 뒤 connection이 남음
|
||||
- repeated reload/update loop
|
||||
- open/maintenance의 `POLICY_REJECTED`: immutable dataset binding missing/mismatch
|
||||
@@ -482,16 +503,13 @@ browser engine/version, fixture ID, fault phase, bounded counts/buckets와 PASS/
|
||||
|
||||
promotion 직전에는 다음 repository evidence도 함께 보존한다.
|
||||
|
||||
- `test:browser-capabilities`가 만든 JUnit에서 Chromium, Firefox, WebKit이 동일
|
||||
14개 testcase set(File 2, IndexedDB 4, OPFS/Cache/StorageManager 각 1,
|
||||
cross-context invalidation 2, presigned streaming download/multipart
|
||||
upload/Image CDN 각 1)을 실제 실행해 총 42개이며 failure/error/skipped가 모두
|
||||
0이어야 한다.
|
||||
`verify:browser-capability-evidence`가 engine 집합과 testcase 동일성을
|
||||
기계적으로 검증한다. 현재 artifact는 Chromium/Firefox 14개씩 총 28개가
|
||||
통과했지만 WebKit 실행에 필요한 native libraries(예:
|
||||
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
|
||||
`libavif.so.16`과 WPE 계열)가 이 host에 없으므로 아직 promotion 가능 상태가
|
||||
- `test:browser-capabilities`가 만든 JUnit에서 Chromium, Firefox, WebKit이
|
||||
source suite의 동일 testcase set을 실제 실행하고 failure/error/skipped가 모두
|
||||
0이어야 한다. 구체적인 case 수는 이 runbook에 복제하지 않는다.
|
||||
`verify:browser-capability-evidence`가 Chromium을 baseline으로 testcase set과
|
||||
engine 집합을 동적으로 검증한다. native capability가 없는 engine도 testcase를
|
||||
skip하지 않고 adapter의 명시적 `UNSUPPORTED`/degraded 결과를 검증해야 한다.
|
||||
세 engine 중 하나라도 실행되지 않은 artifact는 promotion 가능 상태가
|
||||
아니다.
|
||||
- `artifacts/quality/vite-module-inventory.json`에서 optional runtime source root가
|
||||
production chunk에 없음을 `check:optional-recipes`로 검증한다.
|
||||
|
||||
@@ -1,9 +1,48 @@
|
||||
# CI quality-gate orchestration
|
||||
|
||||
`config/ci/gates.json` is the executable registry for all 26 gates. The Gitea
|
||||
`config/ci/gates.json` is the executable registry for all 26 gates and the sole
|
||||
workflow model. `.gitea/workflows/quality-gates.yml` is generated from that
|
||||
strict v2 contract; it is not an independent configuration authority. Update
|
||||
the registry, run `corepack pnpm generate:ci-workflow`, and commit the generated
|
||||
bytes together. `corepack pnpm check:ci-workflow` rejects missing, stale, CRLF,
|
||||
or otherwise byte-different output. `corepack pnpm check:ci` runs artifact-schema
|
||||
checking, the typed contract checker, and this workflow drift check. The Gitea
|
||||
adapter runs each gate as an independent matrix check with full fan-out and no
|
||||
soft-fail wiring.
|
||||
|
||||
Contract loading validates every registered command entry against the
|
||||
authoritative root package-script graph and lifecycle/install policy before the
|
||||
runner enters its execution callback. Foreign cwd/workspace script dispatch,
|
||||
argument-sensitive dispatchers, and ineffective lifecycle suppression therefore
|
||||
fail preflight even when the later `check:ci` command would never run.
|
||||
For npm, that root-only boundary also parses options following an explicit
|
||||
`run`/`run-script` target or implicit `test`/`start`/`stop`/`restart` target:
|
||||
`--workspace`, `-w`, `--workspaces`, and `--prefix` are foreign manifest scope
|
||||
and are rejected before execution. The first literal `--` ends npm option
|
||||
parsing, so later tokens are ordinary script arguments. pnpm and Yarn differ:
|
||||
their options following the selected script name are forwarded to that script;
|
||||
their cwd/workspace selectors are rejected only where the manager consumes
|
||||
them before script selection.
|
||||
|
||||
The npm graph includes existing root-manifest `pre<script>` and `post<script>`
|
||||
hooks around every explicit `run`/`run-script` dependency and implicit
|
||||
`start`/`stop`/`restart`/`test` dependency. Hooks are omitted only when ordered
|
||||
npm options establish an unambiguous effective `--ignore-scripts` value before
|
||||
the first literal `--`; bare and explicitly true forms omit hooks, while false,
|
||||
negative, contradictory, malformed, and post-delimiter forms keep traversal or
|
||||
fail closed. Case-insensitive `npm_config_workspace`, `npm_config_workspaces`,
|
||||
`npm_config_prefix`, `npm_config_userconfig`, and `npm_config_globalconfig`
|
||||
assignments are rejected whenever the tokenized command invokes npm. Direct
|
||||
assignments, static paths whose basename is `env`, exact `command`/`exec`
|
||||
prefix chains, prior exports, and `set -a` assignments share one prefix grammar.
|
||||
Only modeled non-scope `env -i`, `env -u`, `env --unset`, and `env --` forms are
|
||||
allowed; cwd-changing or unknown options fail closed. Dynamic assignment names,
|
||||
unmodeled environment mutation, npm `--userconfig`/`--globalconfig`, and
|
||||
unquoted pathname expansion before the npm argument delimiter are rejected.
|
||||
The gate runner checks the same inherited environment names before loading the
|
||||
contract or entering the execution callback, so they cannot reach a child gate
|
||||
process.
|
||||
|
||||
The dependency graph is:
|
||||
|
||||
```text
|
||||
@@ -32,10 +71,137 @@ full commit ID와 commit timestamp를 한 번 읽어 `SOURCE_DATE_EPOCH`를 유
|
||||
release manifest도 같은 build ID, commit SHA, release ID와 timestamp를 가져야
|
||||
한다.
|
||||
|
||||
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. 이
|
||||
workflow를 required check로 전환하기 전에 staging instance에서
|
||||
`permissions`, `gitea.run_attempt`, `actions/upload-artifact@v4`를 포함한 한
|
||||
번의 전체 provider smoke를 통과시켜야 한다. 모든 setup step은
|
||||
Release tier의 `immutable_build`는 `dist`의 hidden `.vite` entries, raw
|
||||
`pnpm-lock.yaml`, build/module inventory와 local release/security evidence를 한
|
||||
번 생성해 run ID/attempt로 구분된 archive로 게시한다. Raw lock bytes의 SHA-256은
|
||||
dependency inventory와 candidate manifest 양쪽의 `lockfileSha256`과 같아야 한다.
|
||||
Producer는 게시 전에 manifest가 선언한 exact member set과 각 byte digest를
|
||||
검증하고 archive SHA-256을 job output으로 고정한다. Vulnerability/provenance
|
||||
provider job은 같은 archive를 각각 내려받아 output SHA-256과 exact member set을
|
||||
확인한 뒤 checkout과 분리된 root에만 추출하고, `CANDIDATE_LOCKFILE_PATH`와
|
||||
`CANDIDATE_DIST_SHA256`를 외부 command에 전달한다. Provider command 실행 후에는
|
||||
archive와 추출된 candidate를 다시 검증하고, strict provider JSON과 candidate
|
||||
digest가 일치할 때만 report를 업로드한다. Promotion은 같은 archive와 두 report를
|
||||
별도 경로로 내려받고 SHA/member 검증을 마친 뒤 격리된 root에 추출하여 local
|
||||
evidence를 read-only로 다시 계산하고 Ed25519 signature/digest를 확인한다.
|
||||
Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체를 변경 없이
|
||||
그대로 승격한다. Local evidence 관계 검증에 필요한 policy와 verifier source도
|
||||
archive member로 고정되며, checkout 밖의 격리된 cwd에서도 archive path와 기대
|
||||
digest만으로 candidate-internal check를 재계산하고 archived secret-scan
|
||||
policy/rule/SARIF/zero-finding/digest 관계를 검증한다. Archive에 없는 checkout
|
||||
source를 다시 scan했다고 주장하지 않는다.
|
||||
|
||||
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. Linux
|
||||
runner에는 실행 가능한 `/usr/bin/bwrap`, `/usr/bin/prlimit`,
|
||||
`/usr/bin/systemd-run`, `/usr/bin/systemctl`, `bwrap --size` 지원, active user
|
||||
bus, systemd user manager 254 이상, unified cgroup v2와 delegated
|
||||
memory/pids/CPU controller가 모두 필요하다. 실행 파일, user manager/version,
|
||||
trust/archive/path 같은 host-side preflight 실패는 raw report 생성 전에 차단된다.
|
||||
`bwrap --size` 수용 여부와 실제 delegated controller/limit 값은 owned raw inode를
|
||||
만든 뒤 scope 안에서만 확정할 수 있으며, 이 단계의 실패는 해당 inode를
|
||||
identity-bound cleanup하고 fail closed한다.
|
||||
|
||||
각 provider는 고유한 collected user scope에서 실행된다. Supervisor는 실행 전에
|
||||
실제 cgroup membership과 `memory.max=1073741824`, `memory.swap.max=0`,
|
||||
`pids.max=64`, `cpu.max="100000 100000"`을 확인한다. 내부 process에는 core 0,
|
||||
file-size 8,388,607 bytes, open FD 64, CPU 1,200초 상한도 적용된다.
|
||||
같은 UID 전체에 합산되는 `RLIMIT_NPROC`로 provider별 32개를 보장한다고 주장하지
|
||||
않으며 aggregate PID authority는 cgroup `TasksMax=64`다.
|
||||
Bubblewrap는 network namespace를 분리해 완전 offline으로 실행하고 workspace,
|
||||
verified candidate, `.git` mask, `/tmp`, `/etc`, `/proc`, `/dev`를 read-only로
|
||||
유지한다. Archive 검증·추출과 sandbox/trust preflight가 끝난 뒤 supervisor가
|
||||
생성하고 inode를 고정한 정확한 raw report 파일 하나만 read-write bind된다.
|
||||
주변 `untrusted` directory 전체는 writable이 아니다. 실행 전 또는 provider
|
||||
실패 시, 그리고 성공적으로 sealed evidence를 게시한 뒤에도 supervisor가 소유한
|
||||
inode만 atomic quarantine을 거쳐 제거하므로 빈 stale report 없이 재시도할 수 있다.
|
||||
|
||||
Provider command와 provider-prefixed environment는 bounded length-prefixed bwrap vector로
|
||||
`systemd-run` stdin에 전달되어 supervisor/systemd/bwrap wrapper argv나 unit
|
||||
metadata에 노출되지 않는다. 단, 최종 provider executable의 일반 argv는 같은
|
||||
UID의 process inspection에 보일 수 있으므로 command 문자열과 인자에 token,
|
||||
password, private-key material을 넣으면 안 된다. Credential은 반드시 해당 종류의
|
||||
`VULNERABILITY_PROVIDER_*` 또는 `PROVENANCE_PROVIDER_*` environment로만 전달하고
|
||||
`*_COMMAND`에는 넣지 않는다. 정상 provider 종료까지 같은 stdin을 parent-liveness
|
||||
pipe로 열어 두며 supervisor hard death의 EOF를 받은 in-scope wrapper는 provider
|
||||
process group 전체를 종료하고 dev/inode가 일치하는 raw report만 정리한다.
|
||||
|
||||
별도의 trusted guardian child는 provider scope 밖에서 filesystem transaction 전체를
|
||||
소유한다. Client는 spawn 전에 canonical raw/evidence directory를
|
||||
`O_DIRECTORY|O_NOFOLLOW`로 열고 identity를 확인한 뒤 provider kind와 nonce에서
|
||||
canonical raw/final 및 nonce-private raw-staging/sealed-temp exact leaf를 확정한다.
|
||||
Client가 두 private file을 `O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW` mode `0600`으로
|
||||
미리 할당하고 dev/inode를 기록한다. Directory descriptor는 guardian fd 3/4,
|
||||
private file descriptor는 fd 5/6으로 상속되며 guardian argv에는 Node와 helper
|
||||
경로만 있다. 최초 canonical v2 frame에는 provider kind, absolute deadline,
|
||||
32-byte nonce만 전달한다.
|
||||
|
||||
Guardian bootstrap은 procfs link를 읽기 전에 fd 5/6을 fstat한다. Procfs pathname은
|
||||
canonical directory의 exact direct-child grammar를 만족하고 descriptor-relative
|
||||
lstat이 이미 확보한 fd identity/type/mode/size/link count와 일치할 때만 해당
|
||||
identity의 cleanup alias가 된다. Guard frame 검증 뒤 raw staging을 fixed raw leaf에
|
||||
no-overwrite hard link하고 두 alias의 link count 2를 확인한 다음 private alias를
|
||||
제거하고 raw directory를 sync한다. Canonical raw가 같은 identity와 link count 1로
|
||||
남은 뒤에만 READY를 응답한다. READY 전 종료 시 client는 pre-spawn raw identity로
|
||||
raw staging/canonical을, sealed identity로 temp/final을 각각 확인해 일치하는 alias만
|
||||
정리한다. 현재 canonical pathname을 새 ownership identity로 승격하지 않으므로 외부
|
||||
file과 concurrent same-kind winner를 보존하면서 같은 workspace를 즉시 재시도할 수
|
||||
있다. Supervisor는 READY identity와 canonical target도 정확히 확인한다.
|
||||
|
||||
검증된 JSON bytes는 pinned temp inode에만 기록하고 `0400` 적용과 file sync를 마친다.
|
||||
Guardian은 authenticated publish의 size/hash/identity를 재검증하고 같은 directory에서
|
||||
no-replace `link(temp, final)`, temp unlink, directory sync를 수행한 뒤 PUBLISHED를
|
||||
응답한다. `GITHUB_OUTPUT` append 이후 commit은 raw inode를 제거하고
|
||||
`commitPending`으로만 전이하며, 깨끗한 control EOF까지 확인해야 PASS가 된다. 그 전의
|
||||
EOF, deadline, 잘못된 frame/nonce, commit 뒤 추가 byte 또는 guardian 조기 종료는
|
||||
identity가 일치하는 raw/temp/final을 모두 정리하고 fail closed한다. Provider wall
|
||||
timeout 30분에 post-processing 10분을 더한 guardian lease 상한은 40분이다.
|
||||
|
||||
Client가 private allocation 뒤 guardian spawn 전에 hard stop되면 empty mode-`0600`
|
||||
nonce-private leaf만 남을 수 있다. 기록되지 않은 pathname은 ownership 근거가 아니므로
|
||||
자동 sweeping하지 않는다. 이 private leaf는 fixed raw/final name을 점유하지 않아
|
||||
same-kind retry를 막지 않는다.
|
||||
|
||||
`GITHUB_OUTPUT`은 runner가 소유한 regular file이라는 실행기 계약을 전제로 한다.
|
||||
Sealing/output I/O의 OS-level cancellation을 보장하지 않는다. Provider scope가 이미
|
||||
수집된 뒤 guardian이 종료되면 scope-active latch는 늦은 kill을 시작하지 않고 lifecycle
|
||||
error만 기록한다. 이후 publish/commit의 awaited failure가 identity가 고정된
|
||||
raw/temp/final fallback을 모두 정리하고 fail closed한다.
|
||||
|
||||
Provider stdout/stderr는 credential을 포함할 수 있는 untrusted bytes이므로 CI log로
|
||||
재전송하거나 보관하지 않고, byte 수만 합산해 1 MiB 상한을 적용한다. Guardian의
|
||||
stderr/control fd가 닫혀 진단 출력이 `EPIPE`/`EBADF`가 되어도 cleanup 뒤 nonzero
|
||||
종료는 생략되지 않는다. Provider wall-clock 상한은 30분이다. Wall
|
||||
timeout, aggregate output 초과, parent-liveness loss, 실행 중 guardian loss는 provider process group을
|
||||
명시적으로 SIGKILL한다. 일반 command 실패와 FD/CPU RLIMIT 종료는 실제 exit/signal로
|
||||
systemd completion을 거치며, 모든 경로에서 wrapper 종료와 systemd unit/cgroup
|
||||
collection을 확인한다. Adapter는 사전에 배치된 offline data와 supervisor candidate binding만
|
||||
읽어 정확한 report inode에 기록해야 한다. Scope/cgroup limit drift, residual unit,
|
||||
workspace·host home/toolcache·sealed evidence 접근 의존성은 모두 blocking failure다.
|
||||
|
||||
Workflow가 실행하는 action은 `scripts/contracts/ci-gates.ts`의 단일 typed,
|
||||
runtime-frozen registry에서만 resolve된다. `uses:`에는 repository 별칭, tag,
|
||||
branch 또는 short SHA를 허용하지 않고 다음 absolute upstream URL과 full 40-hex
|
||||
commit만 사용한다.
|
||||
|
||||
- `https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5`
|
||||
(`v4.3.1`)
|
||||
- `https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020`
|
||||
(`v4.4.0`)
|
||||
- `https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7`
|
||||
(patched `v4` branch)
|
||||
- `https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7`
|
||||
(patched `v4` branch)
|
||||
|
||||
Gitea 1.22 공식 Actions 문서는 artifact v4 호환 경로로 이
|
||||
`ChristopherHX` patched upload/download fork를 권고한다. 이 권고는 실제
|
||||
설치 환경의 호환성을 증명하지 않으므로, 지원 baseline은 계속 Gitea 1.26.4+
|
||||
및 Gitea Runner 1.0.0+이며 action 변경은 registry, semantic tests, snapshot과
|
||||
generated workflow를 함께 갱신해야 한다.
|
||||
|
||||
이 workflow를 required check로 전환하기 전에 staging instance에서
|
||||
`permissions`, `gitea.run_attempt`, 위 digest-pinned patched artifact action을
|
||||
포함한 한 번의 실제 end-to-end provider smoke를 반드시 통과시켜야 한다.
|
||||
mock/local generation 결과는 required-check enablement evidence가 아니다. 모든 setup step은
|
||||
`node-version-file: .nvmrc`를 사용하므로 CI Node.js 버전은 `.nvmrc`의 exact
|
||||
pin과 같다. CI contract는 `.nvmrc`가 full semantic version인지, 모든 job이 이
|
||||
파일을 사용하는지 함께 검사한다. `ubuntu-latest` runner label은
|
||||
@@ -132,6 +298,83 @@ Repository variables required by higher tiers:
|
||||
- `HOSTING_BASE_URL` for live header verification
|
||||
- `FIELD_WEB_VITALS_INPUT` for the privacy-approved field sample document
|
||||
- `MIN_ELIGIBLE_SAMPLES` after the baseline decision
|
||||
- `VULNERABILITY_PROVIDER_COMMAND` and `PROVENANCE_PROVIDER_COMMAND` for the
|
||||
independently operated scanner and attestor commands
|
||||
- `VULNERABILITY_PUBLIC_KEY_PATH`, `VULNERABILITY_KEY_ID`,
|
||||
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID` for separately managed
|
||||
trusted Ed25519 verification material
|
||||
|
||||
두 provider role은 서로 다른 key ID뿐 아니라 canonical DER-SPKI public-key bytes도
|
||||
사용해야 한다. 동일 key를 서로 다른 ID로 재등록한 구성도 finalizer가 거절한다.
|
||||
|
||||
If any external provider command, report, trust path, or key ID is absent,
|
||||
promotion remains unavailable with `FAIL_UNVERIFIED`; there is no local
|
||||
generator/restore fallback.
|
||||
|
||||
Each provider command must atomically emit strict provider evidence v2 from
|
||||
the supervisor bindings for evidence type, issued/expires timestamps, run
|
||||
ID/attempt, `PROVIDER_INVOCATION_NONCE`, source identity, candidate digests,
|
||||
key ID, and DER-SPKI key fingerprint. Promotion receives the two supervisor
|
||||
job outputs as `VULNERABILITY_INVOCATION_NONCE` and
|
||||
`PROVENANCE_INVOCATION_NONCE`; do not replace them with constants or values
|
||||
parsed from provider reports.
|
||||
|
||||
Promotion verification/staging step과 promoted-release upload action 사이에는
|
||||
어떤 step도 둘 수 없고 upload에는 `if: always()`를 사용할 수 없다. 이 인접성은
|
||||
실패한 검증의 publication을 막지만 staging path를 upload action에
|
||||
cryptographically hand off하지는 않는다. 따라서 promotion runner는 trusted,
|
||||
exclusive, single-tenant여야 하고 staging step 종료부터 바로 다음 upload action이
|
||||
파일을 여는 시점까지 같은 UID로 실행되는 provider command나 다른 process가
|
||||
살아남지 않는다는 운영 가정을 만족해야 한다. 업로드된 artifact의 downstream
|
||||
consumer도 artifact service나 transfer action을 신뢰 경계 밖으로 보고, candidate
|
||||
manifest와 signed provider evidence에 바인딩된 digest를 다운로드 후 다시
|
||||
검증해야 한다. 현재 producer-side adjacency 자체는 consumer-side digest
|
||||
revalidation을 대신하지 않는다.
|
||||
Promotion job에는 job-level `if`가 없다. 기본 `needs` 성공 의미론으로 immutable
|
||||
build, vulnerability provider, provenance provider 세 job이 모두 성공해야 하며,
|
||||
`always()`나 `cancelled()`로 cancellation을 덮어쓰지 않는다. Bare `always()`는
|
||||
step cleanup에만 사용된다. 다만 cancellation 시 cleanup 실행 여부는 workflow
|
||||
정적 타입이나 단위 테스트로 증명하지 않았으며 runner/native smoke에서 확인해야
|
||||
하는 신뢰 경계다.
|
||||
Finalizer output은 `RUNNER_TEMP` 아래 random private directory이며 exact-five
|
||||
upload는 `${{ steps.finalize.outputs.staging_root }}` 아래 다음 다섯 canonical
|
||||
pathname만 사용한다: `release-candidate.tar.gz`, `vulnerability-report.json`,
|
||||
`provenance-attestation.json`, `provider-verification.json`,
|
||||
`promotion-verification.json`. 바로 다음
|
||||
`always()` cleanup은 staging path/token, runner-temp device/inode와 staging-leaf
|
||||
device/inode 여섯 output을 모두 요구한다. cleanup은 pin한 leaf descriptor에서
|
||||
exact-five name만 unlink하고 non-recursive `rmdir`만 사용하므로 교체된 directory나
|
||||
canary tree를 recursive 삭제하지 않는다. stable `.release/promoted-staging`
|
||||
directory를 만들거나 재사용하지 않는다. exact five는 captured archive 한 개,
|
||||
captured report 두 개와 process 안에서 생성한 provider/promotion verification v3
|
||||
두 개이며 promotion
|
||||
record는 provider record, local assessment, report hashes와 run/source/candidate/
|
||||
nonces/key identities/trust-policy hash 및 signed `secretScanAttestation`을 함께
|
||||
bind한다. 이 attestation은 PASS와 local-assessment/source-set/policy/SARIF/
|
||||
scan-input digest를 포함한다. Supervisor/finalizer는 captured archive에서 기대
|
||||
tuple을 유도해 exact equality를 확인하지만, 실제로 같은 source-set 전체를
|
||||
독립 스캔하고 forged empty SARIF에 서명하지 않을 책임은 trusted vulnerability
|
||||
provider에 있다. Staging은 restrictive
|
||||
umask와 무관하게 directory `0700`, file `0400`을 강제하고, 모든 write 뒤 live
|
||||
time으로 exact-five signature/freshness를 다시 확인한 뒤에만 output을 공개한다.
|
||||
Descriptor-relative 정리는 ancestor/leaf 교체와 symlink를 fail-closed로 처리하지만
|
||||
upload action의 same-UID pathname reopen 또는 atomic
|
||||
`renameat2` handoff를 보장하지 않는다. staging Gitea smoke/native adapter 확인
|
||||
전에는 그 경계를 닫았다고 보고하지 않는다. 실제 smoke는 exact-five
|
||||
upload-download와 success, validation failure, upload failure, cancellation 각각의
|
||||
cleanup을 관찰해야 한다. 현재 repository에는 native uploader나 `renameat2`
|
||||
보장이 없다.
|
||||
또한 portable Node의 `mkdir`와 최초 pathname `lstat`는 atomic하지 않다. 구현은
|
||||
mkdir 직후 metadata를 저장하고 이후 `O_DIRECTORY|O_NOFOLLOW` descriptor의
|
||||
device/inode와 비교한 뒤에만 permission을 바꾸지만, 최초 lstat보다 앞서 성공한
|
||||
malicious same-UID 교체는 native/privilege 경계로 남는다. 따라서 `RUNNER_TEMP`
|
||||
private `0700` ancestor와 exclusive single-tenant runner가 필수다.
|
||||
실패 cleanup도 created device/inode와 opened descriptor가 일치한 뒤에만 활성화된다.
|
||||
불일치 descriptor는 close만 수행하며 현재 visible replacement pathname은 unlink나
|
||||
`rmdir`하지 않는다. 공격자가 original directory를 다른 이름이나 parent 밖으로
|
||||
이동한 경우 portable Node parent scan으로 안전하게 회수할 수 없으므로, 공격자를
|
||||
배제한 trusted runner/native cleanup 또는 격리된 test fixture가 잔여 directory를
|
||||
후처리해야 한다.
|
||||
|
||||
Branch protection must mark each `FE-GATE-* / <name>` check required for its
|
||||
declared tier. This repository cannot configure server-side protection by
|
||||
|
||||
@@ -12,6 +12,26 @@ session/account Query lifecycle, strict query policy와 Web Storage v2 lifecycle
|
||||
현재 `DESIGNED_NOT_IMPLEMENTED`다. 아래 목표 절차를 현재 runtime의 보장으로
|
||||
해석하지 않는다.
|
||||
|
||||
|
||||
## Public cache staging repair와 offline activation (STO-03 ~ STO-05)
|
||||
|
||||
- release marker는 "staging이 끝났다"는 **주장**이고 모든 entry의 존재·digest
|
||||
증거가 아니다. 같은 manifest로 `stageRelease`를 다시 호출하면 runtime이
|
||||
candidate를 재검증하고, browser eviction이나 부분 손상이 발견되면 그 owned
|
||||
candidate만 삭제한 뒤 network에서 다시 stage한다. marker만 보고 성공을
|
||||
반환하지 않는다.
|
||||
- 검증 중 abort나 읽기 불가(UNKNOWN)는 stage 성공이 아니며 active pointer를
|
||||
건드리지 않는다. candidate를 임의로 삭제하지도 않는다.
|
||||
- `activateRelease`와 `cleanupOwned`는 network I/O가 없다. fetcher 없이도
|
||||
동작하므로 offline rollback과 quota recovery cleanup이 `UNSUPPORTED`로 막히지
|
||||
않는다. 두 operation은 Cache Storage와 mutation lock만 요구하고 실패 시
|
||||
recovery는 `RETRY`다. `stageRelease`만 fetcher를 요구하며 recovery는
|
||||
`ONLINE_ONLY`다.
|
||||
- variant를 사용하는 policy(`allowedVaryHeaderNames` 비어 있지 않음)는 반드시
|
||||
`allowedResponseHeaderNames`에 `vary`를 포함해야 한다. 아니면 composition이
|
||||
`TypeError`로 즉시 실패한다. 저장된 variant가 같은 key로 충돌하는 상태를 만들지
|
||||
않기 위한 cross-field invariant다.
|
||||
|
||||
## 1. 변경할 수 없는 복구 원칙
|
||||
|
||||
- 서버가 server state와 authorization의 source of truth다.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,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,143 @@
|
||||
# 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/auth/index.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 3 | `src/adapters/browser-file-storage/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 4 | `src/adapters/browser-file-storage/result.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 5 | `src/adapters/browser-file-storage/storage-manager-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 6 | `src/adapters/browser-files/browser-file-picker.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 7 | `src/adapters/browser-files/browser-file-policy-registry.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 8 | `src/adapters/browser-files/browser-file-vault.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 9 | `src/adapters/browser-files/create-browser-file-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 10 | `src/adapters/browser-files/download-delivery-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 11 | `src/adapters/browser-files/file-observer.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 12 | `src/adapters/browser-files/file-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 13 | `src/adapters/browser-files/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 14 | `src/adapters/browser-files/object-url-lease.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 15 | `src/adapters/browser-rpc/browser-rpc-runtime.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 16 | `src/adapters/browser-rpc/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 17 | `src/adapters/browser-rpc/transport.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 18 | `src/adapters/browser-rpc/unavailable-browser-rpc-transport.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 19 | `src/adapters/browser-transfer/image-cdn/README.md` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 20 | `src/adapters/browser-transfer/image-cdn/browser-image-probe.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 21 | `src/adapters/browser-transfer/image-cdn/image-cdn-policy.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 22 | `src/adapters/browser-transfer/image-cdn/image-cdn-runtime.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 23 | `src/adapters/browser-transfer/image-cdn/image-header-metadata.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 24 | `src/adapters/browser-transfer/image-cdn/index.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 25 | `src/adapters/browser-transfer/image-cdn/p256-image-capability-verifier.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 26 | `src/adapters/browser-transfer/index.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 27 | `src/adapters/browser-transfer/presigned/incremental-sha256.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 28 | `src/adapters/browser-transfer/presigned/index.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 29 | `src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 30 | `src/adapters/browser-transfer/presigned/presigned-capability-vault.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 31 | `src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 32 | `src/adapters/browser-transfer/resumable-upload/checkpoint-schema.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 33 | `src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 34 | `src/adapters/browser-transfer/resumable-upload/http-control-plane-adapter.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 35 | `src/adapters/browser-transfer/resumable-upload/index.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 36 | `src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 37 | `src/adapters/browser-transfer/resumable-upload/presigned-upload-part-executor.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 38 | `src/adapters/browser-transfer/resumable-upload/resumable-upload-runtime.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 39 | `src/adapters/browser-transfer/resumable-upload/runtime-policy.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 40 | `src/adapters/browser-transfer/resumable-upload/upload-byte-source.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 41 | `src/adapters/browser-transfer/resumable-upload/upload-cancellation-channel.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 42 | `src/adapters/browser-transfer/resumable-upload/upload-mutation-lock.ts` | [Browser transfer](./04-browser-transfer.md) |
|
||||
| 43 | `src/adapters/cache-storage/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 44 | `src/adapters/cache-storage/public-cache-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 45 | `src/adapters/cache-storage/public-response-cache-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 46 | `src/adapters/cross-context-invalidation/browser-cross-context-host.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 47 | `src/adapters/cross-context-invalidation/browser-cross-context-invalidation.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 48 | `src/adapters/cross-context-invalidation/index.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 49 | `src/adapters/diagnostics/bounded-diagnostics.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 50 | `src/adapters/diagnostics/index.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 51 | `src/adapters/http/bounded-body-reader.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 52 | `src/adapters/http/bounded-json.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 53 | `src/adapters/http/client.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 54 | `src/adapters/http/http-contract-bridge.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 55 | `src/adapters/http/http-effect-certainty.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 56 | `src/adapters/http/http-execution-v3.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 57 | `src/adapters/http/index.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 58 | `src/adapters/http/request-builder.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 59 | `src/adapters/http/resource-mapper.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 60 | `src/adapters/http/retry-policy.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 61 | `src/adapters/http/schema-registry.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 62 | `src/adapters/platform/abortable-operation.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 63 | `src/adapters/platform/browser-lifecycle.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 64 | `src/adapters/platform/browser-mutation-intent-factory.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 65 | `src/adapters/platform/bounded-capacity.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 66 | `src/adapters/platform/index.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 67 | `src/adapters/platform/indexeddb-connection.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 68 | `src/adapters/platform/indexeddb-transaction.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 69 | `src/adapters/platform/system-clock.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 70 | `src/adapters/query-cache/conditional-validator-store.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 71 | `src/adapters/query-cache/cursor-pagination-runtime.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 72 | `src/adapters/query-cache/index.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 73 | `src/adapters/query-cache/server-state-scope-runtime.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 74 | `src/adapters/query-cache/tanstack-cache-coordinator.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 75 | `src/adapters/query-cache/tanstack-query-cache.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 76 | `src/adapters/realtime/event-codec.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 77 | `src/adapters/realtime/event-consumer.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 78 | `src/adapters/realtime/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 79 | `src/adapters/realtime/json-member-scanner.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 80 | `src/adapters/realtime/live-poll-handoff-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 81 | `src/adapters/realtime/polling/bounded-poll-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 82 | `src/adapters/realtime/polling/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 83 | `src/adapters/realtime/reconnect-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 84 | `src/adapters/realtime/reconnect-policy.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 85 | `src/adapters/realtime/result.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 86 | `src/adapters/realtime/sse/fetch-sse-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 87 | `src/adapters/realtime/sse/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 88 | `src/adapters/realtime/sse/sse-parser.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 89 | `src/adapters/realtime/stream-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 90 | `src/adapters/realtime/websocket/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 91 | `src/adapters/realtime/websocket/websocket-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 92 | `src/adapters/realtime/websocket/websocket-protocol.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||
| 93 | `src/adapters/service-worker/index.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 94 | `src/adapters/service-worker/service-worker-entry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 95 | `src/adapters/service-worker/service-worker-lifecycle.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 96 | `src/adapters/service-worker/service-worker-page-controller.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 97 | `src/adapters/service-worker/service-worker-protocol.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 98 | `src/adapters/service-worker/service-worker-removal.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 99 | `src/adapters/service-worker/service-worker-static-assets.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 100 | `src/adapters/storage/browser-storage-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 101 | `src/adapters/storage/browser-storage-codec.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 102 | `src/adapters/storage/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 103 | `src/adapters/storage/indexeddb/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 104 | `src/adapters/storage/indexeddb/indexeddb-failure.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 105 | `src/adapters/storage/indexeddb/indexeddb-governance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 106 | `src/adapters/storage/indexeddb/indexeddb-maintenance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 107 | `src/adapters/storage/indexeddb/indexeddb-migrations.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 108 | `src/adapters/storage/indexeddb/indexeddb-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 109 | `src/adapters/storage/indexeddb/indexeddb-types.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 110 | `src/adapters/storage/opfs/browser-opfs-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 111 | `src/adapters/storage/opfs/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 112 | `src/adapters/storage/opfs/indexeddb-opfs-journal.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 113 | `src/adapters/storage/opfs/opfs-byte-store-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 114 | `src/adapters/storage/opfs/opfs-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 115 | `src/adapters/storage/opfs/opfs-worker-client.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 116 | `src/adapters/storage/opfs/opfs-worker-protocol.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 117 | `src/adapters/storage/opfs/opfs-worker-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||
| 118 | `src/adapters/telemetry/best-effort-telemetry.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 119 | `src/adapters/telemetry/index.ts` | [Network/state](./01-network-and-state.md) |
|
||||
| 120 | `src/adapters/web-push/inbound/notification-click-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 121 | `src/adapters/web-push/inbound/push-event-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 122 | `src/adapters/web-push/index.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 123 | `src/adapters/web-push/notification-registry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 124 | `src/adapters/web-push/push-association-fence-store.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 125 | `src/adapters/web-push/push-codec.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 126 | `src/adapters/web-push/push-registration-gateway.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 127 | `src/adapters/web-push/push-subscription-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 128 | `src/adapters/web-push/runtime-support.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 129 | `src/adapters/web-push/service-worker-runtime.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
| 130 | `src/adapters/web-push/service-worker-scope-host.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||
|
||||
합계: **130/130**. 새 adapter 파일이 추가되면 이 ledger와 해당 상세 리뷰 inventory를 같은 변경에서 갱신한다.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,147 @@
|
||||
# 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를 장애 중 자동 전환하지 않는다.
|
||||
|
||||
## 테스트의 어댑터 import
|
||||
|
||||
`tests/` 아래 어댑터 import는 배럴로 일괄 이관하지 않는다. `check:architecture`는
|
||||
`src`만 스캔하므로 강제되지 않고, 단위 테스트의 상당수가 배럴에 없는 내부
|
||||
심볼을 의도적으로 겨눈다.
|
||||
|
||||
어떤 테스트 파일을 **다른 이유로** 수정하거나 분할할 때, 그 파일이 쓰는
|
||||
심볼이 해당 그룹 배럴에 있으면 그 파일 안에서만 배럴 경로로 바꾼다.
|
||||
배럴에 없는 심볼이면 깊은 경로를 유지한다. 배럴에 추가하고 싶으면 그 심볼이
|
||||
공개 표면임을 먼저 논증한다 — 테스트 편의로 배럴을 키우면 배럴이 경계가
|
||||
아니라 재수출 덤프가 된다.
|
||||
+255
-10
@@ -2,7 +2,17 @@
|
||||
|
||||
## Local blocking controls
|
||||
|
||||
- `pnpm install --frozen-lockfile` and a real manifest/lock mismatch fixture
|
||||
- `pnpm install --frozen-lockfile --ignore-scripts` and a real manifest/lock
|
||||
mismatch fixture; contract loading applies the root-only graph and lifecycle
|
||||
policy to every registered command before the gate runner can spawn one, and
|
||||
rejects nested installs without effective `--ignore-scripts`; npm script
|
||||
traversal includes existing pre/post hooks unless an ordered bare or explicit
|
||||
true `--ignore-scripts` suppresses them. Workspace/prefix and indirect
|
||||
user/global config authority are rejected in npm options, direct or dynamic
|
||||
assignments, exact `command`/`exec`/`env` prefix chains, cross-segment shell
|
||||
state, and the inherited runner environment. Unknown or cwd-changing `env`
|
||||
options and unquoted pre-delimiter pathname expansion fail closed, while the
|
||||
explicitly modeled non-scope `env` options remain usable
|
||||
- all direct and transitive lockfile rows with package SHA-512 integrity
|
||||
- production/development, direct/transitive and platform-optional classification
|
||||
- package-manifest license allow/deny policy
|
||||
@@ -19,8 +29,8 @@ The canonical commands are:
|
||||
```bash
|
||||
corepack pnpm verify:lockfile
|
||||
corepack pnpm verify:reproducible-build
|
||||
corepack pnpm build:release
|
||||
corepack pnpm verify:supply-chain
|
||||
corepack pnpm build:release-candidate
|
||||
corepack pnpm verify:local-evidence
|
||||
corepack pnpm check:supply-chain:fixtures
|
||||
```
|
||||
|
||||
@@ -31,16 +41,251 @@ rejected.
|
||||
|
||||
## External promotion controls
|
||||
|
||||
The vulnerability adapter reads the file named by
|
||||
`VULNERABILITY_REPORT_PATH`. It requires a provider, the exact lockfile digest,
|
||||
severity findings and valid independent, unexpired exception evidence.
|
||||
`PROVENANCE_ATTESTATION_PATH` must name a provider, signer and the exact built
|
||||
dist subject digest.
|
||||
Promotion reads the provider files named by `VULNERABILITY_REPORT_PATH` and
|
||||
`PROVENANCE_ATTESTATION_PATH`. The vulnerability report must bind both the
|
||||
exact lockfile digest and candidate `distSha256`; the provenance attestation
|
||||
must name `dist` with that same digest. Both documents use strict schemas and
|
||||
Ed25519 signatures verified with separately configured trusted public keys and
|
||||
key IDs (`VULNERABILITY_PUBLIC_KEY_PATH`, `VULNERABILITY_KEY_ID`,
|
||||
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID`). Keys of another curve,
|
||||
including Ed448, are rejected even if a document labels its algorithm
|
||||
`Ed25519`. The two roles must use different key IDs and different canonical
|
||||
DER-SPKI key bytes; giving the same key two IDs is rejected.
|
||||
|
||||
`immutable_build` archives the raw `pnpm-lock.yaml`, `dist` (including hidden
|
||||
`.vite` files), the build manifest, module inventory, release verification,
|
||||
secret-scan result, and local supply-chain evidence once. The candidate
|
||||
manifest hashes the raw lockfile bytes and requires that digest to equal the
|
||||
dependency inventory's `lockfileSha256`. Before upload, the producer validates
|
||||
the manifest-bound exact archive member set and every member digest, then
|
||||
publishes the archive SHA-256 as an immutable job output. The two provider jobs
|
||||
download this same archive separately, compare that output digest, validate the
|
||||
exact member set before extracting only into isolated roots, and receive
|
||||
`CANDIDATE_LOCKFILE_PATH` and `CANDIDATE_DIST_SHA256`; configured
|
||||
`VULNERABILITY_PROVIDER_COMMAND` and `PROVENANCE_PROVIDER_COMMAND` must emit the
|
||||
signed reports. After each external command returns, provider upload validation
|
||||
rechecks the unchanged archive and extracted candidate, parses the provider JSON
|
||||
with its strict schema, and binds its dist and lockfile digests before upload.
|
||||
|
||||
If either provider input is absent, local verification remains meaningful but
|
||||
`artifacts/security/supply-chain-verification.json` records
|
||||
`promotionStatus: FAIL_UNVERIFIED`. `verify:supply-chain:promotion` then exits
|
||||
non-zero. Scanner or signing outages are not converted to an empty PASS.
|
||||
`promotionStatus: FAIL_UNVERIFIED`. The finalizer and downstream
|
||||
`verify:promotion` exact-five validator then exit non-zero. Promotion derives
|
||||
the candidate file set and digests only from the captured tar bytes, then
|
||||
read-only revalidates archived executable schemas, archived policy/verifier
|
||||
source bytes, raw lockfile, module inventory, build outputs, release coherence,
|
||||
SBOM, provenance and supply-chain coherence. Candidate-internal checks are
|
||||
recomputed; for the checkout-dependent secret scan, promotion independently
|
||||
checks the archived policy, exact rule set, strict SARIF, zero findings and all
|
||||
manifest/assessment digest bindings. It does not claim to rescan source bytes
|
||||
that are not candidate members. It never rebuilds or rewrites candidate
|
||||
evidence and never falls back to the checkout tree.
|
||||
Promotion uploads the already verified archive itself with the two provider
|
||||
reports and generated verification records; it does not create a replacement
|
||||
archive from extracted files. Scanner or signing outages are not converted to
|
||||
an empty PASS.
|
||||
|
||||
The generated workflow is also a supply-chain control. `config/ci/gates.json`
|
||||
is its sole typed authority. Run `corepack pnpm generate:ci-workflow` after a
|
||||
contract change and `corepack pnpm check:ci-workflow` (or the encompassing
|
||||
`corepack pnpm check:ci`) to reject byte drift in the checked-in Gitea adapter.
|
||||
|
||||
Action resolution is separately closed over one typed, runtime-frozen registry
|
||||
in `scripts/contracts/ci-gates.ts`. Every generated `uses:` value is an absolute
|
||||
upstream URL pinned to a full commit SHA:
|
||||
|
||||
- `https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5`
|
||||
(`v4.3.1`)
|
||||
- `https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020`
|
||||
(`v4.4.0`)
|
||||
- `https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7`
|
||||
(patched `v4` branch)
|
||||
- `https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7`
|
||||
(patched `v4` branch)
|
||||
|
||||
Unknown actions, relative repositories, tags/branches and short SHAs are
|
||||
rejected. Gitea 1.22's official Actions documentation recommends the
|
||||
`ChristopherHX` patched artifact forks for v4 compatibility; the supported
|
||||
deployment baseline is nevertheless Gitea 1.26.4+ with Gitea Runner 1.0.0+.
|
||||
A real end-to-end provider smoke on the staging Gitea instance remains
|
||||
mandatory before any generated job becomes a required check.
|
||||
|
||||
External provider supervision is fail-closed and requires a Linux runner with
|
||||
executable `/usr/bin/bwrap`, `/usr/bin/prlimit`, `/usr/bin/systemd-run`, and
|
||||
`/usr/bin/systemctl`, bubblewrap support for `--size`, an active user bus, a
|
||||
systemd user manager version 254 or newer, unified cgroup v2, and delegated
|
||||
memory, pids, and CPU controllers. Executable access, user-manager/version,
|
||||
trust, archive, and path failures are rejected before raw creation. Bubblewrap
|
||||
`--size` acceptance and effective delegated controller values can only be
|
||||
verified after the owned raw inode exists inside a new scope; failures there
|
||||
remove that inode by identity and fail closed. Each invocation runs in a unique collected user scope and verifies
|
||||
its effective cgroup membership and limits before bubblewrap starts: memory is
|
||||
exactly 1 GiB, swap is zero, `TasksMax` is 64, and CPU quota is 100% per 100 ms.
|
||||
The inner process also has zero core size, a 8,388,607-byte file-size limit,
|
||||
64 open files and at most 1,200 CPU seconds. `TasksMax=64` is the authoritative
|
||||
aggregate PID boundary; no per-provider `RLIMIT_NPROC=32` claim is made because
|
||||
that limit is counted across the runner's same-UID process population.
|
||||
|
||||
Bubblewrap uses a private network namespace (`--unshare-net`), mounts the
|
||||
workspace and verified candidate read-only, hides `.git`, and read-only binds
|
||||
the trusted `process.execPath` at `/tmp/node`. The supervisor creates and pins
|
||||
the exact configured raw-report inode only after sandbox/trust/archive
|
||||
preflight; that inode is the only provider evidence path mounted read-write.
|
||||
The provider cannot write the surrounding `untrusted` directory, workspace,
|
||||
candidate, host home/toolcache, sealed evidence path, or general temporary
|
||||
filesystem. Pre-execution and provider failures remove only the supervisor-
|
||||
owned raw inode so the same job can retry without a stale empty report.
|
||||
|
||||
The complete bwrap argument/environment vector, including the provider command
|
||||
and provider-prefixed environment, is carried in a bounded length-prefixed frame over
|
||||
`systemd-run` stdin rather
|
||||
than placed in the supervisor, systemd, or bwrap wrapper argv. This prevents
|
||||
credentials from entering unit metadata and wrapper command lines. The final
|
||||
provider executable and its ordinary arguments remain visible to same-UID
|
||||
process inspection, so commands must never contain tokens or secrets. The same
|
||||
stdin remains open as a parent-liveness channel until normal provider exit; EOF
|
||||
caused by supervisor death makes the in-scope wrapper kill the provider process
|
||||
group and remove only the dev/inode-matched raw report. Supply
|
||||
credentials only through the provider-kind prefix
|
||||
(`VULNERABILITY_PROVIDER_*` or `PROVENANCE_PROVIDER_*`, excluding `*_COMMAND`).
|
||||
|
||||
A separate trusted guardian starts outside the provider scope and owns the
|
||||
filesystem transaction. Before spawn, the client opens and identity-checks the
|
||||
canonical raw and evidence directories with `O_DIRECTORY|O_NOFOLLOW`, derives
|
||||
the exact canonical and nonce-private leaves, and exclusively allocates empty
|
||||
mode-`0600` raw-staging and sealed-temp files. It records both dev/inode pairs
|
||||
before spawn and inherits the directory descriptors as guardian fd 3/fd 4 and
|
||||
the private file descriptors as fd 5/fd 6. Its argv contains only the trusted
|
||||
Node and helper paths. A bounded canonical v2 request carries only provider
|
||||
kind, an absolute deadline, and a random 32-byte control nonce.
|
||||
|
||||
At bootstrap the guardian fstats fd 5/fd 6 before reading their procfs links.
|
||||
Each procfs target is accepted only as a direct-child alias whose exact grammar,
|
||||
descriptor-relative lstat, type, mode, size, link count, and dev/inode match the
|
||||
already-recorded descriptor identity. The guardian publishes raw staging to the
|
||||
fixed raw leaf with a no-overwrite hard link, verifies both aliases at link
|
||||
count two, removes the private raw alias, syncs the raw directory, and verifies
|
||||
the canonical raw alias at link count one before authenticated READY. If startup
|
||||
ends before READY is accepted, the client cleans raw staging/canonical only when
|
||||
they match its pre-spawn raw identity and sealed temp/final only when they match
|
||||
its pre-spawn sealed identity. It never derives cleanup ownership by opening a
|
||||
current canonical pathname, so an external file or a concurrent same-kind
|
||||
winner is preserved. Provider execution starts only after the supervisor
|
||||
confirms that the returned identities and canonical targets match exactly.
|
||||
|
||||
After evidence validation, the supervisor writes schema-validated bytes to the
|
||||
pinned temp inode, changes it to `0400`, fsyncs it, and sends authenticated
|
||||
size/hash/identity metadata. The guardian verifies the held descriptor and
|
||||
pathname, publishes without replacement using same-directory `link`, removes
|
||||
the temp name, fsyncs the directory, and returns authenticated PUBLISHED. Only
|
||||
after successful `GITHUB_OUTPUT` append does the supervisor send commit. Commit
|
||||
removes the raw inode and enters `commitPending`; clean control EOF is the sole
|
||||
success terminal and preserves the sealed final. EOF without that terminal,
|
||||
deadline expiry, malformed/trailing control data, a wrong nonce, or premature
|
||||
guardian exit cleans every matching raw/temp/final identity and fails closed.
|
||||
Guardian loss while the provider scope is active also triggers whole-scope kill
|
||||
and collection.
|
||||
|
||||
`GITHUB_OUTPUT` is assumed to be a runner-owned regular file. This protocol
|
||||
does not claim OS-level cancellation of sealing or output I/O. If the guardian
|
||||
exits after scope collection, the scope-active latch records the lifecycle error
|
||||
without starting a late kill. Publication or terminal commit observes the
|
||||
nonzero exit and the client removes every identity-pinned raw/temp/final
|
||||
fallback before failing closed. The lease is bounded by the 30-minute provider
|
||||
wall limit plus a fixed ten-minute post-processing allowance.
|
||||
|
||||
A client hard stop after private allocation but before guardian spawn can leave
|
||||
only empty mode-`0600` nonce-private leaves. Automatic sweeping is intentionally
|
||||
omitted because an unrecorded pathname does not prove ownership; these private
|
||||
leaves cannot occupy the fixed raw or final names and do not block a retry.
|
||||
|
||||
Provider stdout and stderr are untrusted secret-bearing bytes. The supervisor
|
||||
does not retain or forward them to CI logs; it counts them only to enforce one
|
||||
1 MiB aggregate limit. Guardian diagnostics are best effort, so closed stderr
|
||||
or control descriptors cannot bypass cleanup or the required nonzero exit. The
|
||||
provider wall-clock limit is 30 minutes. Wall timeout, output overflow, parent-liveness loss,
|
||||
and active-scope guardian loss explicitly SIGKILL the whole provider process group. Ordinary command and
|
||||
RLIMIT failures complete through systemd with their concrete exit/signal; every
|
||||
path still waits for wrapper closure and requires the systemd unit/cgroup to be
|
||||
collected before returning. Provider adapters must therefore operate entirely from pre-populated
|
||||
offline data, consume the supplied candidate bindings, and write exactly the
|
||||
pinned report inode. Missing prerequisites, cgroup drift, stale or misplaced
|
||||
outputs, post-command candidate drift, and residual scope cleanup all stop
|
||||
publication.
|
||||
|
||||
Provider documents are strict schema v2. Their Ed25519 signature covers the
|
||||
supervisor-supplied evidence type, validity window, run ID/attempt, independent
|
||||
32-byte invocation nonce, archived source identity, and all four candidate
|
||||
digests. Each provider job exposes its supervisor-generated nonce as a job
|
||||
output; promotion treats those outputs as the independent expected values and
|
||||
never lets a report define its own expected nonce. A report from another
|
||||
attempt, source, archive, nonce, or key fingerprint is fail-closed even when it
|
||||
has been correctly re-signed.
|
||||
|
||||
The immutable archive contains a strict producer-local assessment plus the
|
||||
policy and verifier source bytes needed to validate its archived relationships.
|
||||
Promotion recomputes candidate-internal checks and validates the captured
|
||||
secret-scan policy/rules/SARIF/digest relationships from an isolated extraction
|
||||
root; it does not reopen checkout policy or source paths or claim to rescan
|
||||
unarchived checkout source. The finalizer captures the archive,
|
||||
both reports, and both public keys once, generates both verification v3 records
|
||||
in memory, and writes exactly five mode-`0400` files beneath a random
|
||||
mode-`0700` directory in `RUNNER_TEMP`, independently of a restrictive runner
|
||||
umask. The exact five are the captured archive, captured vulnerability report,
|
||||
captured provenance attestation, generated provider-verification v3, and
|
||||
generated promotion-verification v3. Before returning, the finalizer validates
|
||||
those exact bytes again with live-time provider signature/freshness checks. The
|
||||
promotion record binds the exact provider-record hash, local-assessment hash,
|
||||
both report hashes, run/source/candidate identities, both nonces, both key
|
||||
IDs/fingerprints, canonical trust-policy hash, and the vulnerability provider's
|
||||
signed `secretScanAttestation`. That strict attestation says `PASS` and binds
|
||||
the captured local-assessment, source-set, secret-scan policy, SARIF, and actual
|
||||
scan-input digests. The supervisor derives the expected tuple from the captured
|
||||
archive and exact equality is rechecked at upload and finalization. The trusted
|
||||
vulnerability provider remains responsible for independently scanning that
|
||||
source set and refusing to sign a forged empty SARIF or incomplete scan input;
|
||||
the signature proves the provider made the claim, not that an untrusted
|
||||
provider performed the scan honestly. It never creates or reuses
|
||||
`.release/promoted-staging`.
|
||||
|
||||
The final promotion verification/staging step must be immediately adjacent to
|
||||
the promoted-release upload, and that upload must not use `always()`. This
|
||||
reduces the post-verification mutation window but does not seal a pathname
|
||||
across two action steps. The runner is therefore required to be trusted,
|
||||
exclusive and single-tenant, with no provider command or other same-UID process
|
||||
surviving from staging into the immediately following upload. The artifact
|
||||
service and transfer actions also remain outside the candidate's cryptographic
|
||||
identity: every downstream consumer must revalidate the downloaded archive,
|
||||
manifest member digests and signed provider evidence. Producer-side adjacency
|
||||
does not provide consumer-side digest revalidation.
|
||||
The promotion job has no job-level `if`: ordinary `needs` success semantics
|
||||
require immutable build and both provider jobs to succeed, and cancellation is
|
||||
not overridden with `always()` or `cancelled()`. Cleanup alone uses bare `always()`
|
||||
and is guarded by all six finalizer outputs: staging path, token, parent
|
||||
device/inode, and staging-leaf device/inode. Cleanup opens the pinned leaf,
|
||||
requires the exact five names, unlinks only those known files through the
|
||||
descriptor, and uses a non-recursive `rmdir`; an exchanged directory or canary
|
||||
is never recursively removed.
|
||||
The immediately following upload action still reopens pathnames. The
|
||||
descriptor-relative staging and cleanup code does not claim an atomic
|
||||
`renameat2` handoff or close a malicious same-UID Gitea upload adapter; the
|
||||
staging Gitea smoke/native platform adapter remains the required closure for
|
||||
that boundary. That smoke must exercise exact-five upload and download plus
|
||||
cleanup on success, validation failure, upload failure, and cancellation. No
|
||||
native uploader or `renameat2` guarantee exists in this repository today.
|
||||
Portable Node also cannot make `mkdir` plus the first pathname `lstat` atomic.
|
||||
The implementation compares the immediate post-`mkdir` identity with the
|
||||
subsequent `O_DIRECTORY|O_NOFOLLOW` descriptor before changing permissions, but
|
||||
a malicious same-UID actor that wins before that first `lstat` remains part of
|
||||
the native/privilege boundary. The private `0700` runner-temp ancestor and
|
||||
single-tenant runner requirement are therefore security controls, not merely
|
||||
hardening.
|
||||
Failure cleanup is armed only after that created device/inode matches the opened
|
||||
descriptor. If the opened descriptor is a replacement, it is closed without
|
||||
unlinking or removing anything; the visible replacement is likewise untouched.
|
||||
Portable Node cannot safely rediscover an attacker-moved original directory by
|
||||
scanning the parent, so that residual must be removed by the isolated test
|
||||
fixture or trusted runner/native cleanup after the attacker is excluded.
|
||||
|
||||
Approved vulnerability exceptions require vulnerability/package identity,
|
||||
owner, a different reviewer, reason and expiry. Expired or self-approved
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# HTTP Worker and Adapter Remediation 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:** Remove duplicate HTTP authorities, harden Service Worker activation bounds and identity, and decompose large browser adapters only after shared golden behavior is locked.
|
||||
|
||||
**Architecture:** Installed contract contributions are the HTTP source of truth and expose provider-neutral typed outcomes. Worker protocol V2 exchanges a canonical full-identity digest. Browser adapter facades remain stable while shared persisted schemas and cohesive internal modules are extracted.
|
||||
|
||||
**Tech Stack:** TypeScript 7, Fetch API, Service Worker API, IndexedDB, OPFS, React 19, Vitest 4, Playwright.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Contract and application layers never import concrete adapter outcome types.
|
||||
- Production exports accept only `BoundQuery` and `BoundMutation` after migration.
|
||||
- Runtime timeout is a global ceiling applied over descriptor deadlines.
|
||||
- Worker marker reads are bounded even without `Content-Length` and cancel oversized/non-terminating streams.
|
||||
- Public adapter facades and product-default optional capability selection remain unchanged.
|
||||
- Extraction follows characterization tests; file length alone does not justify a split.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Installed HTTP contract as single source of truth
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/features/reference-feature/contracts/reference-feature-contract.ts`
|
||||
- Modify: `src/features/reference-feature/contracts/reference-schemas.ts`
|
||||
- Modify: `src/contracts/external-contract-runtime.ts`
|
||||
- Modify: `src/contracts/api-operations.ts`
|
||||
- Modify: `src/contracts/rest-profiles.ts`
|
||||
- Modify: `src/contracts/schema-registry.ts`
|
||||
- Modify: `src/features/installed-feature-contracts.ts`
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts`
|
||||
- Modify: `tests/features/reference-feature/reference-contract.test.ts`
|
||||
- Modify: `tests/runtime-schema/http-schema.test.ts`
|
||||
- Modify: `tests/integration/http-execution-contract.test.ts`
|
||||
- Modify: `tests/unit/http-execution-v3.test.ts`
|
||||
|
||||
- [ ] Add parity tests showing method/path/validators/retry/effect/deadline/byte bounds come from one contribution; `createdAt` accepts omitted or RFC3339 datetime and rejects arbitrary strings.
|
||||
- [ ] Run focused tests and confirm RED on duplicated descriptors and permissive date schema.
|
||||
- [ ] Make the installed contribution authoritative; generate temporary legacy views from it and migrate all production callers before deleting the legacy registries/codecs.
|
||||
- [ ] Apply `REQUEST_TIMEOUT_MS` as `min(runtimeCeiling, descriptorDeadline)` without replacing shorter descriptor deadlines.
|
||||
- [ ] Keep the deadline authoritative through response admission and body reads: a deadline-owned abort after headers must still return `TRANSPORT_FAILURE / TIMEOUT`, perform one attempt, and never enter retry sleep instead of being masked as `RESPONSE_STREAM_FAILURE`.
|
||||
- [ ] Treat credential resolution as a credential-only patch boundary: reject attempts to inject or overwrite `Idempotency-Key` (including case variants) or transport/query authority, and cover hostile patches in the executor regression table.
|
||||
- [ ] Prove diagnostic privacy against the full runtime seam with non-empty identity references; assertions over an empty diagnostic projection do not count as evidence that intent/key/query identity is absent.
|
||||
- [ ] Re-run focused tests, prove `rg` has zero production callers of removed registries, and commit with `git commit -m "refactor: consolidate installed HTTP contracts"`.
|
||||
|
||||
### Task 2: Provider-neutral typed operation outcomes
|
||||
|
||||
**Files:**
|
||||
- Create: `src/contracts/operation-outcome.ts`
|
||||
- Create: `src/application/ports/contract-operation-executor.ts`
|
||||
- Modify: `src/features/reference-feature/adapters/reference-http-gateway.ts`
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts`
|
||||
- Modify: `src/adapters/http/http-execution-v3.ts`
|
||||
- Modify: `tests/features/reference-feature/reference-contract.test.ts`
|
||||
- Modify: `tests/unit/runtime-adapters.test.ts`
|
||||
|
||||
- [ ] Add compile/runtime tests that unknown operation IDs and mismatched input/output types fail, and that the feature gateway has no import from `src/adapters/http`.
|
||||
- [ ] Run focused tests/typecheck and confirm RED because the port is `operationId: string`, `input: unknown`, and concrete `HttpExecutionOutcome` leaks inward.
|
||||
- [ ] Derive `InstalledOperationMap` from installed contracts, expose generic `execute<K extends keyof Map>(operationId: K, input: Map[K]["input"], context)` and map HTTP outcomes to provider-neutral contract outcomes at the adapter boundary.
|
||||
- [ ] Re-run tests, typecheck, and architecture; commit with `git commit -m "refactor: type installed contract operations"`.
|
||||
|
||||
### Task 3: Bound-only server-state exports
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/presentation/adapters/query/application-query.ts`
|
||||
- Modify: `src/presentation/adapters/query/index.ts`
|
||||
- Modify: `src/features/reference-feature/presentation/use-reference-feature.ts`
|
||||
- Create: `tests/helpers/legacy-application-query-harness.tsx`
|
||||
- Modify: `tests/component/application-query.test.tsx`
|
||||
|
||||
- [ ] Add type tests that production hooks reject raw query keys and raw mutation executors while bound definitions still compile.
|
||||
- [ ] Run typecheck and confirm current overloads accept raw forms.
|
||||
- [ ] Move legacy raw harness behavior under `tests/helpers`; remove `LegacyMutationOptions` and the raw query union from production exports; migrate feature callers to `bindQuery`/bound mutations.
|
||||
- [ ] Run focused component tests and typecheck; commit with `git commit -m "refactor: expose bound server-state hooks only"`.
|
||||
|
||||
### Task 4: Bounded Service Worker marker reader
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapters/service-worker/bounded-worker-response.ts`
|
||||
- Modify: `src/adapters/service-worker/service-worker-lifecycle.ts`
|
||||
- Modify: `tests/unit/service-worker-runtime.test.ts`
|
||||
|
||||
- [ ] Add tests for oversized declared length, headerless oversized chunks, invalid UTF-8, malformed JSON, and a non-terminating stream. Assert reader cancellation and bounded completion.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` and confirm RED because lifecycle calls `response.text()`.
|
||||
- [ ] Implement realm-safe stream reads up to `maxBytes + 1`, fatal `TextDecoder`, explicit cancellation, and strict marker parsing. Never call `Response.text()` for protocol data.
|
||||
- [ ] Re-run tests and commit with `git commit -m "fix: bound Service Worker activation markers"`.
|
||||
|
||||
### Task 5: Service Worker protocol V2 full identity
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/contracts/service-worker.ts`
|
||||
- Modify: `src/adapters/service-worker/service-worker-protocol.ts`
|
||||
- Modify: `src/adapters/service-worker/service-worker-entry.ts`
|
||||
- Modify: `src/adapters/service-worker/service-worker-lifecycle.ts`
|
||||
- Modify: `src/adapters/service-worker/service-worker-page-controller.ts`
|
||||
- Modify: `src/bootstrap/register-service-worker.ts`
|
||||
- Modify: `scripts/generate-build-manifest.ts`
|
||||
- Modify: `tests/unit/service-worker-runtime.test.ts`
|
||||
- Modify: `tests/unit/service-worker-build-input.test.ts`
|
||||
|
||||
- [ ] Add a tuple-mutation table for protocol/cache schema/build/release/contract/static set; each mutation must change the digest and reject activation. Add a valid ACTIVE fixture build that recomputes static set digest from asset entries.
|
||||
- [ ] Run focused tests and confirm RED because protocol V1 compares partial fields.
|
||||
- [ ] Set `SERVICE_WORKER_PROTOCOL_VERSION = 2`, define canonical sorted identity serialization, compute SHA-256 over every identity field, and exchange/validate the digest on every page-worker message.
|
||||
- [ ] Keep default capability selection `null`; use ACTIVE only in the explicit fixture build.
|
||||
- [ ] Re-run focused tests and the supported fixture build; commit with `git commit -m "fix: bind Service Worker activation to full identity"`.
|
||||
|
||||
### Task 6: Shared IndexedDB persisted-row schema
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapters/storage/indexeddb/indexeddb-persisted-schema.ts`
|
||||
- Modify: `src/adapters/storage/indexeddb/indexeddb-types.ts`
|
||||
- Modify: `src/adapters/storage/indexeddb/indexeddb-runtime.ts`
|
||||
- Modify: `src/adapters/storage/indexeddb/indexeddb-maintenance.ts`
|
||||
- Create: `tests/fixtures/indexeddb/persisted-rows.ts`
|
||||
- Modify: `tests/unit/indexeddb-runtime.test.ts`
|
||||
- Modify: `tests/unit/indexeddb-maintenance.test.ts`
|
||||
|
||||
- [ ] Before extraction, run the same accepted/rejected record, receipt, retention, and budget golden rows through runtime and maintenance and assert identical verdicts.
|
||||
- [ ] Confirm RED on at least one drift fixture using the duplicate current guards.
|
||||
- [ ] Move persisted types/guards into the shared module; runtime and maintenance import it without behavior changes.
|
||||
- [ ] Re-run both large suites and commit with `git commit -m "refactor: share IndexedDB persisted schemas"`.
|
||||
|
||||
### Task 7: Cohesive browser adapter decomposition
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapters/storage/opfs/opfs-worker-runtime.ts`
|
||||
- Create: `src/adapters/storage/opfs/opfs-worker-bootstrap.ts`
|
||||
- Create: `src/adapters/storage/opfs/opfs-worker-message-host.ts`
|
||||
- Create: `src/adapters/storage/opfs/opfs-worker-core.ts`
|
||||
- Create: `src/adapters/storage/opfs/opfs-worker-lock.ts`
|
||||
- Create: `src/adapters/storage/opfs/opfs-physical-io.ts`
|
||||
- Modify: `src/adapters/cache-storage/public-response-cache-adapter.ts`
|
||||
- Create: `src/adapters/cache-storage/public-cache-manifest.ts`
|
||||
- Create: `src/adapters/cache-storage/cache-lock.ts`
|
||||
- Modify: `src/adapters/browser-files/download-delivery-adapter.ts`
|
||||
- Create: `src/adapters/browser-files/download-browser-managed.ts`
|
||||
- Create: `src/adapters/browser-files/download-picker-stream.ts`
|
||||
- Create: `src/adapters/browser-files/download-object-url.ts`
|
||||
- Modify: `tests/unit/opfs-worker-runtime.test.ts`
|
||||
- Modify: `tests/unit/public-response-cache.test.ts`
|
||||
- Modify: `tests/unit/browser-file-download.test.ts`
|
||||
|
||||
- [ ] Add golden facade tests for all success/failure/cancellation/lock-loss branches before moving code; snapshot externally observable operation order and error kinds.
|
||||
- [ ] Run the three focused suites and capture GREEN characterization evidence.
|
||||
- [ ] Extract OPFS bootstrap, host, core state machine, Web Lock, and physical I/O without changing public exports. Do not split the core state machine further.
|
||||
- [ ] Extract public-cache manifest codec/digest and generic lock logic behind the same facade.
|
||||
- [ ] Extract browser-managed, picker streaming, and object-URL download strategies behind the same delivery facade.
|
||||
- [ ] Re-run the same golden suites after each extraction. Any failure is a refactor regression, not a fixture update.
|
||||
- [ ] Commit each adapter independently with `refactor: decompose OPFS worker adapter`, `refactor: extract public cache internals`, and `refactor: extract download delivery strategies`.
|
||||
|
||||
### Task 8: HTTP/worker/adapter verification
|
||||
|
||||
- [ ] Run all focused tests named in Tasks 1–7.
|
||||
- [ ] Run `corepack pnpm check:architecture`, `corepack pnpm check:types`, and `corepack pnpm lint`.
|
||||
- [ ] Run `corepack pnpm test:all`.
|
||||
- [ ] Run supported Service Worker, IndexedDB, OPFS, public-cache, and download Playwright capability specs.
|
||||
- [ ] Run `git diff --check` and report unsupported browser gates without claiming success.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Quality and Architecture Remediation 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:** Make coverage, scenario, CI, and architecture gates measure executable production behavior and fail when their measured universe is empty or incomplete.
|
||||
|
||||
**Architecture:** One typed gate schema drives both the local runner and generated workflow. Coverage separates repository inventory from instrumented totals. The Babel/resolver graph is the sole architecture authority while TypeScript 7 is unsupported by dependency-cruiser.
|
||||
|
||||
**Tech Stack:** TypeScript 7, Node.js 24, Babel parser, Vitest 4, Playwright, Gitea Actions.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- A zero-file or zero-module result is failure, never success.
|
||||
- High-risk changed modules need explicit coverage ownership or an owned, expiring waiver.
|
||||
- Scenario declarations count only when a table-driven test executes all required assertions.
|
||||
- Checked-in workflow content is generated deterministically from the same gate model used locally.
|
||||
- Every enforcement change begins with a failing fixture.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Repository-aware risk coverage
|
||||
|
||||
**Files:**
|
||||
- Modify: `vitest.config.ts`
|
||||
- Modify: `config/testing/risk-coverage.json`
|
||||
- Modify: `scripts/check-risk-coverage.ts`
|
||||
- Modify: `tests/fixtures/coverage/below-threshold.json`
|
||||
- Create: `tests/fixtures/coverage/repository-omission.json`
|
||||
- Create: `tests/unit/risk-coverage.test.ts`
|
||||
|
||||
- [ ] Add tests asserting `selectedTotal`, `repositoryTotal`, uncovered repository modules, and changed high-risk ownership. A summary covering 14 files while production inventory is larger must fail.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/risk-coverage.test.ts` and confirm RED because only selected totals exist.
|
||||
- [ ] Enumerate every production `.ts`/`.tsx` module under `src`, exclude declarations/stories/generated files explicitly, and emit both totals. Expand coverage instrumentation to `src/**/*.{ts,tsx}` with documented exclusions.
|
||||
- [ ] Seed the critical registry with HTTP V3, bounded request/response readers, boot bounds, Service Worker lifecycle, scope generation, and release loading. Validate waiver owner, reason, and future expiry.
|
||||
- [ ] Run focused tests and `corepack pnpm test:coverage`; commit with `git commit -m "fix: measure repository-wide risk coverage"`.
|
||||
|
||||
### Task 2: Executable HTTP scenario catalog
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/mocks/scenarios/catalog.ts`
|
||||
- Create: `tests/integration/http-scenario-catalog.test.ts`
|
||||
- Modify: `tests/mocks/handlers/reference-resources.ts`
|
||||
- Modify: `scripts/check-test-evidence.ts`
|
||||
- Modify: `config/testing/test-evidence.json`
|
||||
|
||||
- [ ] Define typed expectations for status/outcome/effect/retry/fetch count/media type/body bound/scope fence for every declared scenario.
|
||||
- [ ] Add a table-driven test that executes each operation/scenario pair through `ContractHttpExecutor` and asserts every expectation field. Add a deliberately declared-but-unexecuted fixture and make the evidence checker reject it.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/integration/http-scenario-catalog.test.ts && node scripts/check-test-evidence.ts` and confirm RED because the current gate counts source tokens.
|
||||
- [ ] Export execution receipts from the test artifact and make the checker compare exact catalog IDs to exact executed IDs; source-token counts become diagnostics only.
|
||||
- [ ] Re-run tests/checker and commit with `git commit -m "test: execute the HTTP scenario catalog"`.
|
||||
|
||||
### Task 3: Shared CI gate schema and deterministic workflow generation
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/contracts/ci-gates.ts`
|
||||
- Create: `scripts/generate-ci-workflow.ts`
|
||||
- Modify: `scripts/run-ci-gate.ts`
|
||||
- Modify: `config/ci/gates.json`
|
||||
- Modify: `.gitea/workflows/quality-gates.yml`
|
||||
- Create: `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
- [ ] Add invalid gate fixtures for unknown fields, duplicate IDs, missing artifact schemas, unknown dependencies, and cycles. Add a snapshot test for the full generated workflow plus `--check` drift.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/ci-workflow-generation.test.ts` and confirm RED because no shared parser/generator exists.
|
||||
- [ ] Parse gates once with strict Zod schemas. Generate every job, dependency, command, environment mapping, timeout, artifact upload/download, and schema validation deterministically.
|
||||
- [ ] Replace regex/token workflow checks with `node scripts/generate-ci-workflow.ts --check`; generated YAML must match byte-for-byte.
|
||||
- [ ] Re-run tests and check mode; commit with `git commit -m "refactor: generate CI workflow from gate contracts"`.
|
||||
|
||||
### Task 3b: Semantic validation for every CI evidence format
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/contracts/release-artifacts.ts`
|
||||
- Modify: producer scripts for the remaining generic JSON evidence
|
||||
- Modify: `scripts/lib/ci-artifact-validator.ts`
|
||||
- Modify: `config/ci/gates.json`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
- [x] Inventory every artifact still mapped to `generic-json-object` and export/reuse the producer's strict schema, including cross-field status/failure/count invariants. Do not treat a non-empty JSON object as semantic evidence.
|
||||
- [x] Replace substring-only JUnit/HTML acceptance with bounded well-formed document validation. Reject DTD/entities, malformed nesting, duplicate/invalid roots, and trailing non-whitespace content.
|
||||
- [x] Add invalid-but-pattern-matching fixtures for all structured kinds and a table proving every configured artifact resolves to a semantic validator.
|
||||
- [ ] Run focused artifact tests, `corepack pnpm check:ci`, types, lint, and diff checks; commit separately so this evidence-quality closeout is independently reviewable.
|
||||
|
||||
### Task 4: One authoritative architecture graph
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/check-architecture.ts`
|
||||
- Modify: `config/architecture/layers.json`
|
||||
- Modify: `.dependency-cruiser.json`
|
||||
- Create: `tests/fixtures/architecture/forbidden/contracts-import-application.ts`
|
||||
- Create: `tests/fixtures/architecture/forbidden/feature-adapter-imports-global-adapter.ts`
|
||||
- Create: `tests/unit/architecture-policy.test.ts`
|
||||
|
||||
- [ ] Add fixtures proving contracts cannot import application/runtime layers, feature adapters cannot import concrete global adapters, unresolved imports fail, cycles fail, and a zero-module root fails.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/architecture-policy.test.ts` and confirm missing rules/zero-module behavior fail.
|
||||
- [ ] Make the Babel parser plus Node/TS resolver graph authoritative. Keep dependency-cruiser output informational while it sees zero TS7 modules, and explicitly fail authoritative counts of zero modules or zero dependencies in a non-empty source tree.
|
||||
- [ ] Add the two dependency-direction rules to the typed layer policy and ensure aliases/extensions resolve identically to TypeScript.
|
||||
- [ ] Run focused tests and `corepack pnpm check:architecture`; commit with `git commit -m "fix: enforce architecture with the TS7 graph"`.
|
||||
|
||||
### Task 5: Test hygiene and production read/write E2E
|
||||
|
||||
**Files:**
|
||||
- Modify: `vitest.config.ts`
|
||||
- Modify: `tests/setup.ts`
|
||||
- Modify: `playwright.config.ts`
|
||||
- Modify: `playwright.dev.config.ts`
|
||||
- Modify: `playwright.storybook.config.ts`
|
||||
- Modify: `playwright.visual.config.ts`
|
||||
- Create: `tests/e2e/reference-resource-write.spec.ts`
|
||||
- Modify: `scripts/check-test-evidence.ts`
|
||||
|
||||
- [ ] Add a fixture containing `.only` and a leaking fake timer; assert the gate rejects/isolation restores them. Assert every Playwright config resolves `forbidOnly: true`.
|
||||
- [ ] Add E2E that loads a real mocked GET response, submits POST, verifies request body/header contract, verifies response-rendered resource, then reloads and verifies read-after-write.
|
||||
- [ ] Run focused Vitest and Playwright tests and confirm RED for inherited configs/current shallow E2E.
|
||||
- [ ] Enable Vitest sequence hook that rejects `.only`, restore real timers in common `afterEach`, and centralize a Playwright base config with `forbidOnly: true` inherited by all configs.
|
||||
- [ ] Make E2E evidence require both observed response and observed mutation receipt.
|
||||
- [ ] Re-run supported tests; commit with `git commit -m "test: harden test isolation and read-write E2E"`.
|
||||
|
||||
### Task 6: Quality verification
|
||||
|
||||
- [ ] Run `corepack pnpm check:architecture`.
|
||||
- [ ] Run `corepack pnpm test:coverage`.
|
||||
- [ ] Run `node scripts/check-test-evidence.ts`.
|
||||
- [ ] Run `node scripts/generate-ci-workflow.ts --check`.
|
||||
- [ ] Run `corepack pnpm test:all`, `corepack pnpm check:types`, `corepack pnpm lint`, and `git diff --check`.
|
||||
- [ ] Run browser/E2E gates only when the environment supports them and report exact commands separately.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Release Evidence Remediation 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:** Build one immutable release bundle and fail promotion unless every artifact, provider report, schema, and digest proves it describes those exact bytes.
|
||||
|
||||
**Architecture:** Executable Zod contracts validate artifacts at every writer boundary. One fail-closed tracked-file inventory feeds security and provenance. CI creates the candidate once, scans that candidate, then promotes it without rebuilding.
|
||||
|
||||
**Tech Stack:** TypeScript 7, Node.js 24, Zod 4, Vite 8, Gitea Actions, Vitest 4.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Repository code validates but never fabricates external vulnerability or signed provenance evidence.
|
||||
- Missing evidence, unreadable required roots, tool crashes, signals, timeouts, and digest mismatch fail closed.
|
||||
- V2 release identity is its exact contract package set and recomputed set digest; no scalar version is synthesized.
|
||||
- Promotion consumes the same archived `dist` and `distSha256` produced by `immutable_build`.
|
||||
- All production changes start with a failing fixture or unit test.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: One V1/V2 runtime coherence verifier
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/lib/release-runtime-coherence.ts`
|
||||
- Modify: `scripts/verify-release.ts`
|
||||
- Modify: `scripts/drill-runbook.ts`
|
||||
- Modify: `src/contracts/release-tokens.ts`
|
||||
- Modify: `tests/unit/release-coherence.test.ts`
|
||||
- Modify: `tests/unit/release-artifacts.test.ts`
|
||||
|
||||
- [ ] Add a shared matrix covering V1 scalar success/mismatch and V2 package add/remove/version/digest tampering. Assert verifier and rollback drill return identical verdicts.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/release-coherence.test.ts tests/unit/release-artifacts.test.ts` and confirm RED because the drill compares only scalar release tokens.
|
||||
- [ ] Implement async `verifyReleaseRuntimeCoherence({ release, runtime, contractPackages })`; V1 delegates to legacy scalar policy, V2 checks exact sorted package tuples then recomputes `contractSet.setDigest`.
|
||||
- [ ] Remove V2 synthetic `0`/legacy scalar projection from `release-tokens.ts`; call the shared verifier from both scripts.
|
||||
- [ ] Re-run the focused tests and commit with `git commit -m "fix: unify release runtime coherence verification"`.
|
||||
|
||||
### Task 2: Validated artifact writers and generated JSON schemas
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/lib/validated-json-artifact.ts`
|
||||
- Create: `scripts/generate-artifact-schemas.ts`
|
||||
- Modify: `scripts/contracts/release-artifacts.ts`
|
||||
- Modify: `scripts/generate-build-manifest.ts`
|
||||
- Modify: `scripts/generate-supply-chain.ts`
|
||||
- Modify: `scripts/collect-web-vitals-evidence.ts`
|
||||
- Modify: `scripts/test-performance.ts`
|
||||
- Modify: `scripts/verify-release.ts`
|
||||
- Modify: `scripts/drill-runbook.ts`
|
||||
- Modify: `schemas/artifacts/build-manifest.schema.json`
|
||||
- Modify: `schemas/artifacts/dependency-inventory.schema.json`
|
||||
- Modify: `schemas/artifacts/registry-snapshot.schema.json`
|
||||
- Modify: `schemas/artifacts/supply-chain-verification.schema.json`
|
||||
- Create: `tests/unit/validated-json-artifact.test.ts`
|
||||
- Modify: `tests/unit/release-artifacts.test.ts`
|
||||
- Modify: `tests/unit/json-schema.test.ts`
|
||||
|
||||
- [ ] Add tests proving invalid values do not touch the destination, a valid write is atomic, and `generate-artifact-schemas.ts --check` reports checked-in drift.
|
||||
- [ ] Run focused tests and confirm RED because writers call `writeFile` directly and schemas are hand-maintained.
|
||||
- [ ] Implement `writeValidatedJsonArtifact({ path, schema, value })`: parse first, write a sibling temporary file, rename atomically, and clean only its explicit temp file on failure.
|
||||
- [ ] Route every listed writer through the helper. Generate draft-2020-12 schemas deterministically with `additionalProperties: false` and stable final newline.
|
||||
- [ ] Add `generate:artifact-schemas` and `check:artifact-schemas` scripts; run generation then check mode.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/validated-json-artifact.test.ts tests/unit/release-artifacts.test.ts tests/unit/json-schema.test.ts` and commit with `git commit -m "refactor: validate generated evidence artifacts"`.
|
||||
|
||||
### Task 3: Manifest outputs and fail-closed repository inventory
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/lib/repository-file-inventory.ts`
|
||||
- Create: `scripts/lib/build-manifest-outputs.ts`
|
||||
- Modify: `scripts/generate-supply-chain.ts`
|
||||
- Modify: `scripts/security-scan.ts`
|
||||
- Modify: `scripts/verify-release.ts`
|
||||
- Modify: `config/security/secret-scan-policy.json`
|
||||
- Modify: `tests/unit/supply-chain.test.ts`
|
||||
- Create: `tests/unit/repository-file-inventory.test.ts`
|
||||
- Modify: `tests/unit/release-artifacts.test.ts`
|
||||
|
||||
- [ ] Add fixtures for missing required root, optional `ENOENT`, unreadable file, untracked omission, path traversal, module-inventory tamper, and hash mismatch.
|
||||
- [ ] Run focused tests and confirm current discovery skips read failures and verification accepts a stale `moduleInventoryHash`.
|
||||
- [ ] Build inventory from `git ls-files -z` plus explicitly generated inputs; normalize and confine every path under repository root. Only configured optional roots may ignore exact `ENOENT`.
|
||||
- [ ] Make provenance and secret scan consume the same inventory. Add `index.html`, Vite configs, all TS configs, `.nvmrc`, package/lock files, scripts, schemas, configs, and `.gitea/workflows/quality-gates.yml` to mandatory policy coverage.
|
||||
- [ ] Implement `verifyBuildManifestOutputs` to confine declared output paths, read module inventory bytes, and compare raw SHA-256 to `moduleInventoryHash`.
|
||||
- [ ] Re-run focused tests and commit with `git commit -m "fix: fail closed on release input discovery"`.
|
||||
|
||||
### Task 4: Immutable candidate, provider evidence, and promotion
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `scripts/generate-supply-chain.ts`
|
||||
- Modify: `scripts/verify-supply-chain-artifacts.ts`
|
||||
- Modify: `scripts/verify-supply-chain-promotion.ts`
|
||||
- Modify: `scripts/check-supply-chain-provider-fixtures.ts`
|
||||
- Modify: `tests/unit/supply-chain.test.ts`
|
||||
- Modify: `.gitea/workflows/quality-gates.yml`
|
||||
|
||||
- [ ] Add fixtures for absent provider evidence, valid matching digest, wrong digest, and post-attestation byte change. Assert only the valid immutable fixture passes promotion.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/supply-chain.test.ts && corepack pnpm check:supply-chain:provider-fixtures` and confirm RED for promotion wiring.
|
||||
- [ ] Split scripts into `build:release-candidate`, `verify:local-evidence`, `verify:provider-evidence`, and `verify:promotion`; remove any build command from promotion.
|
||||
- [ ] `immutable_build` archives `dist`, build manifest, module inventory, and local evidence together and publishes `distSha256`. Provider jobs download that archive and emit reports bound to the digest.
|
||||
- [ ] Promotion downloads the same archive plus provider reports, exports `VULNERABILITY_REPORT_PATH` and `PROVENANCE_ATTESTATION_PATH`, verifies all schemas/signatures/digests, and uploads/deploys the unchanged bundle.
|
||||
- [ ] Verify missing external evidence remains `FAIL_UNVERIFIED`; do not add a repository-generated passing provider fixture to production flow.
|
||||
- [ ] Re-run fixtures and the workflow contract check, then commit with `git commit -m "fix: promote immutable verified release bundles"`.
|
||||
|
||||
### Task 5: Release/evidence verification
|
||||
|
||||
- [ ] Run `corepack pnpm check:artifact-schemas`.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts tests/unit/release-coherence.test.ts tests/unit/validated-json-artifact.test.ts tests/unit/repository-file-inventory.test.ts tests/unit/supply-chain.test.ts tests/unit/json-schema.test.ts`.
|
||||
- [ ] Run `corepack pnpm check:supply-chain:fixtures` and `corepack pnpm check:supply-chain:provider-fixtures`.
|
||||
- [ ] Run the candidate build and local release verification with deterministic local environment values.
|
||||
- [ ] Confirm promotion fails specifically with `FAIL_UNVERIFIED` when real external evidence paths are absent.
|
||||
- [ ] Run `corepack pnpm check:types`, `corepack pnpm lint`, and `git diff --check`.
|
||||
@@ -0,0 +1,281 @@
|
||||
# Runtime Correctness Remediation 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:** Make topic invalidation hit every real bound query and make each logical mutation preserve a unique intent and effect-aware optimistic state.
|
||||
|
||||
**Architecture:** Contracts own query-key and mutation-intent shapes. Bootstrap indexes feature invalidation contributions once. Presentation creates one intent per admitted logical submit, while HTTP consumes that intent and optimistic settlement follows the returned effect certainty.
|
||||
|
||||
**Tech Stack:** TypeScript 7, React 19, TanStack Query 5, Zod 4, Vitest 4.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Query persistence remains disabled; the key-version change has no persisted migration.
|
||||
- Query keys and invalidation prefixes are created only by `src/contracts/query-keys.ts`.
|
||||
- Cross-context messages carry topics, never namespace IDs, query keys, input identities, intent IDs, or idempotency keys.
|
||||
- A logical mutation creates one intent after duplicate admission and reuses it for every physical attempt.
|
||||
- A missing `KEYED` idempotency key fails before credentials, fetch admission, or diagnostics containing caller data.
|
||||
- `MAYBE_APPLIED` never rolls back, commits, invalidates, or retries automatically.
|
||||
- Every production behavior change is preceded by a focused failing test.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Query-key V2 and namespace identities
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/contracts/query-keys.ts`
|
||||
- Modify: `src/contracts/server-state.ts`
|
||||
- Modify: `tests/component/application-query.test.tsx`
|
||||
- Modify: `tests/unit/query-invalidation-registry.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Adds `QUERY_KEY_SCHEMA_VERSION = 2`.
|
||||
- Adds `QueryNamespaceIdentity = { namespaceId: string; namespaceVersion: number }`.
|
||||
- Adds `defineQueryNamespaceIdentity`, `createBoundQueryKey`, `createQueryInvalidationPrefix`, and `queryNamespaceIdentityKey`.
|
||||
- Changes `bindQuery` to delegate key construction to `createBoundQueryKey`.
|
||||
|
||||
- [ ] **Step 1: Add failing key/prefix parity tests**
|
||||
|
||||
```ts
|
||||
const namespace = defineQueryNamespaceIdentity("reference-resource", 1);
|
||||
const bound = bindQuery(definition, input, scope);
|
||||
expect(bound.queryKey).toEqual([
|
||||
"query", 2, "reference-resource", 1,
|
||||
scope.fingerprint, definition.definitionVersion, bound.identity.token,
|
||||
]);
|
||||
expect(bound.queryKey.slice(0, 4)).toEqual(
|
||||
createQueryInvalidationPrefix(namespace),
|
||||
);
|
||||
```
|
||||
|
||||
Also reject empty/control-character IDs, non-positive versions, and excessive UTF-8 length.
|
||||
|
||||
- [ ] **Step 2: Run RED**
|
||||
|
||||
Run: `corepack pnpm exec vitest run tests/component/application-query.test.tsx tests/unit/query-invalidation-registry.test.ts`
|
||||
|
||||
Expected: missing helper exports and current V1 key order mismatch.
|
||||
|
||||
- [ ] **Step 3: Implement the shared constructors**
|
||||
|
||||
`createBoundQueryKey` must return exactly:
|
||||
|
||||
```ts
|
||||
Object.freeze([
|
||||
"query", QUERY_KEY_SCHEMA_VERSION,
|
||||
namespace.namespaceId, namespace.namespaceVersion,
|
||||
scopeFingerprint, definitionVersion, identityToken,
|
||||
]);
|
||||
```
|
||||
|
||||
`createQueryInvalidationPrefix` returns the first four entries. `bindQuery` constructs the namespace identity from the definition rather than duplicating the tuple.
|
||||
|
||||
- [ ] **Step 4: Run GREEN**
|
||||
|
||||
Run: `corepack pnpm exec vitest run tests/component/application-query.test.tsx tests/unit/query-invalidation-registry.test.ts`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/contracts/query-keys.ts src/contracts/server-state.ts tests/component/application-query.test.tsx tests/unit/query-invalidation-registry.test.ts
|
||||
git commit -m "fix: align bound query keys with invalidation prefixes"
|
||||
```
|
||||
|
||||
### Task 2: Many-to-many invalidation in production composition
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/contracts/query-invalidation.ts`
|
||||
- Modify: `src/features/reference-feature/contracts/reference-feature-contract.ts`
|
||||
- Modify: `src/features/installed-feature-contracts.ts`
|
||||
- Modify: `src/adapters/query-cache/tanstack-cache-coordinator.ts`
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts`
|
||||
- Modify: `tests/unit/query-invalidation-registry.test.ts`
|
||||
- Modify: `tests/unit/tanstack-cache-coordinator.test.ts`
|
||||
- Modify: `tests/features/reference-feature/reference-contract.test.ts`
|
||||
- Modify: `tests/unit/runtime-adapters.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- `InvalidationRegistry.namespaces` and edges use `QueryNamespaceIdentity`.
|
||||
- `indexInvalidationRegistry` returns every namespace identity for each topic.
|
||||
- `createTanStackCacheCoordinator` consumes `InvalidationRegistryIndex`; topic versions remain a separate bounded map used only by cross-context transport.
|
||||
- Installed features export `INVALIDATION_REGISTRY`, composed once at bootstrap.
|
||||
|
||||
- [ ] **Step 1: Add failing real-key invalidation and fan-out tests**
|
||||
|
||||
Seed `QueryClient` with real `bindQuery(...).queryKey` values, map one topic to two namespaces, call local and remote invalidation, and assert both matching queries are invalidated while an unrelated namespace is not. Assert the published event contains only topic/version.
|
||||
|
||||
- [ ] **Step 2: Run RED**
|
||||
|
||||
Run: `corepack pnpm exec vitest run tests/unit/query-invalidation-registry.test.ts tests/unit/tanstack-cache-coordinator.test.ts tests/features/reference-feature/reference-contract.test.ts tests/unit/runtime-adapters.test.ts`
|
||||
|
||||
Expected: the coordinator accepts the legacy flat registry and invalidates prefixes that do not match bound keys.
|
||||
|
||||
- [ ] **Step 3: Compose and index contributions once**
|
||||
|
||||
Feature contribution shape:
|
||||
|
||||
```ts
|
||||
invalidation: Object.freeze({
|
||||
topics: [REFERENCE_RESOURCE_INVALIDATION_TOPIC],
|
||||
namespaces: [defineQueryNamespaceIdentity("reference-resource", 1)],
|
||||
edges: [{
|
||||
topicId: REFERENCE_RESOURCE_INVALIDATION_TOPIC,
|
||||
namespace: defineQueryNamespaceIdentity("reference-resource", 1),
|
||||
}],
|
||||
})
|
||||
```
|
||||
|
||||
`installed-feature-contracts.ts` concatenates these bounded contributions. `runtime-adapters.ts` calls `indexInvalidationRegistry(INVALIDATION_REGISTRY)` exactly once, derives transport topic versions, and passes the index to each generation's coordinator.
|
||||
|
||||
- [ ] **Step 4: Make the coordinator invalidate every indexed prefix**
|
||||
|
||||
For each topic, iterate `namespacesForTopic.get(topic)`, create the V2 prefix with `createQueryInvalidationPrefix`, and call `invalidateQueries({ exact: false, refetchType: "active" })`. Sequence-gap handling visits all indexed topics without duplicating namespace work.
|
||||
|
||||
- [ ] **Step 5: Remove legacy authorities**
|
||||
|
||||
Delete `QUERY_REGISTRY` from `src/contracts/query-keys.ts`, the flat installed `QUERY_REGISTRY`, `InstalledQueryInvalidationDefinition`, and feature-owned concrete TanStack namespace tuples after `rg` shows zero callers.
|
||||
|
||||
- [ ] **Step 6: Run GREEN**
|
||||
|
||||
Run: `corepack pnpm exec vitest run tests/unit/query-invalidation-registry.test.ts tests/unit/tanstack-cache-coordinator.test.ts tests/features/reference-feature/reference-contract.test.ts tests/unit/runtime-adapters.test.ts`
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/contracts/query-invalidation.ts src/features/reference-feature/contracts/reference-feature-contract.ts src/features/installed-feature-contracts.ts src/adapters/query-cache/tanstack-cache-coordinator.ts src/bootstrap/runtime-adapters.ts tests/unit/query-invalidation-registry.test.ts tests/unit/tanstack-cache-coordinator.test.ts tests/features/reference-feature/reference-contract.test.ts tests/unit/runtime-adapters.test.ts
|
||||
git commit -m "fix: index many-to-many query invalidation"
|
||||
```
|
||||
|
||||
### Task 3: Application-owned mutation intent
|
||||
|
||||
**Files:**
|
||||
- Create: `src/contracts/mutation-intent.ts`
|
||||
- Create: `src/application/ports/mutation-intent-factory.ts`
|
||||
- Create: `src/adapters/platform/browser-mutation-intent-factory.ts`
|
||||
- Modify: `src/contracts/server-state.ts`
|
||||
- Modify: `src/presentation/adapters/query/application-query.ts`
|
||||
- Create: `src/presentation/adapters/query/mutation-intent-provider.tsx`
|
||||
- Modify: `src/presentation/adapters/query/server-state-generation-provider.tsx`
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts`
|
||||
- Modify: `src/features/reference-feature/adapters/reference-http-gateway.ts`
|
||||
- Modify: `src/adapters/http/http-effect-certainty.ts`
|
||||
- Modify: `src/adapters/http/http-execution-v3.ts`
|
||||
- Modify: `tests/component/application-query.test.tsx`
|
||||
- Modify: `tests/unit/runtime-adapters.test.ts`
|
||||
- Modify: `tests/unit/http-execution-v3.test.ts`
|
||||
- Modify: `tests/integration/http-execution-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- `MutationIntent` has the exact approved immutable shape.
|
||||
- `MutationIntentFactory.create({ operationId, canonicalInputIdentity, requiresIdempotencyKey })` returns one intent.
|
||||
- `BoundMutation.execute` context adds `intent: MutationIntent`.
|
||||
- `HttpExecutionContext.intent` consumes the application intent without regenerating it.
|
||||
|
||||
- [ ] **Step 1: Add failing lifecycle tests**
|
||||
|
||||
Assert two independent submits receive different intent/key pairs; a `JOIN_IDENTICAL` waiter shares the admitted submit; physical HTTP retry sees the same key; queries have no intent header; diagnostics and URLs contain neither intent ID nor key.
|
||||
|
||||
- [ ] **Step 2: Run RED**
|
||||
|
||||
Run: `corepack pnpm exec vitest run tests/component/application-query.test.tsx tests/unit/runtime-adapters.test.ts tests/unit/http-execution-v3.test.ts tests/integration/http-execution-contract.test.ts`
|
||||
|
||||
Expected: bound mutation context has no intent and bootstrap produces resettable `http-key-N` values.
|
||||
|
||||
- [ ] **Step 3: Define intent validation and browser factory**
|
||||
|
||||
Validate bounded non-empty strings and finite non-negative monotonic timestamps. Use `crypto.randomUUID()` independently for `intentId` and required idempotency key; permit deterministic injected factories in tests.
|
||||
|
||||
- [ ] **Step 4: Create intent after duplicate admission**
|
||||
|
||||
Keep canonical identity calculation before duplicate lookup. Only the execution that wins admission calls the factory. Pass the same frozen intent through `mutation.mutateAsync({ input, intent })` and every bound mutation/feature gateway call.
|
||||
|
||||
- [ ] **Step 5: Remove adapter-local sequence identity**
|
||||
|
||||
Delete `contractExecutionSequence`, `http-intent-N`, `http-key-N`, and the unused HTTP-layer `MutationIntent` factory. Bootstrap passes the supplied intent into `ContractHttpExecutor` unchanged.
|
||||
|
||||
- [ ] **Step 6: Run GREEN**
|
||||
|
||||
Run the command from Step 2 and expect all intent lifecycle assertions to pass.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/contracts/mutation-intent.ts src/application/ports/mutation-intent-factory.ts src/adapters/platform/browser-mutation-intent-factory.ts src/contracts/server-state.ts src/presentation/adapters/query/application-query.ts src/presentation/adapters/query/mutation-intent-provider.tsx src/presentation/adapters/query/server-state-generation-provider.tsx src/bootstrap/runtime-adapters.ts src/features/reference-feature/adapters/reference-http-gateway.ts src/adapters/http/http-effect-certainty.ts src/adapters/http/http-execution-v3.ts tests/component/application-query.test.tsx tests/unit/runtime-adapters.test.ts tests/unit/http-execution-v3.test.ts tests/integration/http-execution-contract.test.ts
|
||||
git commit -m "fix: preserve logical mutation intent"
|
||||
```
|
||||
|
||||
### Task 4: Fail KEYED commands before dispatch
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapters/http/http-execution-v3.ts`
|
||||
- Modify: `tests/unit/http-execution-v3.test.ts`
|
||||
- Modify: `tests/integration/http-execution-contract.test.ts`
|
||||
|
||||
- [ ] Add tests for absent, empty, control-character, and over-budget keys. Spy on `attachCredentials` and `fetch`; both must remain at zero and the result must be `CONTRACT_VIOLATION` with `effect: "NOT_STARTED"`.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/http-execution-v3.test.ts tests/integration/http-execution-contract.test.ts` and confirm RED because KEYED commands currently dispatch without a key.
|
||||
- [ ] Add `MISSING_IDEMPOTENCY_KEY` to the request violation union and validate before credential resolution. Reject a key on `NONE`/query descriptors as the same pre-dispatch contract class.
|
||||
- [ ] Re-run the focused tests and confirm GREEN, including same-key physical retry.
|
||||
- [ ] Commit with `git commit -m "fix: reject invalid keyed mutation intents"`.
|
||||
|
||||
### Task 5: Effect-aware optimistic settlement and reconciliation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/presentation/adapters/query/optimistic-layer-runtime.ts`
|
||||
- Modify: `src/presentation/adapters/query/application-query.ts`
|
||||
- Modify: `src/application/view-models/async-state.ts`
|
||||
- Modify: `src/contracts/errors.ts`
|
||||
- Modify: `tests/unit/optimistic-layer-runtime.test.ts`
|
||||
- Modify: `tests/component/application-query.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- `OptimisticLayerLease` adds `markUncertain()` and `reconcile("APPLIED" | "NOT_APPLIED")`.
|
||||
- Layer status becomes `pending | uncertain | committed`; collapse stops before unresolved uncertain layers.
|
||||
- Controller adds `reconcileUnknownEffect(resolution)` tied to the original intent.
|
||||
- Async state adds `mutation-effect-unknown`.
|
||||
|
||||
- [ ] **Step 1: Add failing certainty matrix tests**
|
||||
|
||||
Cover `NOT_STARTED`, `NOT_APPLIED`, `APPLIED_CONFIRMED`, and `MAYBE_APPLIED`; out-of-order later commits; applied/not-applied reconciliation; scope closure. Assert unknown effect does not call `invalidate` or expose generic retry.
|
||||
|
||||
- [ ] **Step 2: Run RED**
|
||||
|
||||
Run: `corepack pnpm exec vitest run tests/unit/optimistic-layer-runtime.test.ts tests/component/application-query.test.tsx`
|
||||
|
||||
Expected: current catch path rolls every failure back.
|
||||
|
||||
- [ ] **Step 3: Derive settlement before touching optimistic state**
|
||||
|
||||
Use `failure.effect ?? "NOT_STARTED"` only for failures known to be pre-dispatch. The mutation bridge switches explicitly:
|
||||
|
||||
```ts
|
||||
switch (effect) {
|
||||
case "NOT_STARTED":
|
||||
case "NOT_APPLIED": rollback(); break;
|
||||
case "APPLIED_CONFIRMED": commit(); scheduleInvalidation(); break;
|
||||
case "MAYBE_APPLIED": markUncertain(); exposeReconciliation(); break;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Preserve ordered uncertain layers**
|
||||
|
||||
Projection still applies uncertain layers. `collapse` may consume committed layers only until the first pending/uncertain layer. Reconciliation converts uncertain to committed or removes it, then reprojects all later layers.
|
||||
|
||||
- [ ] **Step 5: Run GREEN**
|
||||
|
||||
Run the command from Step 2 and expect all certainty and ordering cases to pass.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/presentation/adapters/query/optimistic-layer-runtime.ts src/presentation/adapters/query/application-query.ts src/application/view-models/async-state.ts src/contracts/errors.ts tests/unit/optimistic-layer-runtime.test.ts tests/component/application-query.test.tsx
|
||||
git commit -m "fix: retain uncertain optimistic mutations"
|
||||
```
|
||||
|
||||
### Task 6: Runtime correctness verification
|
||||
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/query-invalidation-registry.test.ts tests/unit/tanstack-cache-coordinator.test.ts tests/unit/http-execution-v3.test.ts tests/unit/optimistic-layer-runtime.test.ts tests/component/application-query.test.tsx tests/integration/http-execution-contract.test.ts tests/features/reference-feature/reference-contract.test.ts tests/unit/runtime-adapters.test.ts`.
|
||||
- [ ] Run `corepack pnpm check:types`.
|
||||
- [ ] Run `corepack pnpm lint`.
|
||||
- [ ] Run `corepack pnpm test:all`.
|
||||
- [ ] Run `git diff --check`.
|
||||
- [ ] Record any browser-only gate as unverified unless its Playwright command actually ran.
|
||||
@@ -0,0 +1,614 @@
|
||||
# CI/CD Frontend Assurance and Delivery 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:** Make `cicd-platform` the sole owner of frontend workflow orchestration, remote projection of test-assurance plans, deterministic candidate production, supply-chain/provider evidence, immutable publication, and exact-candidate promotion.
|
||||
|
||||
**Architecture:** `cicdctl` compiles typed stages and consumes the digest-pinned test-assurance v3 external contract without interpreting test reports. One source revision produces one verified candidate; artifact-bound testing and every supply-chain stage consume that candidate by SHA-256. `release-control` is the only mutating publication/promotion boundary. The centrally installed required workflow remains a four-step pinned bootstrap and is never copied into a product repository.
|
||||
|
||||
**Tech Stack:** Go, JSON Schema 2020-12, Gitea Actions, Node.js/pnpm runners, canonical SHA-256, Ed25519, content-addressed static publication.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Repository: `/home/donghyeon/workspace/desktop-server-git/cicd-platform`; every command runs from its isolated worktree root.
|
||||
- Start only from the immutable Test Assurance Task 10 commit, its v3 distribution digest, and its v3 schema-bundle digest. No current v2 binary may satisfy this dependency.
|
||||
- Resolve Go 1.26.5 from `toolchains/toolchain-lock.yaml` into `.platform/toolchains/go-1.26.5/bin/go`, verify the distribution SHA-256 before extraction, and set `CICD_GO_BIN` to that absolute path. The host currently has no `go`; PATH fallback is forbidden.
|
||||
- CI may choose runner placement and parallelism but may not change test selection, timeout, retry, artifact requirements, result status, or obligation satisfaction.
|
||||
- Test raw reports remain opaque to CI; only testctl v3 plans, normalized results, evidence, assessments, IDs, digests, attempts, and exit codes are consumed.
|
||||
- The platform path is the only candidate producer in shadow. Legacy product release/promotion commands may only compare bytes and evidence read-only.
|
||||
- The first deterministic build is the candidate. The isolated comparison build is destroyed and cannot be promoted.
|
||||
- Exact new IDs are `ci-test-assurance`, `ci-dependency-vulnerability`, `ci-artifact-signing`, `ci-static-artifact-supply-chain`, and `ci-static-site-publish`.
|
||||
- Existing `ci-sbom` and `ci-provenance` gain static-archive subjects without weakening container subjects.
|
||||
- All new capabilities remain P1/shadow until named Gitea, runner, scanner, signer, and provider evidence supports P2.
|
||||
- Each P1 transition is atomic: canonical ID, descriptor/policy/acceptance/runbook, provider registration, readiness registry row, fixture, immutable evidence, `docs/decisions/readiness/<capability>-P1.yaml`, and `Makefile` capability run land in the same commit. A P0 capability may not have an active provider.
|
||||
- Rollback selects a previous signed platform catalog and immutable subject through platform control; it never restores a copied workflow or a legacy writer.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Define typed stage, artifact, and release-identity contracts
|
||||
|
||||
- [ ] Materialize the locked Go toolchain before writing tests. Run from the isolated CICD worktree (network download requires the normal escalation approval):
|
||||
|
||||
```bash
|
||||
mkdir -p .platform/downloads .platform/toolchains/go-1.26.5
|
||||
curl --fail --location --proto '=https' --tlsv1.3 https://go.dev/dl/go1.26.5.linux-amd64.tar.gz --output .platform/downloads/go1.26.5.linux-amd64.tar.gz
|
||||
printf '%s %s\n' '5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053' '.platform/downloads/go1.26.5.linux-amd64.tar.gz' | sha256sum --check
|
||||
tar -xzf .platform/downloads/go1.26.5.linux-amd64.tar.gz --strip-components=1 -C .platform/toolchains/go-1.26.5
|
||||
export CICD_GO_BIN="$PWD/.platform/toolchains/go-1.26.5/bin/go"
|
||||
"$CICD_GO_BIN" version
|
||||
```
|
||||
|
||||
Expected: exact `go version go1.26.5 linux/amd64`. Stop on download/digest mismatch; never use `/usr/bin/go` or another PATH binary.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/contracts/models.go`
|
||||
- Modify: `packages/contracts/schema.go`
|
||||
- Preserve unchanged: `contracts/schemas/execution-plan.schema.json` and `contracts/schemas/release-manifest.schema.json` v1 contracts
|
||||
- Create: `contracts/schemas/execution-plan-v2.schema.json`
|
||||
- Create: `contracts/schemas/release-manifest-v2.schema.json`
|
||||
- Create: `contracts/schemas/promotion-subject.schema.json`
|
||||
- Modify: `contracts/schemas/delivery-platform.schema.json`
|
||||
- Create: `contracts/schemas/artifact-reference.schema.json`
|
||||
- Create: `contracts/schemas/candidate-bundle.schema.json`
|
||||
- Create: `contracts/schemas/test-assurance-result.schema.json`
|
||||
- Create: `contracts/schemas/supply-chain-evidence.schema.json`
|
||||
- Create: `contracts/schemas/publication-record.schema.json`
|
||||
- Sync generated copies under: `packages/contracts/schemas/`
|
||||
- Modify: `packages/contracts/schema_test.go`
|
||||
- Modify: `packages/canonicalid/id.go`
|
||||
- Modify: `packages/canonicalid/id_test.go`
|
||||
- Create: `contracts/examples/valid/execution-plan-v2.json`
|
||||
- Create: `contracts/examples/valid/release-manifest-v2.json`
|
||||
- Create: `contracts/examples/valid/promotion-subject.json`
|
||||
- Create: `contracts/examples/invalid/13-v2-stage-with-shell-payload.json`
|
||||
- Create: `contracts/examples/invalid/14-release-manifest-with-promotion-state.json`
|
||||
- Modify: `contracts/snapshots/` only by adding a new v2 snapshot set; do not rewrite `contracts/snapshots/v1/`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Create P0 directories: `capabilities/ci-test-assurance/`, `capabilities/ci-dependency-vulnerability/`, `capabilities/ci-artifact-signing/`, `capabilities/ci-static-artifact-supply-chain/`, `capabilities/ci-static-site-publish/`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```go
|
||||
type StageInvocation struct {
|
||||
Kind string // internal | platform-adapter | external-contract
|
||||
Engine string // cicdctl | release-control | test-assurance | provider
|
||||
AdapterID string
|
||||
Operation string
|
||||
Payload json.RawMessage // validated by the adapter/operation-specific schema
|
||||
WorkItemID string
|
||||
Inputs []ArtifactReference
|
||||
Outputs []ArtifactDeclaration
|
||||
}
|
||||
|
||||
type TestAssuranceReference struct {
|
||||
APIVersion string
|
||||
ManifestDigest string
|
||||
PolicyDigest string
|
||||
ExecutionPhase string
|
||||
PlanDigest string
|
||||
EvidenceDigest string
|
||||
AssessmentDigest string
|
||||
InputArtifacts []ArtifactReference
|
||||
}
|
||||
|
||||
type PromotionSubject struct {
|
||||
ReleaseManifestDigest string
|
||||
TargetEnvironment string
|
||||
ApprovalID string
|
||||
ApprovalPolicyDigest string
|
||||
PublicationRecordDigest string
|
||||
ExpectedGitRevision string
|
||||
ExpectedTreeDigest string
|
||||
}
|
||||
```
|
||||
|
||||
`ReleaseManifestV2` is immutable and carries separate source and artifact `TestAssuranceReference` values plus source revision, candidate archive/member-manifest digests, dependency-vulnerability/SBOM/provenance/signature digests, and platform policy/catalog/toolchain digests. Target environment, approval, publication, and Git CAS belong only to `PromotionSubject`; promotion history never rewrites the signed release manifest. The combined signed release identity is `ReleaseManifestV2 + PromotionSubject`.
|
||||
|
||||
Canonical kinds are fixed: `ci-test-assurance` is `KindComposite`; `ci-dependency-vulnerability`, `ci-artifact-signing`, and `ci-static-site-publish` are `KindArtifact`; `ci-static-artifact-supply-chain` is `KindComposite`.
|
||||
|
||||
- [ ] Add RED tests rejecting free-form shell payload, missing invocation on a planned v2 stage, mutable artifact reference, duplicate output ID, test-assurance v2 reference, source/artifact reference aliasing, wrong candidate digest, release manifest missing either assessment, promotion fields inside the immutable manifest, and target/approval missing from `PromotionSubject`.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
"$CICD_GO_BIN" test ./packages/contracts ./packages/canonicalid
|
||||
make GO="$CICD_GO_BIN" contracts
|
||||
```
|
||||
|
||||
Expected: RED because the typed contracts do not exist.
|
||||
|
||||
- [ ] Implement v2 contracts and document-version mappings while retaining v1 dual-read behavior. `StageInvocation.Payload` is a discriminated typed payload validated by `(AdapterID, Operation)`; the adapter passes its internal argv directly through `exec.CommandContext` and never invokes a shell.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
make GO="$CICD_GO_BIN" contracts-sync
|
||||
make GO="$CICD_GO_BIN" contracts
|
||||
"$CICD_GO_BIN" test ./packages/contracts ./packages/canonicalid
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add packages/contracts packages/canonicalid contracts capabilities/ci-test-assurance capabilities/ci-dependency-vulnerability capabilities/ci-artifact-signing capabilities/ci-static-artifact-supply-chain capabilities/ci-static-site-publish
|
||||
git commit -m "feat(contracts): define frontend delivery identities"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Execute typed stages with verified artifact fan-out
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/cicdctl/internal/execution/adapter.go`
|
||||
- Create: `apps/cicdctl/internal/execution/engine.go`
|
||||
- Create: `apps/cicdctl/internal/execution/artifact_store.go`
|
||||
- Create: `apps/cicdctl/internal/execution/local_artifact_store.go`
|
||||
- Create: `apps/cicdctl/internal/execution/engine_test.go`
|
||||
- Create: `apps/cicdctl/internal/execution/artifact_store_test.go`
|
||||
- Modify: `apps/cicdctl/internal/execution/scheduler.go`
|
||||
- Modify: `apps/cicdctl/internal/execution/completeness.go`
|
||||
- Modify: `apps/cicdctl/internal/app/run_command.go`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```go
|
||||
type InvocationAdapter interface {
|
||||
AdapterID() string
|
||||
Execute(context.Context, InvocationRequest) (InvocationOutcome, error)
|
||||
}
|
||||
|
||||
type ArtifactStore interface {
|
||||
Put(context.Context, ArtifactInput) (contracts.ArtifactReference, error)
|
||||
MaterializeVerified(context.Context, contracts.ArtifactReference, string) error
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add RED tests proving source checkout is read-only, independent stages do not share mutable output paths, mutation after `Put` fails, missing adapters are platform defects, failed dependencies block descendants, and a missing terminal result cannot pass.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
"$CICD_GO_BIN" test ./apps/cicdctl/internal/execution ./apps/cicdctl/internal/app -count=1
|
||||
```
|
||||
|
||||
Expected: RED because no invocation engine/artifact store exists.
|
||||
|
||||
- [ ] Implement bounded scheduler waves over existing state-machine rules. Every downstream artifact is materialized to a private directory and rehashed before adapter invocation.
|
||||
- [ ] Add `cicdctl run execute --plan --checkout --results --artifact-root` with atomic result writes.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/execution ./apps/cicdctl/internal/app -count=1`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/cicdctl/internal/execution apps/cicdctl/internal/app
|
||||
git commit -m "feat(cicdctl): execute typed artifact-bound stages"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Integrate the digest-pinned test-assurance v3 contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/cicdctl/internal/adapters/testassurance/contract.go`
|
||||
- Create: `apps/cicdctl/internal/adapters/testassurance/runner.go`
|
||||
- Create: `apps/cicdctl/internal/adapters/testassurance/projector.go`
|
||||
- Create: `apps/cicdctl/internal/adapters/testassurance/contract_test.go`
|
||||
- Create: `apps/cicdctl/internal/adapters/testassurance/runner_test.go`
|
||||
- Create: `apps/cicdctl/internal/adapters/testassurance/projector_test.go`
|
||||
- Modify: `apps/cicdctl/internal/planner/plan.go`
|
||||
- Modify: `apps/cicdctl/internal/capabilities/capabilities.go`
|
||||
- Modify: `apps/cicdctl/internal/app/run_command.go`
|
||||
- Create imported immutable schema bundle: `toolchains/external-contracts/test-assurance-v3/`
|
||||
- Create: `toolchains/external-contracts/test-assurance-v3/contract-lock.json`
|
||||
- Create: `contracts/schemas/external-contract-lock.schema.json`
|
||||
- Create: `packages/contracts/schemas/external-contract-lock.schema.json`
|
||||
- Modify: `toolchains/platform-release-catalog.yaml`
|
||||
- Modify: `contracts/schemas/platform-release-catalog.schema.json`
|
||||
- Modify: `packages/contracts/schemas/platform-release-catalog.schema.json`
|
||||
- Modify: `packages/contracts/schema.go`
|
||||
- Modify: `packages/contracts/schema_test.go`
|
||||
- Modify: `contracts/examples/valid/platform-release-catalog.json`
|
||||
- Create/complete: `capabilities/ci-test-assurance/descriptor.yaml`, `capabilities/ci-test-assurance/policy.yaml`, `capabilities/ci-test-assurance/acceptance.yaml`, `capabilities/ci-test-assurance/runbook-index.yaml`
|
||||
- Create: `docs/decisions/readiness/ci-test-assurance-P1.yaml`
|
||||
- Create evidence under: `docs/decisions/readiness/evidence/ci-test-assurance/`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**External calls:**
|
||||
|
||||
```text
|
||||
testctl validate
|
||||
testctl lock
|
||||
testctl compile
|
||||
testctl select
|
||||
testctl plan
|
||||
testctl execute-one
|
||||
testctl normalize
|
||||
testctl bundle
|
||||
testctl assess
|
||||
```
|
||||
|
||||
All calls use argv arrays and a distribution/schema digest from the signed platform catalog. `contract-lock.json` binds test-assurance source revision, v3 schema major, distribution digest, and schema-bundle digest. The catalog schema adds a typed `test-assurance` engine reference; without that engine, `ci-test-assurance` compilation fails.
|
||||
|
||||
`projector.go` preserves the external plan bytes/digest unchanged and creates a separate CI projection whose nodes reference only `{externalPlanDigest, workItemId}` plus runner placement and CI dependency edges. It never writes a modified testctl plan.
|
||||
|
||||
The source request is planned before the build with no input artifacts. After `ci-frontend` freezes the first candidate, CI creates an artifact request containing its exact `{artifactId, mediaType, sha256}`. Testctl, not CI, matches that request to repository `ArtifactSuiteTemplate` declarations and materializes executable v3 suites/work items. CI rejects any unresolved template, placeholder digest, or work item whose input tuple differs from the candidate reference.
|
||||
|
||||
- [ ] Add RED tests for source-revision/schema/distribution digest mismatch, absent catalog engine, non-v3 output, altered external plan bytes, altered timeout/retry, missing work-item result, source/artifact plan mixing, opaque evidence preservation, and exact testctl exit-code mapping from the published external contract.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
"$CICD_GO_BIN" test ./apps/cicdctl/internal/adapters/testassurance ./apps/cicdctl/internal/planner ./apps/cicdctl/internal/capabilities -count=1
|
||||
```
|
||||
|
||||
Expected: RED.
|
||||
|
||||
- [ ] Implement the adapter without importing JUnit, Playwright, coverage, HTTP, accessibility, or visual parsing code. Invoke v3 artifact work as `testctl execute-one --plan <plan> --work-item-id <id> --artifact-map <map> --output <dir>`.
|
||||
- [ ] Raise `ci-test-assurance` to P1/shadow only after a local v3 source→artifact conformance fixture completes.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/adapters/testassurance ./apps/cicdctl/internal/planner ./apps/cicdctl/internal/capabilities -count=1` and `make GO="$CICD_GO_BIN" registry`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/cicdctl toolchains/external-contracts/test-assurance-v3 toolchains/platform-release-catalog.yaml contracts/schemas/platform-release-catalog.schema.json contracts/examples/valid/platform-release-catalog.json packages/contracts capabilities/ci-test-assurance contracts/readiness-cards.yaml docs/decisions/readiness/ci-test-assurance-P1.yaml docs/decisions/readiness/evidence/ci-test-assurance Makefile
|
||||
git commit -m "feat(test-assurance): project external v3 test plans"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Split Node ownership and freeze the first deterministic candidate
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/cicdctl/internal/manifest/model.go`
|
||||
- Modify: `apps/cicdctl/internal/manifest/compiler.go`
|
||||
- Modify: `apps/cicdctl/internal/manifest/compiler_test.go`
|
||||
- Modify: `apps/cicdctl/internal/templates/nodetypescript/resolver.go`
|
||||
- Modify: `apps/cicdctl/internal/templates/nodetypescript/resolver_test.go`
|
||||
- Modify: `apps/cicdctl/internal/templates/frontend/resolver.go`
|
||||
- Modify: `apps/cicdctl/internal/templates/frontend/resolver_test.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/candidate/builder.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/candidate/archive.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/candidate/builder_test.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/candidate/archive_test.go`
|
||||
- Modify: `capabilities/ci-node-typescript/*`
|
||||
- Modify: `capabilities/ci-frontend/*`
|
||||
- Create: `docs/decisions/readiness/ci-frontend-P1.yaml`
|
||||
- Create: `docs/decisions/readiness/evidence/ci-frontend-P1.bundle.json`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**Manifest config:**
|
||||
|
||||
```go
|
||||
type NodeTypeScriptConfig struct {
|
||||
LintScript string
|
||||
TypecheckScript string
|
||||
TestOwner string // empty or ci-test-assurance
|
||||
}
|
||||
|
||||
type FrontendConfig struct {
|
||||
BuildScript string
|
||||
OutputDirectory string
|
||||
SizeBudgetBytes int64
|
||||
ForbiddenEnvironment []string
|
||||
}
|
||||
```
|
||||
|
||||
Package manager/version/install mode are repository facts derived from `package.json.packageManager` and `pnpm-lock.yaml`; capability config cannot override them. The execution-plan compiler permits exactly one `candidate-producer` operation for a selected release output. A second producer, a legacy product candidate command, or a writer without platform writer identity/operation ID/idempotency key is a contract error.
|
||||
|
||||
- [ ] Add RED tests proving capability config is decoded, pnpm is derived and uses frozen install, manifest package-manager override is rejected, lint/typecheck remain in CI, unit/coverage stages disappear when test owner is `ci-test-assurance`, source revision is built twice in isolated workspaces, only the first byte-identical candidate is retained, and a plan with zero/two candidate producers is rejected.
|
||||
- [ ] Add archive adversarial tests for empty output, traversal, symlink, host path, undeclared member, duplicate path, environment leak, size overflow, and changed tree digest.
|
||||
- [ ] Run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/manifest ./apps/cicdctl/internal/templates/nodetypescript ./apps/cicdctl/internal/templates/frontend ./apps/cicdctl/internal/artifacts/candidate -count=1`; expected RED.
|
||||
- [ ] Implement strict config decoding and canonical archive/member manifest generation. Destroy the verification workspace before returning the candidate reference.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/manifest ./apps/cicdctl/internal/templates/nodetypescript ./apps/cicdctl/internal/templates/frontend ./apps/cicdctl/internal/artifacts/candidate -count=1` and `make GO="$CICD_GO_BIN" registry`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/cicdctl/internal/manifest apps/cicdctl/internal/templates apps/cicdctl/internal/artifacts/candidate capabilities/ci-node-typescript capabilities/ci-frontend contracts/readiness-cards.yaml docs/decisions/readiness/ci-frontend-P1.yaml docs/decisions/readiness/evidence/ci-frontend-P1.bundle.json Makefile
|
||||
git commit -m "feat(frontend): freeze one deterministic static candidate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Bind dependency-vulnerability evidence to source and candidate
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/cicdctl/internal/artifacts/vulnerability/contract.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/vulnerability/adapter.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/vulnerability/validator.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/vulnerability/adapter_test.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/vulnerability/validator_test.go`
|
||||
- Modify: `apps/cicdctl/internal/capabilities/capabilities.go`
|
||||
- Create/complete: `capabilities/ci-dependency-vulnerability/descriptor.yaml`, `capabilities/ci-dependency-vulnerability/policy.yaml`, `capabilities/ci-dependency-vulnerability/acceptance.yaml`, `capabilities/ci-dependency-vulnerability/runbook-index.yaml`
|
||||
- Create: `docs/decisions/readiness/ci-dependency-vulnerability-P1.yaml`
|
||||
- Create evidence under: `docs/decisions/readiness/evidence/ci-dependency-vulnerability/`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**Evidence identity:** source revision, lockfile digest, candidate subject digest, provider ID, scanner/tool digest, vulnerability DB snapshot digest/time, invocation digest, normalized finding set, and evidence signature.
|
||||
|
||||
The adapter executes a digest-pinned provider engine through a typed contract and validates its output. It contains no scanner HTTP client and receives no provider credential; network/credential handling stays inside the provider trust boundary.
|
||||
|
||||
- [ ] Add RED cases for absent report, wrong lockfile/candidate, stale DB, provider crash, malformed report, invalid signature, and zero findings without valid invocation metadata.
|
||||
- [ ] Run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/artifacts/vulnerability -count=1`; expected RED.
|
||||
- [ ] Implement a typed provider profile selected by manifest ID and direct process execution of the pinned engine; arbitrary repository commands and in-process provider clients are forbidden.
|
||||
- [ ] Raise to P1/shadow with signed local fixtures; keep P2 blocked on a named real scanner/provider.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/artifacts/vulnerability ./apps/cicdctl/internal/capabilities -count=1` and `make GO="$CICD_GO_BIN" registry`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/cicdctl/internal/artifacts/vulnerability apps/cicdctl/internal/capabilities capabilities/ci-dependency-vulnerability contracts/readiness-cards.yaml docs/decisions/readiness/ci-dependency-vulnerability-P1.yaml docs/decisions/readiness/evidence/ci-dependency-vulnerability Makefile
|
||||
git commit -m "feat(security): bind dependency findings to candidates"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Generalize SBOM and provenance to static archives
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/cicdctl/internal/artifacts/subject/subject.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/subject/subject_test.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/sbom/contract.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/sbom/validator.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/sbom/sbom_test.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/provenance/contract.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/provenance/validator.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/provenance/provenance_test.go`
|
||||
- Modify: `capabilities/ci-sbom/*`
|
||||
- Modify: `capabilities/ci-provenance/*`
|
||||
- Modify: `apps/cicdctl/internal/capabilities/capabilities.go`
|
||||
- Create: `docs/decisions/readiness/ci-sbom-P1.yaml`
|
||||
- Create: `docs/decisions/readiness/ci-provenance-P1.yaml`
|
||||
- Create: `docs/decisions/readiness/evidence/ci-sbom-P1.bundle.json`
|
||||
- Create: `docs/decisions/readiness/evidence/ci-provenance-P1.bundle.json`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**Interface:** `ImmutableSubject` is a tagged union of container image or static archive. Both evidence types bind subject kind/digest; static provenance additionally binds source revision, build invocation, member-manifest digest, platform/toolchain digests, and determinism evidence. Digest-pinned external SBOM/provenance engines generate documents; cicdctl validates and binds returned evidence but implements no provider network client.
|
||||
|
||||
- [ ] Add RED tests for empty/incomplete SBOM, duplicate package identity, wrong subject kind/digest, missing build invocation, changed source revision, changed member manifest, and fabricated provenance.
|
||||
- [ ] Run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/artifacts/subject ./apps/cicdctl/internal/artifacts/sbom ./apps/cicdctl/internal/artifacts/provenance -count=1`; expected RED.
|
||||
- [ ] Implement output-kind adapters while preserving every existing container test unchanged.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/artifacts/subject ./apps/cicdctl/internal/artifacts/sbom ./apps/cicdctl/internal/artifacts/provenance ./apps/cicdctl/internal/capabilities -count=1`, `make GO="$CICD_GO_BIN" contracts`, and `make GO="$CICD_GO_BIN" registry`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/cicdctl/internal/artifacts/subject apps/cicdctl/internal/artifacts/sbom apps/cicdctl/internal/artifacts/provenance apps/cicdctl/internal/capabilities capabilities/ci-sbom capabilities/ci-provenance contracts/readiness-cards.yaml docs/decisions/readiness/ci-sbom-P1.yaml docs/decisions/readiness/ci-provenance-P1.yaml docs/decisions/readiness/evidence/ci-sbom-P1.bundle.json docs/decisions/readiness/evidence/ci-provenance-P1.bundle.json Makefile
|
||||
git commit -m "feat(supply-chain): support static archive subjects"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Sign the artifact and aggregate reference-only supply-chain evidence
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/cicdctl/internal/artifacts/signing/request.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/signing/verifier.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/signing/signing_test.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/supplychain/aggregate.go`
|
||||
- Create: `apps/cicdctl/internal/artifacts/supplychain/aggregate_test.go`
|
||||
- Modify: `apps/cicdctl/internal/capabilities/capabilities.go`
|
||||
- Create/complete: `capabilities/ci-artifact-signing/descriptor.yaml`, `capabilities/ci-artifact-signing/policy.yaml`, `capabilities/ci-artifact-signing/acceptance.yaml`, `capabilities/ci-artifact-signing/runbook-index.yaml`
|
||||
- Create/complete: `capabilities/ci-static-artifact-supply-chain/descriptor.yaml`, `capabilities/ci-static-artifact-supply-chain/policy.yaml`, `capabilities/ci-static-artifact-supply-chain/acceptance.yaml`, `capabilities/ci-static-artifact-supply-chain/runbook-index.yaml`
|
||||
- Create: `docs/decisions/readiness/ci-artifact-signing-P1.yaml`
|
||||
- Create: `docs/decisions/readiness/ci-static-artifact-supply-chain-P1.yaml`
|
||||
- Create evidence under: `docs/decisions/readiness/evidence/ci-artifact-signing/` and `docs/decisions/readiness/evidence/ci-static-artifact-supply-chain/`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```go
|
||||
type SigningRequest struct { SubjectDigest, KeyID, OperationID string }
|
||||
|
||||
func VerifyStaticSupplyChain(
|
||||
candidate contracts.CandidateBundle,
|
||||
dependencyVulnerability, sbom, provenance, signature contracts.EvidenceRef,
|
||||
) (contracts.SupplyChainEvidenceSet, error)
|
||||
```
|
||||
|
||||
- [ ] Add RED tests proving the signer receives only identity data, not source/candidate bytes; reject wrong subject, expired key, missing/duplicate evidence kind, invalid signature, altered candidate, and copied/rewritten child evidence.
|
||||
- [ ] Generate ephemeral Ed25519 test keys only.
|
||||
- [ ] Run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/artifacts/signing ./apps/cicdctl/internal/artifacts/supplychain -count=1`; expected RED.
|
||||
- [ ] Implement signing on the `isolated-signer` trust partition and a composite containing immutable child references only.
|
||||
- [ ] Raise both to P1/shadow; keep P2 blocked on real signer evidence.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./apps/cicdctl/internal/artifacts/signing ./apps/cicdctl/internal/artifacts/supplychain ./apps/cicdctl/internal/capabilities -count=1` and `make GO="$CICD_GO_BIN" registry`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/cicdctl/internal/artifacts/signing apps/cicdctl/internal/artifacts/supplychain apps/cicdctl/internal/capabilities capabilities/ci-artifact-signing capabilities/ci-static-artifact-supply-chain contracts/readiness-cards.yaml docs/decisions/readiness/ci-artifact-signing-P1.yaml docs/decisions/readiness/ci-static-artifact-supply-chain-P1.yaml docs/decisions/readiness/evidence/ci-artifact-signing docs/decisions/readiness/evidence/ci-static-artifact-supply-chain Makefile
|
||||
git commit -m "feat(supply-chain): sign static candidates and aggregate evidence"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Publish static candidates without rebuilding
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/release-control/internal/publish/publisher.go`
|
||||
- Create: `apps/release-control/internal/publish/local.go`
|
||||
- Create: `apps/release-control/internal/publish/service.go`
|
||||
- Create: `apps/release-control/internal/publish/publisher_test.go`
|
||||
- Create: `apps/release-control/internal/publish/local_test.go`
|
||||
- Modify: `apps/release-control/internal/app/app.go`
|
||||
- Create/complete: `capabilities/ci-static-site-publish/descriptor.yaml`, `capabilities/ci-static-site-publish/policy.yaml`, `capabilities/ci-static-site-publish/acceptance.yaml`, `capabilities/ci-static-site-publish/runbook-index.yaml`
|
||||
- Create: `docs/decisions/readiness/ci-static-site-publish-P1.yaml`
|
||||
- Create evidence under: `docs/decisions/readiness/evidence/ci-static-site-publish/`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**Interface:**
|
||||
|
||||
```go
|
||||
type PublishRequest struct {
|
||||
OperationID, SubjectDigest, CandidateDigest, ReleaseManifestDigest string
|
||||
}
|
||||
type PublicationRecord struct {
|
||||
OperationID, ProviderID, ImmutableURI, SubjectDigest, ServedContentDigest, State string
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add RED tests for idempotent put, same digest already present, different digest collision, response loss after mutation, failed reconciliation, served-content mismatch, mutable URI, and any attempted build/repackage operation.
|
||||
- [ ] Run `"$CICD_GO_BIN" test ./apps/release-control/internal/publish ./apps/release-control/internal/app -count=1`; expected RED.
|
||||
- [ ] Implement a content-addressed local P1 publisher. Response loss returns `INDETERMINATE`; retry requires reconciliation by operation ID.
|
||||
- [ ] Require provider subject and served-content digests both equal the approved candidate.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./apps/release-control/internal/publish ./apps/release-control/internal/app -count=1` and `make GO="$CICD_GO_BIN" registry`; expected PASS.
|
||||
- [ ] Raise to P1/shadow; keep real provider P2 blocked.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/release-control/internal/publish apps/release-control/internal/app capabilities/ci-static-site-publish contracts/readiness-cards.yaml docs/decisions/readiness/ci-static-site-publish-P1.yaml docs/decisions/readiness/evidence/ci-static-site-publish Makefile
|
||||
git commit -m "feat(release-control): publish immutable static candidates"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Promote the exact signed static candidate
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/release-control/internal/candidate/candidate.go`
|
||||
- Modify: `apps/release-control/internal/candidate/candidate_test.go`
|
||||
- Modify: `apps/release-control/internal/approval/approval.go`
|
||||
- Modify: `apps/release-control/internal/approval/approval_test.go`
|
||||
- Modify: `apps/release-control/internal/gitops/cas.go`
|
||||
- Modify: `apps/release-control/internal/gitops/cas_test.go`
|
||||
- Create: `apps/release-control/internal/promotion/service.go`
|
||||
- Create: `apps/release-control/internal/promotion/service_test.go`
|
||||
- Modify: `apps/release-control/internal/app/app.go`
|
||||
- Modify: `capabilities/delivery-release-control/*`
|
||||
- Create: `docs/decisions/readiness/delivery-release-control-P1.yaml`
|
||||
- Create: `docs/decisions/readiness/evidence/delivery-release-control-P1.bundle.json`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
|
||||
**Promotion request:** signed release manifest, confirmed publication, source and artifact test-assurance references, supply-chain evidence index, approval subject/expiry, expected Git revision/tree digest, and operation ID. No source path, build command, or mutable provider URL is accepted.
|
||||
|
||||
```go
|
||||
type PromotionRequest struct {
|
||||
OperationID string
|
||||
ReleaseManifest contracts.ReleaseManifestV2
|
||||
Subject contracts.PromotionSubject
|
||||
Publication contracts.PublicationRecord
|
||||
EvidenceIndexDigest string
|
||||
}
|
||||
```
|
||||
|
||||
The compiler selects exactly one environment adapter by output profile: static archives require the static-site desired-state adapter, while container images require the existing Kubernetes/GitOps adapter. Zero or multiple environment adapters is invalid.
|
||||
|
||||
- [ ] Add RED tests rejecting failed/missing source or artifact assessment, changed candidate, unconfirmed publication, served-content mismatch, unsigned evidence, stale approval, stale Git base, direct mutable URL, rebuild/repackage request, and zero/multiple/wrong-kind environment adapters.
|
||||
- [ ] Preserve and run existing real local Git CAS/response-loss tests.
|
||||
- [ ] Implement static desired-state promotion without an unconditional Kubernetes dependency; container releases keep their environment adapter.
|
||||
- [ ] Raise `delivery-release-control` only to P1/shadow using local Git and local content-addressed publication.
|
||||
- [ ] Run `"$CICD_GO_BIN" test ./apps/release-control/internal/candidate ./apps/release-control/internal/approval ./apps/release-control/internal/gitops ./apps/release-control/internal/publish ./apps/release-control/internal/promotion ./apps/release-control/internal/app -count=1`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/release-control capabilities/delivery-release-control contracts/readiness-cards.yaml docs/decisions/readiness/delivery-release-control-P1.yaml docs/decisions/readiness/evidence/delivery-release-control-P1.bundle.json Makefile
|
||||
git commit -m "feat(release-control): promote verified static subjects"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Keep the centrally installed required workflow thin
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/cicdctl/cmd/sourcectl/main.go`
|
||||
- Create: `apps/cicdctl/cmd/platform-bootstrap/main.go`
|
||||
- Create: `apps/cicdctl/internal/bootstrap/catalog.go`
|
||||
- Create: `apps/cicdctl/internal/bootstrap/pipeline.go`
|
||||
- Create: `apps/cicdctl/internal/bootstrap/status.go`
|
||||
- Create tests under: `apps/cicdctl/internal/bootstrap/`
|
||||
- Modify: `.gitea/workflows/required-delivery-guard.yaml`
|
||||
- Modify: `tools/contractctl/internal/workflow/required_status.go`
|
||||
- Modify: `tools/contractctl/internal/workflow/required_status_test.go`
|
||||
- Modify: `Makefile`
|
||||
- Create: `images/platform-bootstrap/Dockerfile`
|
||||
- Create: `images/platform-bootstrap/entrypoint.sh`
|
||||
- Create: `images/platform-bootstrap/README.md`
|
||||
- Modify: `toolchains/platform-release-catalog.yaml`
|
||||
|
||||
**Workflow sequence:** exact source checkout → signed catalog verification → pinned bootstrap execution → one terminal sentinel publication. All language/test/build/provider/promotion stages are compiled inside the platform plan, not written in YAML. The only required status name is exactly `platform/delivery-pipeline`.
|
||||
|
||||
- [ ] Add RED tests for exactly one stable required status, no language/build logic, no floating action references, digest-pinned binaries, a sentinel on every exit path, and absence of any rule requiring a product-repository workflow copy.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
"$CICD_GO_BIN" test ./tools/contractctl/internal/workflow ./apps/cicdctl/internal/bootstrap ./apps/cicdctl/internal/app -count=1
|
||||
```
|
||||
|
||||
Expected: RED until bootstrap binaries and status finalization exist.
|
||||
|
||||
- [ ] Implement the four-step bootstrap and atomic sentinel finalization.
|
||||
- [ ] Re-run `"$CICD_GO_BIN" test ./tools/contractctl/internal/workflow ./apps/cicdctl/internal/bootstrap ./apps/cicdctl/internal/app -count=1`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add apps/cicdctl/cmd apps/cicdctl/internal/bootstrap .gitea/workflows/required-delivery-guard.yaml tools/contractctl/internal/workflow images/platform-bootstrap Makefile toolchains/platform-release-catalog.yaml
|
||||
git commit -m "feat(workflow): run the pinned delivery platform"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: Prove the complete frontend vertical in shadow
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `fixtures/frontend-delivery-vertical/` with a minimal pnpm frontend, both consumer manifests, v3 source/artifact testctl fixtures, deterministic build, adversarial reports, local signer, local static publisher, and local Git desired state
|
||||
- Create: `apps/cicdctl/internal/reports/shadow_parity.go`
|
||||
- Create: `apps/cicdctl/internal/reports/shadow_parity_test.go`
|
||||
- Create: `contracts/schemas/shadow-parity.schema.json`
|
||||
- Sync: `packages/contracts/schemas/shadow-parity.schema.json`
|
||||
- Modify: `packages/contracts/schema.go`
|
||||
- Modify: `packages/contracts/schema_test.go`
|
||||
- Create: `contracts/snapshots/v2/shadow-parity.schema.json`
|
||||
- Create: `contracts/examples/valid/shadow-parity.json`
|
||||
- Create: `docs/migration/frontend-template.md`
|
||||
- Create P1 evidence under: `docs/decisions/readiness/evidence/`
|
||||
- Modify: `docs/decisions/blocked-tasks.md`
|
||||
- Modify: `contracts/readiness-cards.yaml`
|
||||
- Modify: `Makefile`
|
||||
- Modify fixture registry files consumed by: `tools/fixturectl/`
|
||||
- Modify: `README.md`
|
||||
|
||||
**End-to-end order:** source test plan/assessment → one deterministic candidate → artifact test plan/assessment → vulnerability/SBOM/provenance/signature → static supply-chain composite → content-addressed publication → Git CAS promotion → evidence-index sentinel.
|
||||
|
||||
- [ ] Add RED vertical tests plus faults for missing work item, changed candidate, wrong-subject provider result, missing signature, response loss, concurrent Git writer, expired approval, and missing sentinel.
|
||||
- [ ] Add parity comparison over source revision; selected suites/counts/outcomes; coverage universe; HTTP scenario IDs; three browser outcomes; candidate/member digests; provider/supply-chain digests; and promotion readiness. Exclude timestamps, durations, temp paths, and runner IDs.
|
||||
- [ ] Verify one-writer behavior: the platform fixture produces the only candidate; legacy probes receive read-only references and cannot publish/promote.
|
||||
- [ ] Run before evidence updates:
|
||||
|
||||
```bash
|
||||
make GO="$CICD_GO_BIN" capabilities
|
||||
```
|
||||
|
||||
Expected: RED because the P1 chain lacks complete evidence.
|
||||
|
||||
- [ ] Add only locally observed P1/shadow evidence. Keep P2 blocked with named missing Gitea/runner/scanner/signer/provider prerequisites.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
make GO="$CICD_GO_BIN" contracts
|
||||
make GO="$CICD_GO_BIN" registry
|
||||
make GO="$CICD_GO_BIN" capabilities
|
||||
make GO="$CICD_GO_BIN" boundary
|
||||
make GO="$CICD_GO_BIN" verify
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: PASS at P1/shadow; no P2/active claim.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add fixtures/frontend-delivery-vertical apps/cicdctl/internal/reports contracts packages/contracts Makefile docs README.md
|
||||
git commit -m "test(vertical): prove frontend delivery in shadow"
|
||||
```
|
||||
|
||||
## Handoff to the frontend consumer
|
||||
|
||||
The consumer migration may begin from the immutable Task 11 platform release. The product manifest pins its signed catalog version and selects all required capabilities. Central workflow/status installation remains an environment/platform operation. Product files never copy the workflow, provider orchestration, test normalizers, or promotion engine.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Counter-bearing Coverage Provenance 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:** Align repository coverage provenance names and static classification with the counters that Vitest/V8 actually emits, without making claims about JavaScript runtime executability.
|
||||
|
||||
**Architecture:** The inventory parser classifies source files only by whether their top-level AST contains statements known to receive V8 counters. Coverage evaluation requires exact agreement between that static counter-bearing/counterless partition and producer rows, while policy-sensitive modules must remain counter-bearing. The JSON artifact exposes the same terminology as the inventory and diagnostics.
|
||||
|
||||
**Tech Stack:** TypeScript 7, Node.js 24, `@babel/eslint-parser`, Vitest 4, V8 coverage.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Runtime declarations/initializers and direct execution statements are counter-bearing.
|
||||
- Type-only modules, `import type {}`, `import {}`, bare side-effect imports, value imports, named value re-exports, and star value re-exports are counterless under the observed Vitest/V8 producer.
|
||||
- Counterless does not mean non-executable; code, artifacts, diagnostics, tests, and documentation must not make that claim.
|
||||
- Exact all-zero rows are accepted only for statically counterless modules.
|
||||
- Critical and high-risk policy modules cannot be counterless.
|
||||
- All source edits use `apply_patch` and behavior changes follow RED-GREEN TDD.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock the Vitest/V8 classifier contract with RED tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/unit/risk-coverage.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `buildProductionModuleInventory()` and `evaluateRiskCoverage()`.
|
||||
- Produces: expectations for `counterBearingModules`, `counterlessModules`, `counterBearingTotal`, `instrumentedCounterBearingTotal`, `counterlessTotal`, and `counterlessModules`.
|
||||
|
||||
- [x] **Step 1: Rename the test inventory helper and artifact assertions to the desired API.**
|
||||
|
||||
```ts
|
||||
function inventory(
|
||||
files: readonly string[],
|
||||
generatedExclusions: readonly string[] = [],
|
||||
counterlessModules: readonly string[] = [],
|
||||
): ProductionModuleInventory {
|
||||
return {
|
||||
files,
|
||||
preExclusionTotal: files.length + generatedExclusions.length,
|
||||
generatedExclusions,
|
||||
counterBearingModules: files.filter((file) => !counterlessModules.includes(file)),
|
||||
counterlessModules,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: Add a real-source inventory regression table.**
|
||||
|
||||
```ts
|
||||
const counterlessSources = {
|
||||
"import-type-empty.ts": "import type {} from './a.ts';\n",
|
||||
"import-value-empty.ts": "import {} from './a.ts';\n",
|
||||
"import-side-effect.ts": "import './a.ts';\n",
|
||||
"import-value.ts": "import { a } from './a.ts';\n",
|
||||
"reexport-named.ts": "export { a } from './a.ts';\n",
|
||||
"reexport-star.ts": "export * from './a.ts';\n",
|
||||
};
|
||||
```
|
||||
|
||||
Assert every key appears in `counterlessModules`, while `export const runtimeValue = 1` and `void globalThis` appear in `counterBearingModules`.
|
||||
|
||||
- [x] **Step 3: Run the focused test and verify RED.**
|
||||
|
||||
Run: `./node_modules/.bin/vitest run tests/unit/risk-coverage.test.ts --reporter=dot`
|
||||
|
||||
Expected: TypeScript/test failures because the counter-bearing API fields do not exist and the current bare import classifier is executable-labelled.
|
||||
|
||||
### Task 2: Rename and align static coverage provenance
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/risk-coverage.ts`
|
||||
- Modify: `tests/unit/risk-coverage.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Babel `Program.body` nodes and parsed Istanbul/V8 counters.
|
||||
- Produces: `hasCoverageCounterBearingStatements(source, relativePath)`, a complete `counterBearingModules`/`counterlessModules` partition, and consistently named `RiskCoverageResult` fields.
|
||||
|
||||
- [x] **Step 1: Implement the minimal classifier needed by the RED cases.**
|
||||
|
||||
`ImportDeclaration`, `ExportAllDeclaration`, and export declarations without a local declaration return `false`; `importKind === "type"` therefore remains counterless even with an empty specifier list. Runtime declarations/initializers and direct statements return `true`.
|
||||
|
||||
- [x] **Step 2: Rename inventory, evaluator sets, totals, diagnostics, and policy guards.**
|
||||
|
||||
Use these exact artifact fields: `counterBearingTotal`, `instrumentedCounterBearingTotal`, `counterlessTotal`, `counterlessModules`. Use diagnostics containing `counter-bearing`, `counterless`, and `policy-sensitive module cannot be counterless`; remove executable/non-executable terminology from the risk-coverage implementation and tests.
|
||||
|
||||
- [x] **Step 3: Run focused GREEN verification.**
|
||||
|
||||
Run: `./node_modules/.bin/vitest run tests/unit/risk-coverage.test.ts tests/unit/risk-coverage-files.test.ts tests/unit/bounded-body-reader.test.ts --reporter=dot`
|
||||
|
||||
Expected: all focused tests pass and both static partition directions remain fail-closed.
|
||||
|
||||
### Task 3: Refresh documentation, repository evidence, and removal evidence
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/testing/frontend-platform-testing-strategy.md`
|
||||
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/task-1-report.md`
|
||||
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/progress.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: root and sample-removal checker output after Task 2.
|
||||
- Produces: documented V8 counter-bearing semantics and current 285/285 plus 268/268 evidence.
|
||||
|
||||
- [x] **Step 1: Document that counterless imports/re-exports may execute at runtime but receive no file counters in the observed producer.**
|
||||
|
||||
- [x] **Step 2: Run relevant verification.**
|
||||
|
||||
```sh
|
||||
./node_modules/.bin/tsc --noEmit -p tsconfig.node.json
|
||||
./node_modules/.bin/tsc --noEmit -p tsconfig.test.json
|
||||
./node_modules/.bin/eslint scripts/lib/risk-coverage.ts tests/unit/risk-coverage.test.ts --max-warnings=0
|
||||
node scripts/check-risk-coverage.ts
|
||||
corepack pnpm test:sample-removal
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected root checker: `Risk coverage: PASS (285/285 production modules, 80 thresholds)`.
|
||||
|
||||
Expected removal checker: `Risk coverage: PASS (268/268 production modules, 76 thresholds)`; the already-known dependency-cruiser architecture diagnostic may remain the sole removal failure.
|
||||
|
||||
- [x] **Step 3: Commit the independently verified follow-up.**
|
||||
|
||||
```sh
|
||||
git add docs/superpowers/plans/2026-08-02-counter-bearing-coverage-provenance.md docs/testing/frontend-platform-testing-strategy.md scripts/lib/risk-coverage.ts tests/unit/risk-coverage.test.ts
|
||||
git commit -m "refactor: align coverage counter provenance"
|
||||
```
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: terminology, import/re-export edge cases, policy diagnostics, artifact fields, root/removal evidence, report, and ledger are each assigned above.
|
||||
- Placeholder scan: no deferred implementation or unspecified test step remains.
|
||||
- Type consistency: inventory and result names use `counterBearing*`/`counterless*` throughout; the classifier is `hasCoverageCounterBearingStatements`.
|
||||
@@ -0,0 +1,380 @@
|
||||
# Frontend Thin Platform Consumer Migration 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. Use `superpowers:using-git-worktrees` before editing.
|
||||
|
||||
**Goal:** Convert `clean-architecture-frontend-template` into a thin consumer of `test-assurance-platform` and `cicd-platform` while preserving product source, tests, fixtures, raw product artifact codecs, and all origin-unknown worktree changes.
|
||||
|
||||
**Architecture:** Product manifests declare risks, obligations, suites, components, outputs, and platform capabilities. Product scripts execute one bounded product purpose and emit raw artifacts. Test assurance owns selection through assessment; CI/CD owns workflow through promotion. Shadow mode has one candidate writer—the platform—and legacy release/promotion code is read-only until removed. The central required workflow is installed by the platform and is never copied into this repository.
|
||||
|
||||
**Tech Stack:** TypeScript 7.0.2, Node.js 24.14.0, pnpm 11.17.0, Vitest 4.1.10, Playwright 1.62.0, YAML/JSON Schema, `testctl` v3, `cicdctl`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Repository: `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template`; every command runs from its isolated migration worktree root unless it is explicitly marked read-only against the original dirty worktree.
|
||||
- Begin only from a clean worktree whose HEAD contains the approved spec amendment and all three 2026-08-02 plans in one immutable planning commit; never modify or clean the original dirty worktree.
|
||||
- Consume the immutable Test Assurance Task 10 release and CI/CD Task 11 P1/shadow release from the two companion plans.
|
||||
- Resolve `CICDCTL_BIN` and `TESTCTL_BIN` to absolute executable paths from the signed CICD catalog, and resolve `CICDCTL_DIGEST` and `TESTCTL_DIGEST` from the same catalog. Before every plan/manifest command, `sha256sum` must equal the signed value; PATH fallback is forbidden.
|
||||
- Do not claim P2/active or delete the legacy path until actual Gitea/runner/scanner/signer/provider evidence and required-status installation are observed.
|
||||
- Use only the exact capability IDs approved in the design.
|
||||
|
||||
- Keep production code, product tests/assertions, mocks, scenarios, fixtures, Vitest/Playwright config, V8 instrumentation inputs, and runtime/release artifact codecs.
|
||||
- Remove local workflow compilation, risk selection, waiver/normalization/assessment, test scheduling, provider invocation, signing, retention, candidate publication, and promotion engines after cutover.
|
||||
- `check:types` remains a CI Node responsibility. `check:architecture` alone maps to `architecture-typescript`.
|
||||
- Artifact browser suites consume the platform candidate; their Playwright `webServer` must never rebuild it.
|
||||
- The product does not add `.gitea/workflows/required-delivery-guard.yaml` or any equivalent copied central workflow.
|
||||
- Rollback changes only the signed platform catalog/version pin and promotes a previous immutable subject through `release-control`; it never re-enables a legacy writer.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Preserve and classify the existing dirty Task 3 work
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create in the clean migration worktree: `docs/migration/task3-wip-provenance.json`
|
||||
- Create: `docs/migration/task3-wip-disposition.md`
|
||||
- Read only: original dirty worktree tracked diff and untracked files
|
||||
|
||||
**Ledger contract:** Each entry records `path`, `workingBlobSha256`, and an ordered `hunks` array. Every hunk records `oldRange`, `newRange`, `diffSha256`, `owner` (`product`, `test-assurance`, `cicd`, `unrelated-or-unknown`), `provenance`, and `disposition` (`preserve-in-original`, `reimplement-in-platform`, `retain-product-codec`, `remove-after-cutover`). Mixed-responsibility files therefore have multiple hunk owners. The ledger does not copy source content or create a patch archive.
|
||||
|
||||
- [ ] Verify the migration worktree entry commit before inspecting WIP:
|
||||
|
||||
```bash
|
||||
git show HEAD:docs/superpowers/specs/2026-08-02-platform-owned-frontend-assurance-delivery-design.md
|
||||
git show HEAD:docs/superpowers/plans/2026-08-02-frontend-platform-consumer-migration.md
|
||||
git show HEAD:docs/superpowers/plans/2026-08-02-test-assurance-frontend-capabilities.md
|
||||
git show HEAD:docs/superpowers/plans/2026-08-02-cicd-frontend-assurance-delivery.md
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: all four documents exist and migration-worktree status is empty.
|
||||
- [ ] Capture `git status --short`, `git diff --name-status`, `git diff --numstat`, `git diff | sha256sum`, per-hunk unified diffs, and `git hash-object` for each present untracked/modified file without writing to the original worktree.
|
||||
- [ ] Add a failing migration test in `tests/unit/platform-migration-provenance.test.ts` that requires every hunk to have one owner/provenance/disposition, permits mixed owners per file, and rejects a blanket `frontend`/`keep-all` classification.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
corepack pnpm exec vitest run tests/unit/platform-migration-provenance.test.ts
|
||||
```
|
||||
|
||||
Expected: RED because the ledger does not exist.
|
||||
|
||||
- [ ] Create the ledger and disposition document with `apply_patch`. Explicitly classify workflow/gate/test-normalization/provider/promotion WIP to its platform owner and origin-unknown files as preserved in the original worktree.
|
||||
- [ ] Recompute every original working-blob and hunk digest, re-run the test, and compare original status plus digests with the capture; expected PASS with no drift.
|
||||
- [ ] Commit only the ledger, disposition, and test:
|
||||
|
||||
```bash
|
||||
git add docs/migration/task3-wip-provenance.json docs/migration/task3-wip-disposition.md tests/unit/platform-migration-provenance.test.ts
|
||||
git commit -m "docs: preserve Task 3 migration provenance"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Declare the CI/CD platform consumer
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `delivery-platform.yaml`
|
||||
- Create: `tests/unit/delivery-platform-manifest.test.ts`
|
||||
|
||||
**Selected capabilities:**
|
||||
|
||||
```text
|
||||
ci-standard-core
|
||||
ci-node-typescript
|
||||
ci-frontend
|
||||
ci-test-assurance
|
||||
ci-dependency-vulnerability
|
||||
ci-sbom
|
||||
ci-provenance
|
||||
ci-artifact-signing
|
||||
ci-static-artifact-supply-chain
|
||||
ci-static-site-publish
|
||||
delivery-release-control
|
||||
```
|
||||
|
||||
The manifest declares one frontend component rooted at `.`, one static-site output rooted at `dist`, pnpm frozen install, `lint`, `check:types`, `build`, the size/environment/determinism policy, and a signed `platformVersion` that exists in the released CI/CD catalog. It does not contain a workflow DAG, provider command, test command, or mutable tool image.
|
||||
|
||||
- [ ] Add RED tests that invoke `$CICDCTL_BIN manifest compile` after digest verification and reject missing capabilities, a floating platform version, product-owned test stages, arbitrary shell/provider commands, or a second candidate output.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
corepack pnpm exec vitest run tests/unit/delivery-platform-manifest.test.ts
|
||||
```
|
||||
|
||||
Expected: RED because `delivery-platform.yaml` is absent.
|
||||
|
||||
- [ ] Add the manifest only; do not add a package script that could resolve an arbitrary PATH binary or local compilation logic.
|
||||
- [ ] Compile the manifest with `"$CICDCTL_BIN" manifest compile --manifest delivery-platform.yaml --source-revision "$PLATFORM_SOURCE_REVISION" --output artifacts/platform/effective-project.json`; expected PASS at shadow activation.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add delivery-platform.yaml tests/unit/delivery-platform-manifest.test.ts
|
||||
git commit -m "chore(platform): declare frontend delivery capabilities"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Declare risks, obligations, change surfaces, and source suites
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `test-assurance.yaml`
|
||||
- Create generated: `test-assurance.lock.json`
|
||||
- Create: `config/test-assurance/risks/frontend.json`
|
||||
- Create: `config/test-assurance/obligations/frontend.json`
|
||||
- Create: `config/test-assurance/suites/source.json`
|
||||
- Create: `config/test-assurance/suites/artifact-templates.json`
|
||||
- Create: `config/test-assurance/change-surfaces/frontend.json`
|
||||
- Create: `config/test-assurance/legacy-command-disposition.json`
|
||||
- Create: `scripts/run-contract-negative-fixtures.ts`
|
||||
- Create: `scripts/reporters/vitest-discovery-reporter.ts`
|
||||
- Create: `scripts/write-production-module-inventory.ts`
|
||||
- Modify: `scripts/check-architecture.ts`
|
||||
- Modify: `vitest.config.ts`
|
||||
- Create: `tests/unit/test-assurance-source-manifest.test.ts`
|
||||
- Create: `tests/unit/raw-source-reporters.test.ts`
|
||||
- Create: `tests/unit/legacy-command-migration-completeness.test.ts`
|
||||
- Modify: `package.json`
|
||||
|
||||
**Source suite mapping:**
|
||||
|
||||
| Suite ID | Product argv | Capability |
|
||||
|---|---|---|
|
||||
| `runtime-schema` | `corepack pnpm test:runtime-schema` | `unit-typescript-vitest` |
|
||||
| `unit` | `corepack pnpm test:unit` | `unit-typescript-vitest` |
|
||||
| `reference-feature` | `corepack pnpm test:reference-feature` | `unit-typescript-vitest` |
|
||||
| `optional-recipes` | `corepack pnpm test:recipes` | `unit-typescript-vitest` |
|
||||
| `component` | `corepack pnpm test:component` | `component-react-vitest` |
|
||||
| `integration` | `corepack pnpm test:integration` | `integration-http-msw` |
|
||||
| `http-scenarios` | `corepack pnpm test:http-scenario-evidence` | `integration-http-msw` |
|
||||
| `architecture` | `corepack pnpm check:architecture` | `architecture-typescript` |
|
||||
| `coverage` | `corepack pnpm test:coverage` | `coverage-v8` |
|
||||
| `contract-negative-fixtures` | `corepack pnpm test:contract-negative-fixtures` | `unit-typescript-vitest` |
|
||||
|
||||
Every source suite declares `executionPhase: SOURCE`, an empty `requiredInputArtifacts`, a bounded argv array, timeouts, environment allowlist, and exact raw artifacts. `check:types` is absent because CI owns it. `test-assurance.yaml` references `artifact-templates.json` through `artifactSuiteTemplateFiles`; those templates are completed in Task 4.
|
||||
|
||||
- [ ] Add RED tests for exact capability IDs, one purpose per suite, no aggregate `test:all`, no `check:types`, no shell string, no `--passWithNoTests` on required suites, exact report paths, and source phase with zero input artifacts.
|
||||
- [ ] Parse every unique command and argument tuple in legacy `config/ci/gates.json` and require exactly one disposition: `ci-node`, `test-assurance-suite`, `cicd-release-or-security`, `product-dev-only`, or `retired-with-platform-evidence`. Missing or duplicate classification fails.
|
||||
- [ ] Add `test:contract-negative-fixtures` as one bounded product harness: it runs every expected-fail type/coverage/design-system/i18n/diagnostics/registry/route fixture, asserts the exact expected exit code and diagnostic identity from the disposition file, and exits 0 only when all negative contracts fail for the intended reason.
|
||||
- [ ] Classify `test:browser-capabilities` into the three artifact browser suites, `test:storybook` as a retained source test harness until an explicit platform disposition is proven, and `playwright.dev.config.ts` as `product-dev-only`; no legacy gate command disappears without platform evidence.
|
||||
- [ ] Add RED reporter tests requiring Vitest discovery IDs/counts alongside JUnit, a canonical production-module inventory alongside V8 coverage, an architecture graph/violations report, and typed HTTP receipts. These are raw observations only and contain no PASS/waiver/threshold decision.
|
||||
- [ ] Run the focused test; expected RED.
|
||||
- [ ] Add declarations. Remove `--passWithNoTests` from required source suite scripts while retaining optional behavior only for suites whose obligation explicitly permits no applicable tests. Wire the Vitest reporter into each required Vitest suite, make `check:architecture` emit the typed graph/violation JSON, and rewrite `test:coverage` to emit V8 summary plus production inventory without calling the local risk/threshold assessor.
|
||||
- [ ] Use released `testctl validate`, `lock`, and `compile` to generate the lock; never type toolchain digests by hand.
|
||||
- [ ] Re-run the focused test and:
|
||||
|
||||
```bash
|
||||
"$TESTCTL_BIN" validate --repository . --out artifacts/platform/source-validation.json
|
||||
"$TESTCTL_BIN" lock --repository . --out test-assurance.lock.json
|
||||
"$TESTCTL_BIN" compile --repository . --lock test-assurance.lock.json --out artifacts/platform/compiled-policy.json
|
||||
```
|
||||
|
||||
Expected: PASS with v3 and exact Node/pnpm/TypeScript/Vitest/Playwright pins.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add test-assurance.yaml test-assurance.lock.json config/test-assurance scripts/reporters/vitest-discovery-reporter.ts scripts/write-production-module-inventory.ts scripts/check-architecture.ts scripts/run-contract-negative-fixtures.ts vitest.config.ts tests/unit/test-assurance-source-manifest.test.ts tests/unit/raw-source-reporters.test.ts tests/unit/legacy-command-migration-completeness.test.ts package.json
|
||||
git commit -m "chore(test-assurance): declare frontend source suites"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Make browser suites consume the immutable candidate
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `playwright.config.ts`
|
||||
- Modify: `playwright.capabilities.config.ts`
|
||||
- Modify: `playwright.visual.config.ts`
|
||||
- Modify: `playwright.storybook.config.ts` only if it remains a required artifact suite
|
||||
- Create: `scripts/serve-test-candidate.ts`
|
||||
- Create: `scripts/reporters/playwright-evidence-reporter.ts`
|
||||
- Create: `tests/support/browser/mutation-evidence.ts`
|
||||
- Modify: `tests/e2e/reference-form.spec.ts`
|
||||
- Modify: `package.json`
|
||||
- Modify: `config/test-assurance/suites/artifact-templates.json`
|
||||
- Modify: `config/test-assurance/obligations/frontend.json`
|
||||
- Create: `tests/unit/artifact-suite-contract.test.ts`
|
||||
|
||||
**Artifact suite mapping:**
|
||||
|
||||
| Suite ID | Product argv | Capability |
|
||||
|---|---|---|
|
||||
| `e2e-chromium` | `corepack pnpm test:e2e:chromium` | `e2e-playwright-chromium` |
|
||||
| `e2e-firefox` | `corepack pnpm test:e2e:firefox` | `e2e-playwright-firefox` |
|
||||
| `e2e-webkit` | `corepack pnpm test:e2e:webkit` | `e2e-playwright-webkit` |
|
||||
| `accessibility` | `corepack pnpm test:a11y` | `accessibility-web` |
|
||||
| `visual-regression` | `corepack pnpm test:visual` | `visual-regression-web` |
|
||||
|
||||
All five repository templates declare `executionPhase: ARTIFACT` and one input declaration containing `artifactId: frontend-site` and `mediaType: application/vnd.delivery.static-site.v1+tar`. Templates contain no `sha256` field. After the platform builds the candidate, CI creates an artifact `ExecutionRequest` containing the actual candidate SHA-256; testctl materializes executable v3 `SuiteDefinition` and `WorkItem` documents with that exact digest.
|
||||
|
||||
- [ ] Add RED tests requiring separate Chromium/Firefox/WebKit commands and artifacts, rejecting a SHA/digest placeholder in committed templates, rejecting build commands in Playwright `webServer`, requiring candidate-root environment input, and checking typed Playwright JSON/JUnit, trace/screenshot/console/network indexes, browser provider identity, write mutation receipts, accessibility/manual-review identity, and visual baseline/diff identity.
|
||||
- [ ] Run the focused test; expected RED because browser configuration currently rebuilds the site.
|
||||
- [ ] Implement `serve-test-candidate.ts` as a bounded read-only static server over the executor-verified candidate directory. It reads the candidate root from the allowlisted environment and never verifies or substitutes the platform digest itself.
|
||||
- [ ] Change `playwright.config.ts` and `playwright.capabilities.config.ts` to call that server and add per-browser package scripts using `--project`; each browser suite includes both `tests/e2e/` and `tests/browser-capabilities/`. Add the product-owned Playwright reporter and mutation-evidence helper so tests emit traces, screenshots, console/network indexes, response/mutation/reload receipts, accessibility findings/manual records, and visual baselines/diffs without assessing them. Extend `reference-form.spec.ts` with the existing production-shaped create handler: observe the successful HTTP response, read the created resource, reload, read it again, and write one typed receipt keyed by the test/scenario ID.
|
||||
- [ ] Validate/lock/compile with testctl and run each product suite against a local candidate materialized by the platform fixture.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add playwright.config.ts playwright.capabilities.config.ts playwright.visual.config.ts playwright.storybook.config.ts scripts/serve-test-candidate.ts scripts/reporters/playwright-evidence-reporter.ts tests/support/browser/mutation-evidence.ts tests/e2e/reference-form.spec.ts package.json config/test-assurance tests/unit/artifact-suite-contract.test.ts
|
||||
git commit -m "refactor(browser): test the immutable platform candidate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Separate raw reporters and freeze legacy assurance as read-only
|
||||
|
||||
**Files:**
|
||||
|
||||
- Retain/refactor: `scripts/run-http-scenario-evidence.ts`
|
||||
- Retain/refactor: `scripts/lib/http-scenario-evidence.ts`
|
||||
- Retain/refactor: `scripts/write-a11y-report.ts`
|
||||
- Create: `scripts/lib/manual-a11y-record.ts`
|
||||
- Retain: product V8 instrumentation and module-inventory code in `vite.config.ts`, `vitest.config.ts`, and product codecs
|
||||
- Retain read-only until Task 8: `scripts/check-test-evidence.ts`, `scripts/verify-browser-capability-evidence.ts`, `scripts/check-risk-coverage.ts`, `scripts/lib/risk-coverage.ts`, `scripts/lib/local-policy-evidence.ts`, `scripts/lib/manual-a11y-evidence.ts`, `scripts/verify-a11y-manual.ts`
|
||||
- Create: `scripts/run-legacy-assurance-probe.ts`
|
||||
- Modify: `package.json`
|
||||
- Create: `tests/unit/raw-product-evidence-contract.test.ts`
|
||||
- Create: `tests/unit/legacy-assurance-readonly.test.ts`
|
||||
|
||||
**Boundary:** Product emitters may validate their own artifact schema and cross-fields, but the new manifests may not invoke local obligation satisfaction, waiver, quarantine, retry/flaky, coverage threshold, scenario completeness, browser matrix completeness, or evidence freshness logic. Legacy assessors remain callable only through `run-legacy-assurance-probe.ts`, which writes comparison output to a dedicated read-only shadow namespace and has no candidate/provider/promotion operation.
|
||||
|
||||
- [ ] Add RED boundary tests proving raw emitters contain no verdict semantics, new manifests never invoke a legacy assessor, and every legacy assessor is reachable only from the comparison probe.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/raw-product-evidence-contract.test.ts tests/unit/legacy-assurance-readonly.test.ts`; expected RED before the boundary is enforced.
|
||||
- [ ] Move any reusable false-green fixture to the Test Assurance platform implementation commit; in this repository keep only the product input fixture needed to reproduce the report.
|
||||
- [ ] Extract production-module inventory generation from `check-risk-coverage.ts` and raw manual-review codecs from `manual-a11y-evidence.ts`; keep threshold/completeness logic unchanged solely for the read-only probe until parity.
|
||||
- [ ] Update suite declarations to point directly at raw artifacts and ensure no automated workflow invokes the legacy probe.
|
||||
- [ ] Re-run `corepack pnpm exec vitest run tests/unit/raw-product-evidence-contract.test.ts tests/unit/legacy-assurance-readonly.test.ts tests/unit/test-assurance-source-manifest.test.ts tests/unit/artifact-suite-contract.test.ts`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add scripts/run-http-scenario-evidence.ts scripts/lib/http-scenario-evidence.ts scripts/write-a11y-report.ts scripts/lib/manual-a11y-record.ts scripts/write-production-module-inventory.ts scripts/run-legacy-assurance-probe.ts package.json tests/unit/raw-product-evidence-contract.test.ts tests/unit/legacy-assurance-readonly.test.ts config/test-assurance
|
||||
git commit -m "refactor(testing): isolate raw and legacy assurance paths"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Run one-writer shadow parity
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `docs/operations/platform-shadow-parity.md`
|
||||
- Create: `docs/operations/evidence/platform-shadow-readiness.json` only from an actual platform run
|
||||
- Create: `tests/unit/platform-shadow-contract.test.ts`
|
||||
- Modify: `delivery-platform.yaml` and `test-assurance.lock.json` only to pin the released shadow versions
|
||||
|
||||
**Parity identity:** source revision, delivery manifest/catalog digests, test manifest/lock/policy digests, selected suite IDs, discovered/executed counts, terminal classifications, coverage production-module universe, HTTP declared/executed IDs, three browser results, candidate/member digests, source and artifact plan/evidence/assessment digests, supply-chain/provider digests, and promotion readiness. Exclude timestamp, duration, temp path, and runner identity.
|
||||
|
||||
- [ ] Add RED tests proving there is no product-local central workflow, the platform is the only command allowed to build/freeze a candidate in shadow, legacy release/promotion commands are read-only probes, and no product script invokes a provider or mutates desired state.
|
||||
- [ ] Before shadow execution, query actual Gitea registration/status state and require: legacy `quality-gates.yml` registration `disabled`; legacy required status `detached`; central workflow installed externally with activation `shadow`; no product `required-delivery-guard.yaml`; legacy candidate/provider/promotion invocation count `0`; platform candidate writer count exactly `1`. Record actual workflow/status IDs and writer identity.
|
||||
- [ ] Run the released platform fixture against this exact source revision for a passing run and deliberate failures: zero discovery, missing report, retry-only pass, missing browser, changed candidate, wrong provider digest, and response loss.
|
||||
- [ ] Have the platform runner invoke `run-legacy-assurance-probe.ts` read-only and compare its test classifications with platform assessment. Do not register/run the legacy workflow and do not run legacy candidate creation, publication, or promotion.
|
||||
- [ ] Record the actual signed parity report digest and environment identities only after the run exists. If the required external environment is unavailable, leave this task incomplete and retain shadow activation.
|
||||
- [ ] Re-run focused contract tests; expected PASS for repository constraints even if P2 evidence remains blocked.
|
||||
- [ ] Commit the shadow contract and runbook before external evidence:
|
||||
|
||||
```bash
|
||||
git add docs/operations/platform-shadow-parity.md tests/unit/platform-shadow-contract.test.ts delivery-platform.yaml test-assurance.lock.json
|
||||
git commit -m "test(shadow): define parity and one-writer contract"
|
||||
```
|
||||
|
||||
- [ ] Only after the real run creates `docs/operations/evidence/platform-shadow-readiness.json`, verify its digest/signature and commit that file alone as `test(shadow): record platform parity evidence`. If the environment is unavailable, do not create or stage the file and leave Task 6 incomplete.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Activate the platform and prove platform-only rollback
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `delivery-platform.yaml` only to select the signed active catalog version
|
||||
- Modify: `test-assurance.lock.json` only through the verified testctl binary
|
||||
- Modify: `docs/operations/release-cache-rollback.md`
|
||||
- Modify: `docs/operations/platform-shadow-parity.md`
|
||||
- Create: `tests/unit/platform-rollback-contract.test.ts`
|
||||
- Create from a real drill only: `docs/operations/evidence/platform-cutover-rollback.json`
|
||||
|
||||
**Cutover gate:** all ten frontend test capabilities are R1 or higher; every selected CI/CD capability is P1/shadow or higher; source revision and manifest/catalog/lock digests match; the passing run and every named fault fixture have zero parity mismatch; legacy writer count is zero and platform writer count is one; named P2 Gitea/runner/scanner/signer/provider evidence exists; central status `platform/delivery-pipeline` is installed; the previous signed platform pin rollback drill verifies served-content digest; and the WIP ledger has zero unclassified hunks.
|
||||
|
||||
**Rollback sequence:** pause new promotions → reconcile every indeterminate operation ID → pin the previous signed platform catalog/version → verify and use the previous `CICDCTL_BIN`/`TESTCTL_BIN` digests → compile both manifests → promote the previous stable immutable subject through `release-control` → verify served-content digest → resume. No product workflow, legacy writer, rebuild, repackage, or mutable tag is permitted.
|
||||
|
||||
- [ ] Add RED tests rejecting rollback text/code that restores `quality-gates.yml`, invokes `ci:gate`, enables a legacy writer, rebuilds a candidate, or omits signed catalog, executable, and subject digest checks.
|
||||
- [ ] Activate the externally installed central workflow/status only after every cutover-gate predicate is machine-verified; do not delete repository files in this task.
|
||||
- [ ] Run a staging rollback to the previous signed platform pin and previous immutable subject, then roll forward again. Record operation IDs, catalog/executable/release/served-content digests, Gitea status ID, writer identity, and reconciliation outcome.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/platform-shadow-contract.test.ts tests/unit/platform-rollback-contract.test.ts`; expected PASS.
|
||||
- [ ] Commit runbook/test first. Commit `platform-cutover-rollback.json` separately only after a real signed drill exists; otherwise leave Task 7 incomplete.
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Remove local assurance and delivery engines after rollback evidence
|
||||
|
||||
**Entry gate:** Task 7 has a signed evidence digest and served-content equality; removing local engines is forbidden before it.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Delete: `.gitea/workflows/quality-gates.yml`
|
||||
- Delete: `config/ci/gates.json`
|
||||
- Delete: `scripts/generate-ci-workflow.ts`, `scripts/check-ci-contract.ts`, `scripts/run-ci-gate.ts`, `scripts/contracts/ci-gates.ts`
|
||||
- Delete: `scripts/lib/ci-contract-report.ts`, `scripts/lib/ci-gate-log.ts`, `scripts/lib/ci-step-result.ts`, `scripts/lib/ci-artifact-validator.ts`, `scripts/lib/ci-candidate-archive-cli.ts`, `scripts/lib/ci-candidate-archive.ts`, `scripts/lib/package-script-graph.ts`
|
||||
- Delete: `scripts/check-test-evidence.ts`, `scripts/verify-browser-capability-evidence.ts`, `scripts/check-risk-coverage.ts`, `scripts/lib/risk-coverage.ts`, `scripts/lib/local-policy-evidence.ts`, `scripts/verify-a11y-manual.ts`, `scripts/run-legacy-assurance-probe.ts`
|
||||
- Delete after raw codec extraction: `scripts/lib/manual-a11y-evidence.ts`
|
||||
- Delete: `scripts/create-release-candidate.ts`, `scripts/verify-reproducible-build.ts`, `scripts/verify-ci-candidate-archive.ts`, `scripts/verify-release-candidate.ts`, `scripts/run-and-validate-provider.ts`, `scripts/stage-verified-promotion.ts`, `scripts/verify-provider-evidence.ts`, `scripts/verify-supply-chain-promotion.ts`, `scripts/lib/release-candidate.ts`, `scripts/lib/promotion-stager.ts`, `scripts/lib/promotion-verifier.ts`, `scripts/lib/provider-evidence.ts`, `scripts/lib/provider-upload-validator.ts`
|
||||
- Delete: `scripts/security-scan.ts`, `scripts/generate-supply-chain.ts`, `scripts/verify-supply-chain-artifacts.ts`, `scripts/verify-archived-local-evidence.ts`, `scripts/check-supply-chain-provider-fixtures.ts`, `scripts/lib/local-release-evidence.ts`, `scripts/lib/release-input-evidence.ts`, `scripts/lib/supply-chain.ts`
|
||||
- Delete: `tests/unit/ci-workflow-generation.test.ts`, `tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap`, `tests/unit/ci-step-result.test.ts`, and `tests/fixtures/ci-contract/`
|
||||
- Preserve/refactor: `scripts/contracts/release-artifacts.ts`, `scripts/generate-build-manifest.ts`, `scripts/generate-artifact-schemas.ts`, `scripts/lib/build-manifest-outputs.ts`, `scripts/lib/release-runtime-coherence.ts`, `scripts/lib/manual-a11y-record.ts`, and their product-schema tests
|
||||
- Modify: `package.json`
|
||||
- Modify: `docs/operations/ci-quality-gates.md`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Create: `tests/unit/platform-engine-removal.test.ts`
|
||||
|
||||
- [ ] Recompute original dirty-worktree blob/hunk digests and stop on drift. Compare the current migration diff to the exact allowed path/hunk set in the ledger before deleting anything.
|
||||
- [ ] Add a RED removal test that rejects workflow-DAG types, Gitea expressions, artifact upload/download orchestration, test normalizers/assessors, provider/promotion mutation, exact job counts, `ci:gate`, and workflow generator/checker scripts in product paths.
|
||||
- [ ] Extract the raw manual accessibility record codec to `scripts/lib/manual-a11y-record.ts`, then delete only the assessment/expiry-completeness engine. Delete all other listed platform-owned files hunk-by-hunk; preserve product behavior, assertions, fixtures, and raw artifact codecs.
|
||||
- [ ] Remove `generate:ci-workflow`, `check:ci-workflow`, `ci:gate`, local `check:ci`, legacy assessor, provider/promotion, supply-chain, and candidate orchestration scripts from `package.json`. Keep build, lint/typecheck, one-purpose suites, raw reporters, and developer-only commands.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/platform-engine-removal.test.ts tests/unit/delivery-platform-manifest.test.ts tests/unit/test-assurance-source-manifest.test.ts tests/unit/artifact-suite-contract.test.ts`, then `corepack pnpm check:types` and `corepack pnpm lint`; expected PASS.
|
||||
- [ ] Verify `git diff --name-status` exactly matches the ledger's Task 8 allowlist. Stage only the explicit files listed in this task; never use `git add scripts`, `git add tests`, or `git add -A`.
|
||||
- [ ] Commit as `refactor(platform): remove copied assurance and delivery engines`.
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Finalize documentation, provenance, and consumer verification
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/operations/release-cache-rollback.md`
|
||||
- Modify: `docs/operations/platform-shadow-parity.md`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Modify: `README.md`
|
||||
- Modify and close: `docs/migration/task3-wip-provenance.json`
|
||||
- Modify: `docs/migration/task3-wip-disposition.md`
|
||||
|
||||
- [ ] Mark every WIP hunk `retained`, `reimplemented-in-platform`, `removed-after-cutover`, or `preserved-in-original`; require zero open disposition and reverify original dirty-worktree hashes.
|
||||
- [ ] Verify both binaries before invoking them:
|
||||
|
||||
```bash
|
||||
printf '%s %s\n' "$CICDCTL_DIGEST" "$CICDCTL_BIN" | sha256sum --check
|
||||
printf '%s %s\n' "$TESTCTL_DIGEST" "$TESTCTL_BIN" | sha256sum --check
|
||||
```
|
||||
|
||||
- [ ] Run final repository verification:
|
||||
|
||||
```bash
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm check:types
|
||||
corepack pnpm lint
|
||||
corepack pnpm test:all
|
||||
corepack pnpm exec vitest run tests/unit/delivery-platform-manifest.test.ts tests/unit/test-assurance-source-manifest.test.ts tests/unit/artifact-suite-contract.test.ts tests/unit/platform-engine-removal.test.ts tests/unit/platform-rollback-contract.test.ts
|
||||
"$TESTCTL_BIN" validate --repository . --out artifacts/platform/final-validation.json
|
||||
"$CICDCTL_BIN" manifest compile --manifest delivery-platform.yaml --source-revision "$PLATFORM_SOURCE_REVISION" --output artifacts/platform/effective-project.json
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: PASS. `PLATFORM_SOURCE_REVISION` is the exact 40-hex revision recorded by the platform run, not a branch or mutable lookup. Product tree contains manifests and product tests, not copied platform engines.
|
||||
- [ ] Commit the two migration ledger files and exact modified docs/README as `docs(platform): finalize consumer cutover`.
|
||||
|
||||
## Cross-repository execution order
|
||||
|
||||
1. Complete and release the Test Assurance plan through R1/v3 conformance.
|
||||
2. Complete and release the CI/CD plan through P1/shadow vertical conformance.
|
||||
3. Execute frontend Tasks 1–5 and validate both manifests locally.
|
||||
4. Execute Task 6 only with external one-writer state verified.
|
||||
5. Execute Task 7 only after every named P2 gate exists and prove rollback before deletion.
|
||||
6. Execute Tasks 8–9 only after signed rollback evidence. Until then, the correct state is shadow with legacy comparison code retained and no false active claim.
|
||||
@@ -0,0 +1,598 @@
|
||||
# Promotion Security Review Fixes Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make the captured candidate archive and a strict exact-five bundle validator the only promotion authority while closing evidence, lifecycle, staging, output, freshness, and workflow gaps found by security review.
|
||||
|
||||
**Architecture:** Candidate identity and local verification are derived exclusively from the inode-captured tar stream. The finalizer revalidates archived subordinate evidence, provider signatures, freshness, role-separated trust, and then creates and validates an exact-five bundle before returning descriptor- and inode-bound cleanup metadata. Provider execution, CLI output publication, and workflow gating expose small injectable boundaries so failure and cleanup behavior can be tested directly.
|
||||
|
||||
**Tech Stack:** Node.js 24, TypeScript, Zod, Vitest, GNU tar, bubblewrap, Gitea Actions workflow generation.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Work sequentially on the current clean `develop` HEAD and produce one review-fix commit.
|
||||
- Every production change follows focused RED, observed expected failure, minimal GREEN, and regression verification.
|
||||
- Candidate verification performs no checkout reads; test subprocesses from outside the checkout with contradictory canaries.
|
||||
- Preserve real GNU tar and bubblewrap coverage; do not claim native uploader or atomic `renameat2`/`unlinkat` semantics.
|
||||
- Gitea 1.26.4 and act_runner 1.0.0 exact-five upload/download/cancel behavior remains an explicitly documented external smoke boundary.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Canonical Captured Archive and Archived Local Authority
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/release-candidate.ts`
|
||||
- Modify: `scripts/lib/local-release-evidence.ts`
|
||||
- Modify: `scripts/lib/ci-candidate-archive.ts`
|
||||
- Delete: `scripts/lib/promotion-verifier.ts`
|
||||
- Delete: `scripts/verify-provider-evidence.ts`
|
||||
- Delete: `scripts/verify-supply-chain-promotion.ts`
|
||||
- Modify: `tests/unit/security-followup.test.ts`
|
||||
- Modify: `tests/unit/supply-chain.test.ts`
|
||||
- Modify: `tests/integration/security-followup-archive.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: captured tar bytes plus expected SHA-256.
|
||||
- Produces: `withVerifiedCapturedCandidate()` callback data derived only from the extracted, exact-member, digest-verified tar; `verifyArchivedLocalEvidence()` independently recomputes all feasible archived checks.
|
||||
|
||||
- [x] Add failing tests for invalid tar, archive/tree mismatch, contradictory archived subordinate FAIL, exact archived policy bytes, and checkout-independent execution.
|
||||
- [x] Run focused tests and record the expected RED diagnostics in the durable task report.
|
||||
- [x] Archive the exact policy/verifier inputs required for independent release, supply-chain, dependency, license, vulnerability, and secret-scan checks.
|
||||
- [x] Re-run producer checks against the extracted archive and require their result to agree with the assessment and member identities.
|
||||
- [x] Remove the obsolete standalone PASS issuers and route all fixture checking through real captured tar/finalizer validation.
|
||||
- [ ] Run focused archive, supply-chain, and integration tests to GREEN.
|
||||
|
||||
### Task 2: Exact-Five Validator and Role-Separated Trust
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/lib/exact-promotion-bundle.ts`
|
||||
- Create: `scripts/verify-exact-promotion-bundle.ts`
|
||||
- Modify: `scripts/lib/provider-evidence.ts`
|
||||
- Modify: `scripts/lib/promotion-stager.ts`
|
||||
- Modify: `scripts/contracts/promotion-artifacts.ts`
|
||||
- Modify: `tests/unit/security-followup.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: exactly five captured byte buffers and two trusted Ed25519 identities.
|
||||
- Produces: `verifyExactPromotionBundle()` that requires literal verifier identity/version, provider and subordinate PASS states, exact hashes, and equal run/source/candidate/provider/trust fields.
|
||||
|
||||
- [x] Add failing tests for provider FAIL/absence, arbitrary provider hash, swapped roles, shared-field mismatch, archive/report digest mismatch, and identical role keys.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Implement strict exact-five parsing/cross-record validation and expose a downstream CLI command.
|
||||
- [x] Reject equal DER-SPKI fingerprints and equal role key identity before evaluation/finalization.
|
||||
- [x] Invoke exact-five validation inside the finalizer before publication; the full real-build fixture rerun remains sandbox-blocked below.
|
||||
|
||||
### Task 3: Provider Lifecycle and Freshness
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-supervisor.ts`
|
||||
- Create: `scripts/lib/provider-process-runner.ts`
|
||||
- Modify: `scripts/run-and-validate-provider.ts`
|
||||
- Modify: `tests/unit/security-followup.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: a spawned bubblewrap child, injected timeout/clock, captured report.
|
||||
- Produces: a runner that SIGKILLs on timeout but rejects only after `close`, and supervision that samples freshness after provider/report capture.
|
||||
|
||||
- [x] Add failing stubborn-descendant/short-timeout and sequence-clock expiry tests.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Extract the process runner, wait for close after timeout, and preserve the timeout diagnostic.
|
||||
- [x] Issue provider timestamps immediately before execution, validate with a fresh clock after capture, and reject crossing expiry.
|
||||
- [x] Run focused lifecycle tests, including real stubborn descendants, to GREEN; the shared real-build/bubblewrap fixture remains sandbox-blocked below.
|
||||
|
||||
### Task 4: Inode-Pinned Staging and Output-Failure Cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/promotion-stager.ts`
|
||||
- Create: `scripts/lib/stage-verified-promotion-cli.ts`
|
||||
- Modify: `scripts/stage-verified-promotion.ts`
|
||||
- Modify: `scripts/cleanup-verified-promotion.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `FinalizedPromotion.stagingIdentity` and a testable CLI function whose append failure invokes cleanup from the in-memory result.
|
||||
|
||||
- [x] Add failing tests for leaf replacement during writes, final visibility mismatch, partial-failure cleanup, GITHUB_OUTPUT open/write failure, and expiry during staging.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Open the created leaf with `O_DIRECTORY|O_NOFOLLOW`, write through `/proc/self/fd/<leafFd>`, pin dev/ino, require visible identity equality, and propagate identity through cleanup.
|
||||
- [x] Force directory/file modes with `fchmod(0700/0400)` independent of a restrictive owner-preserving umask.
|
||||
- [x] Extract CLI dependencies; on any post-finalization output failure call direct cleanup before rethrowing.
|
||||
- [x] Revalidate evidence freshness before sealing and immediately before publication; isolated lifecycle/mode/output tests are GREEN and the shared real-build fixture remains sandbox-blocked below.
|
||||
|
||||
### Task 5: Workflow and Install Policy
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `scripts/check-ci-contract.ts`
|
||||
- Modify: `scripts/contracts/ci-gates.ts`
|
||||
- Modify: `scripts/generate-ci-workflow.ts`
|
||||
- Modify: `config/ci/gates.json`
|
||||
- Modify: `.gitea/workflows/quality-gates.yml`
|
||||
- Modify: `tests/unit/ci-workflow-generation.test.ts`
|
||||
- Modify: `tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: promotion job `if: ${{ always() && needs.immutable_build.result == 'success' && needs.vulnerability_provider.result == 'success' && needs.provenance_provider.result == 'success' }}` and install-bearing script graph enforcement.
|
||||
|
||||
- [x] Add failing contract/generator tests for the job condition, upload without `always()`, missing cleanup outputs, and nested install scripts lacking `--ignore-scripts`.
|
||||
- [x] Run focused tests and record RED.
|
||||
- [x] Add `--ignore-scripts` to `verify:lockfile` and recursively reject each reachable install invocation without it.
|
||||
- [x] Extend the typed job condition and render the explicit cancellation-resistant exact-needs predicate.
|
||||
- [x] Regenerate workflow/snapshot and run workflow contract/byte tests to GREEN.
|
||||
|
||||
### Task 6: Fixtures, Documentation, Full Verification, and Commit
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/check-supply-chain-provider-fixtures.ts`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Modify: `docs/operations/ci-quality-gates.md`
|
||||
- Modify: `.superpowers/sdd/2026-08-01-release-evidence-remediation/task-4-report.md` (ignored durable report)
|
||||
|
||||
- [x] Replace plaintext candidate fixtures with a real tar and canonical captured-archive/exact-five validation.
|
||||
- [x] Rewrite operator docs around the sole captured-archive/exact-five authority and retain the Gitea/runner external-smoke residual.
|
||||
- [ ] Run focused fixtures, archive integration, workflow snapshot/bytes, full unit, types, lint, `check:ci`, and diff checks; escalate only a sandbox-caused EPERM.
|
||||
- [x] Append all RED/GREEN and verification evidence/constraints to the durable report.
|
||||
- [ ] Invoke verification-before-completion, review the complete diff, commit once, and report commit/range/status.
|
||||
|
||||
---
|
||||
|
||||
## Review-Fix Wave D: Sealed Bytes, Replay Context, Scan Trust, and Cancellation
|
||||
|
||||
**Constraint:** Work only in the existing uncommitted tree. Do not write `.git`, stage, or commit. Each task follows a focused RED→GREEN cycle and records sandbox `EPERM` separately from product failures.
|
||||
|
||||
### Task D1: Seal the actual staged inode bytes
|
||||
|
||||
**Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interface:** The staging writer captures each canonical file through the already-open leaf FD using `O_NOFOLLOW`; it requires a regular single-link inode, mode `0400`, stable dev/ino/size, and the declared SHA-256. `verifyExactPromotionBundle` receives only these captured staged buffers immediately before return.
|
||||
|
||||
- [x] Add RED tests for unlink/recreate and chmod/mutation after a file write.
|
||||
- [x] Implement bounded descriptor-relative capture and exact-five seal validation.
|
||||
- [x] Run focused staging tests to GREEN.
|
||||
|
||||
### Task D2: Bind downstream verification to external expected identity
|
||||
|
||||
**Files:** `scripts/lib/exact-promotion-bundle.ts`, `scripts/verify-exact-promotion-bundle.ts`, `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interface:** `verifyExactPromotionBundle` requires `expected.run.id`, `expected.run.attempt`, `expected.sourceRevision`, and `expected.archiveSha256`; optional bundle/dist/lock/source-set digests are compared when supplied. The CLI obtains these values from dedicated environment variables and never derives them from the bundle.
|
||||
|
||||
- [x] Add a RED signed other-run replay test.
|
||||
- [x] Implement external expected-context comparison in library and CLI.
|
||||
- [x] Run exact-bundle tests to GREEN where the sandbox permits.
|
||||
|
||||
### Task D3: Pin mkdir-to-open identity
|
||||
|
||||
**Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, `docs/security/supply-chain.md`, `docs/operations/ci-quality-gates.md`
|
||||
|
||||
**Interface:** A post-mkdir/pre-open test hook can replace the leaf. The implementation compares mkdir-returned pathname metadata with the `O_DIRECTORY|O_NOFOLLOW` handle `fstat` before any write; it never uses pathname chmod.
|
||||
|
||||
- [x] Add a RED pre-open replacement test.
|
||||
- [x] Compare created and opened metadata and reject replacement.
|
||||
- [x] Document the residual portable Node same-UID pre-lstat/native-privilege boundary.
|
||||
|
||||
### Task D4: Conservatively parse install invocations
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** A bounded shell/token parser recognizes `pnpm install|i`, `npm install|ci|i`, and `yarn install` after supported manager-global options with split or `=` values. Any reachable package-manager invocation that cannot be classified is rejected.
|
||||
|
||||
- [x] Add the eight required global-option/alias RED cases plus malformed fail-closed cases.
|
||||
- [x] Implement tokenization and manager-specific invocation classification.
|
||||
- [x] Run install-policy tests to GREEN.
|
||||
|
||||
### Task D5: Signed secret-scan attestation
|
||||
|
||||
**Files:** `scripts/lib/provider-evidence.ts`, `scripts/lib/provider-supervisor.ts`, `scripts/lib/provider-upload-validator.ts`, `scripts/lib/promotion-stager.ts`, `scripts/lib/exact-promotion-bundle.ts`, relevant unit/integration tests and docs.
|
||||
|
||||
**Interface:** Vulnerability evidence v2 contains a strict `secretScanAttestation` with `status: PASS`, local-assessment, source-set, policy, SARIF, and scan-input digests. The supervisor derives the expected tuple from captured archive members, exports it to the provider, and upload/final verification requires exact equality under the Ed25519 signature.
|
||||
|
||||
- [x] Add RED forged-empty-SARIF and attestation-mismatch tests.
|
||||
- [x] Derive one captured-archive scan context and bind it through supervisor, signed schema, finalizer records, and exact validation.
|
||||
- [x] Run provider/security tests to GREEN where the sandbox permits.
|
||||
|
||||
### Task D6: Cancellation-safe workflow and exact upload paths
|
||||
|
||||
**Files:** `scripts/contracts/ci-gates.ts`, `scripts/generate-ci-workflow.ts`, `config/ci/gates.json`, generated workflow/snapshot, workflow tests, and operations/security docs.
|
||||
|
||||
**Interface:** Promotion uses a typed dependency-success/no-job-if variant, so cancellation cannot be overridden by job-level `always()`. Step cleanup retains bare `always()` for ordinary failures. Upload documentation names the five canonical paths under `staging_root` and states cancellation cleanup remains a runner/native smoke boundary.
|
||||
|
||||
- [x] Add RED generator/contract assertions for no promotion job `if` and retained cleanup `always()`.
|
||||
- [x] Regenerate workflow and snapshot after the typed condition change.
|
||||
- [x] Correct operator/security wording and run workflow/CI checks to GREEN.
|
||||
|
||||
### Task D7: Verification
|
||||
|
||||
- [x] Run focused suites after each GREEN, then affected/full unit tests, all TypeScript targets, ESLint, `check:ci`, generated-byte check, and both diff checks.
|
||||
- [x] Append exact PASS totals and sandbox-blocked commands to the ignored durable report.
|
||||
- [x] Report modified files and remaining native/Gitea/unsandboxed verification boundaries; do not attempt git staging or commit.
|
||||
|
||||
## Wave E: Unified parser and downstream boundary review
|
||||
|
||||
### Task E1: One tokenized manager parser
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** One parse result reports manager invocations, package-script dependencies, unsupported controls, and effective lifecycle suppression. Both graph traversal and install policy consume it. Single `&`, unknown manager grammar, and malformed options fail closed. The last valid `--ignore-scripts` assignment controls the effective value; false, contradictory, valueless, and malformed assignments are unsafe. Lifecycle-capable mutation builtins are never implicit repository scripts and require effective suppression.
|
||||
|
||||
- [x] Add RED tables for single-ampersand segmentation, false/override/malformed suppression, global-option run/implicit dependencies, yarn/corepack reachability, and builtin/script-name collisions.
|
||||
- [x] Replace the regex traversal and separate install scan with one parser result.
|
||||
- [x] Run the parser-focused and full workflow-generation suites.
|
||||
|
||||
### Task E2: Evaluator-owned secret-scan equality
|
||||
|
||||
**Files:** `scripts/lib/provider-evidence.ts`, `tests/unit/security-followup.test.ts`, finalizer tests.
|
||||
|
||||
**Interface:** `evaluatePromotionEvidence` itself compares the parsed vulnerability report's signed `secretScanAttestation` with `expected.secretScanAttestation`. A mismatch makes vulnerability and overall promotion status `FAIL_UNVERIFIED`, including the production finalizer path.
|
||||
|
||||
- [x] Add a RED evaluator mismatch test.
|
||||
- [x] Implement exact equality before vulnerability PASS assignment.
|
||||
- [x] Run security/provider-focused tests.
|
||||
|
||||
### Task E3: Downstream CLI exact-five contract
|
||||
|
||||
**Files:** `tests/unit/ci-artifact-contract.test.ts`, `tests/unit/security-followup.test.ts`, `scripts/verify-exact-promotion-bundle.ts` if required.
|
||||
|
||||
**Interface:** A real finalizer-produced canonical exact-five directory passes the downstream CLI when all required external expected values and trust keys are supplied. Every required expected variable missing or mismatched exits non-zero. Optional digests remain exact when present.
|
||||
|
||||
- [x] Add RED happy-path and required-env negative coverage using real finalizer output where sandbox execution permits.
|
||||
- [x] Make only the minimal CLI/library changes needed for GREEN.
|
||||
- [x] Separate child-process sandbox blockers from library assertions.
|
||||
|
||||
### Task E4: Verification
|
||||
|
||||
- [x] Run focused parser/security/CLI suites, TypeScript, ESLint, `check:ci`, and `git diff --check`.
|
||||
- [x] Run full unit if feasible and report nested-process `EPERM` separately.
|
||||
- [x] Do not stage or commit.
|
||||
|
||||
## Wave F: Manager parser boundary hardening
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Add behavior tests before production changes and keep unsupported manager grammar fail-closed.
|
||||
|
||||
### Task F1: Workspace dispatch and authoritative script lookup
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** The parser receives the authoritative root `scripts` record. Explicit `run` resolves a root script; pnpm/yarn implicit dispatch resolves only a known root script. Yarn `workspace` and `workspaces` dispatch are unsupported because the root graph does not load workspace package scripts. Builtin aliases are canonicalized before root-script lookup.
|
||||
|
||||
- [x] Add RED policy and graph tables for the three Yarn workspace dispatchers, pnpm `ln`, and unknown manager subcommands.
|
||||
- [x] Remove workspace dispatchers from safe builtins, pass known root scripts into the parser, and canonicalize `pnpm ln` to lifecycle `link` before implicit lookup.
|
||||
- [x] Run the focused dependency/lifecycle cases to GREEN.
|
||||
|
||||
### Task F2: Shell comments and lifecycle option state
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interface:** Unquoted `#` in a manager-bearing command is unsupported control syntax; quoted `#` remains ordinary token content. Lifecycle options are parsed in order into a canonical suppression state covering `--ignore-scripts`, `--no-ignore-scripts`, and `--config.ignore-scripts`; conflicting, malformed, unknown, or ineffective states fail closed. Other option-like lifecycle arguments require an explicit manager allowlist.
|
||||
|
||||
- [x] Add RED comment, negative suppression, supported positive, and unknown lifecycle-option tables.
|
||||
- [x] Implement comment-aware tokenization and one ordered lifecycle argument parser.
|
||||
- [x] Preserve the checked-in `--frozen-lockfile --ignore-scripts` path and run focused tests to GREEN.
|
||||
|
||||
### Task F3: Verification
|
||||
|
||||
- [x] Validate every checked-in package script through graph/install consumers without false positives.
|
||||
- [x] Run the full workflow-generation file and related security tests.
|
||||
- [x] Run all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; report nested-process `EPERM` separately.
|
||||
- [x] Update the durable report; do not stage or commit.
|
||||
|
||||
## Wave G: Verified cleanup and complete gate/parser preflight
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Every production change follows a focused failing behavior test.
|
||||
|
||||
### Task G1: Verified-FD-only failure cleanup
|
||||
|
||||
**Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, security/operations documentation.
|
||||
|
||||
**Interface:** `openedIdentityVerified` becomes true only after the opened directory descriptor matches the post-`mkdir` device/inode. Failure cleanup may unlink canonical files or `rmdir` only through that verified descriptor and a still-matching visible identity. A mismatched opened descriptor and any visible replacement are close-only; a moved original directory remains for fixture/operator cleanup because portable Node cannot safely recover it.
|
||||
|
||||
- [x] Change the pre-open replacement regression to require both the replacement canary and displaced original directory to survive the failure.
|
||||
- [x] Run the focused test to RED against parent-directory identity scanning.
|
||||
- [x] Remove unverified inode discovery/recovery and gate descriptor cleanup on explicit identity verification.
|
||||
- [x] Run staging race and cleanup tests to GREEN and document the native residual.
|
||||
|
||||
### Task G2: Contract-wide lifecycle preflight
|
||||
|
||||
**Files:** `scripts/contracts/ci-gates.ts`, `scripts/run-ci-gate.ts`, optional focused runner helper, `tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** `loadCiGateContract` runs `validateInstallScriptPolicy` over every unique contract command script after script existence and graph checks. The runner enters its execution callback only after this loader succeeds, enabling a no-execute regression without relying on a nested child process.
|
||||
|
||||
- [x] Add RED loader tables for contradictory npm suppression, pnpm config false, and `pnpm ln`, plus a production runner-boundary no-execute spy.
|
||||
- [x] Enforce contract-command install policy and route runner execution through the preflight boundary.
|
||||
- [x] Run loader/runner preflight tests to GREEN.
|
||||
|
||||
### Task G3: Foreign manifest scope by command class
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Manager-global options that change cwd, manifest or workspace scope are recorded during parsing. Explicit and implicit package-script dispatch with any such option is unsupported under the root-only graph. Lifecycle commands remain classifiable and are accepted only when their own ordered suppression/option grammar is safe.
|
||||
|
||||
- [x] Add RED policy+graph tables for pnpm filter/dir/`-C`, npm workspace/prefix, and Yarn cwd dispatch.
|
||||
- [x] Add positive externally scoped lifecycle cases with verified suppression.
|
||||
- [x] Track scope options and reject only package-script dispatch; run focused tests to GREEN.
|
||||
|
||||
### Task G4: Argument-sensitive builtin grammar and verification
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`, durable report.
|
||||
|
||||
**Interface:** Broad command-name-only safe builtins are replaced by exact per-manager read-only invocations. Init/explore/Yarn npm namespaces are unsupported. Audit is accepted only as an exact bare read-only command; `fix` and all unknown arguments are rejected.
|
||||
|
||||
- [x] Add RED policy+graph coverage for npm init/explore/audit-fix and Yarn npm publish, plus a bare-audit positive.
|
||||
- [x] Replace permissive builtin lookup with exact argument grammar.
|
||||
- [x] Audit every current package script for graph/policy false positives.
|
||||
- [x] Run staging/parser/no-execute/workflow/security suites, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; record sandbox `EPERM` separately and do not stage or commit.
|
||||
|
||||
## Wave H: npm post-script scope-option boundary
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Reproduce every reviewer command in a failing test before changing the parser.
|
||||
|
||||
### Task H1: Explicit and implicit npm dispatch arguments
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** After an explicit `npm run`/`run-script` dependency or an implicit npm lifecycle script, manager options before the first literal `--` are parsed using an exact harmless allowlist. Workspace/prefix selectors (`--workspace`, `-w`, `--workspaces`, `--prefix`, including supported attached/equal forms) and unknown manager options fail closed. Tokens after the first literal `--` are script arguments and cannot change the authoritative manifest scope.
|
||||
|
||||
- [x] Add RED policy-and-graph coverage for all seven reviewer inputs, short/equal forms, unknown pre-delimiter options, the literal `--` boundary, and ordinary current-tree dispatch.
|
||||
- [x] Implement one npm post-script argument parser shared by explicit and implicit dispatch.
|
||||
- [x] Run focused parser tests to GREEN.
|
||||
|
||||
### Task H2: Contract loader and runner boundary
|
||||
|
||||
**Files:** `tests/unit/ci-workflow-generation.test.ts`, contract preflight only if the RED test exposes a separate integration defect.
|
||||
|
||||
**Interface:** Every reviewer input is rejected by contract loading while the referenced root scripts exist and are otherwise safe. `withCiGatePreflight` must not enter its callback for any rejected command.
|
||||
|
||||
- [x] Add a table-driven loader/no-callback regression for the same seven reviewer inputs.
|
||||
- [x] Run focused preflight tests to GREEN.
|
||||
|
||||
### Task H3: Verification
|
||||
|
||||
- [x] Re-audit current package scripts through graph and policy consumers.
|
||||
- [x] Run workflow/security suites, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`.
|
||||
- [x] Record results in the durable report and do not stage or commit.
|
||||
|
||||
**Verification evidence:** The focused npm parser/preflight selection passed
|
||||
31/31. The workflow file passed 208/210; its two remaining tests reached the
|
||||
known nested-spawn sandbox boundary and reported `EPERM`. Security, supply-chain,
|
||||
and local-promotion tests passed 64/64. All six TypeScript targets, ESLint,
|
||||
`check:ci`, and `git diff --check` passed. Auditing the checked-in package found
|
||||
zero policy failures across 109 scripts and zero graph failures across 108
|
||||
entries (excluding the intentionally direct runner entry `ci:gate`). A broader
|
||||
artifact-contract run passed 102 assertions and blocked 23 fixture cases at the
|
||||
same nested `git ls-files` `EPERM` boundary. No `.git` write was performed.
|
||||
|
||||
Local pnpm 11.17 execution showed post-script `--filter`/`--dir` tokens arriving
|
||||
in the root script's argv, and official Yarn run documentation defines all
|
||||
parameters after the script name as script arguments. Those pre-existing
|
||||
negative expectations were therefore corrected to positive regressions; only
|
||||
npm receives the new post-script manager-option grammar.
|
||||
|
||||
## Wave I: npm hook closure and environment scope
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Add focused behavior tests and observe RED before each
|
||||
production change.
|
||||
|
||||
### Task I1: npm pre/main/post dependency closure
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Explicit npm `run`/`run-script` and implicit
|
||||
`start`/`stop`/`restart`/`test` return existing root-manifest lifecycle hooks in
|
||||
`pre`, main, `post` order. Hooks are omitted only when ordered manager/tail
|
||||
suppression is unambiguously effective before the first literal `--`; bare,
|
||||
false, negative, contradictory, malformed, or post-delimiter suppression keeps
|
||||
hook traversal active or fails closed.
|
||||
|
||||
- [x] Add RED policy/graph tables for nested, test, and restart pre/post hooks.
|
||||
- [x] Add RED suppression positives and false/negative/contradictory/delimiter negatives.
|
||||
- [x] Make npm tail parsing update the invocation suppression state and expand dependencies.
|
||||
- [x] Run hook/parser tests to GREEN.
|
||||
|
||||
### Task I2: Tokenized npm scope environment
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`, `scripts/contracts/ci-gates.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Case-insensitive assignments to `npm_config_workspace`,
|
||||
`npm_config_workspaces`, or `npm_config_prefix` fail closed when their shell
|
||||
segment executes npm. Direct assignment, `env`, `/usr/bin/env`, and an exported
|
||||
assignment inherited by a later npm segment are covered without raw-substring
|
||||
false positives for quoted text. `withCiGatePreflight` also rejects the same
|
||||
sensitive keys inherited through `process.env` before entering its callback.
|
||||
|
||||
- [x] Add RED policy/graph coverage for all reviewer assignment forms and quoted/current-tree positives.
|
||||
- [x] Add RED loader/no-callback coverage for command assignments and inherited process environment.
|
||||
- [x] Implement token/segment assignment state and the preflight environment boundary.
|
||||
- [x] Run environment/parser/preflight tests to GREEN.
|
||||
|
||||
### Task I3: Verification
|
||||
|
||||
- [x] Audit every current script through graph and policy consumers.
|
||||
- [x] Run focused parser/preflight, workflow/security, all TypeScript targets,
|
||||
ESLint, `check:ci`, and `git diff --check`.
|
||||
- [x] Update durable operations/security documentation and record sandbox-only
|
||||
nested spawn failures separately; do not stage or commit.
|
||||
|
||||
**Verification evidence:** Hook closure began RED 9/9 and GREEN 9/9;
|
||||
ordered suppression began with 6 expected failures and finished GREEN 22/22;
|
||||
wrapper/export/inherited environment coverage began with 7 expected failures
|
||||
and finished GREEN 28/28. A final all-command-class environment RED 3/3
|
||||
closed scoped lifecycle and builtin invocations. The combined Wave I focused
|
||||
selection passed 59/59. The complete workflow file passed 269/271; its only
|
||||
two failures were the existing nested child-spawn `EPERM` fixtures. Security,
|
||||
supply-chain, and local-promotion tests passed 64/64. All six TypeScript
|
||||
targets, ESLint, `check:ci`, and `git diff --check` passed. The checked-in tree
|
||||
had zero policy failures across 109 scripts, zero graph failures across 108
|
||||
entries after excluding the intentional direct runner entry `ci:gate`, and no
|
||||
sensitive inherited npm scope environment. No `.git` write was performed.
|
||||
|
||||
## Wave J: coherent npm environment state and hook semantics
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Add each reviewer form as a failing regression before changing
|
||||
the parser.
|
||||
|
||||
### Task J1: Stateful shell npm-scope environment analysis
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Shell segments carry a conservative npm-scope environment state
|
||||
to later reachable npm invocations. The parser recognizes any static path whose
|
||||
basename is `env`, optionally behind `command`, and rejects case-insensitive
|
||||
scope assignments in direct or env-wrapper contexts. Static assignment/export
|
||||
and `set -a` transitions are modeled across segments. Dynamic assignment names
|
||||
and environment mutations that cannot be modeled accurately (`set +a`,
|
||||
`unset`, `export -n`, `eval`, dot/source) make later npm dispatch unsupported.
|
||||
Quoted harmless text and non-scope static assignments remain accepted; analysis
|
||||
uses token and segment structure rather than raw substring matching.
|
||||
|
||||
- [x] Add RED policy/graph tables for every reviewer state transition, env path,
|
||||
command wrapper, dynamic assignment name, and unsupported mutation.
|
||||
- [x] Implement a shared tokenized shell-environment state machine and immediate
|
||||
npm invocation environment inspection.
|
||||
- [x] Add harmless quoted/static positive regressions and run focused tests GREEN.
|
||||
|
||||
### Task J2: Actual npm lifecycle-hook suppression semantics
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** Existing pre/main/post hooks are traversed for `run`,
|
||||
`run-script`, `start`, `stop`, `restart`, and `test`. Bare `--ignore-scripts`
|
||||
means true and omits hooks just like explicit true. False, negative,
|
||||
contradictory, malformed, and post-delimiter forms retain hook traversal or fail
|
||||
closed according to the existing ordered grammar.
|
||||
|
||||
- [x] Add hook safety/order coverage for run-script, start, and stop.
|
||||
- [x] Move bare suppression forms to positive regressions and retain all false,
|
||||
negative, contradictory, and delimiter negatives.
|
||||
- [x] Remove the explicitly-valued distinction and run focused tests GREEN.
|
||||
|
||||
### Task J3: Contract boundary and verification
|
||||
|
||||
- [x] Run every environment reviewer command through policy, graph, contract
|
||||
loading, and `withCiGatePreflight`, asserting the callback is never entered.
|
||||
- [x] Retain the inherited process-environment regression and audit the current
|
||||
package tree for policy/graph false positives.
|
||||
- [x] Run workflow/security suites, all TypeScript targets, ESLint, `check:ci`,
|
||||
and `git diff --check`; record sandbox-only failures and do not stage or commit.
|
||||
|
||||
**Verification evidence:** The initial Wave J selection produced 38 expected
|
||||
failures across loader/policy/graph environment cases and bare hook suppression,
|
||||
then passed 84/84 after implementation. A separate unsupported dynamic env-wrapper
|
||||
expansion regression went RED 2/2 and GREEN 2/2; the final combined selection
|
||||
passed 86/86. The full workflow file passed 327/329, with only the existing two
|
||||
nested child-spawn `EPERM` fixtures failing at the sandbox boundary. Security,
|
||||
supply-chain, local-promotion, and promotion-readiness tests passed 71/71. All
|
||||
six TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. The
|
||||
checked-in tree had zero policy failures across 109 scripts, zero graph failures
|
||||
across 108 entries after excluding the intentional direct runner entry `ci:gate`,
|
||||
and no sensitive inherited npm scope environment. No `.git` write was performed.
|
||||
|
||||
## Wave K: common shell-prefix grammar
|
||||
|
||||
**Constraint:** Continue in the existing uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Every wrapper/prefix reviewer command must be RED in all four
|
||||
public enforcement paths before production changes.
|
||||
|
||||
### Task K1: Shared prefix parser and state-builtin targeting
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** A single token-based prefix helper consumes leading static
|
||||
assignments, then exact `command`/`exec` wrapper chains and their supported
|
||||
separator syntax. It reports the effective command token/index, whether parsing
|
||||
is uncertain, and the leading assignments. Both immediate npm env inspection
|
||||
and persistent `export`/`set` state updates use this result. `command --` is
|
||||
accepted; unknown `command` options and unmodeled `exec` options before npm/env
|
||||
fail closed. Static paths retain basename-`env` behavior.
|
||||
|
||||
- [x] Add common policy/graph RED cases for `exec env`, `exec /bin/env`,
|
||||
`command exec env`, `command -- env`, assignment-prefixed `export`, and
|
||||
assignment-prefixed `set -a`.
|
||||
- [x] Reuse the same reviewer table through `loadCiGateContract` and
|
||||
`withCiGatePreflight`, asserting rejection and no callback entry.
|
||||
- [x] Implement the shared prefix parser, route immediate env inspection and
|
||||
state-builtin updates through it, and run the reviewer selection GREEN.
|
||||
- [x] Preserve split assignment/export ordering, dynamic LHS, quoted text,
|
||||
harmless `MESSAGE=...`, and supported command-wrapper positives.
|
||||
|
||||
### Task K2: Hook selection and final verification
|
||||
|
||||
**Files:** `tests/unit/ci-workflow-generation.test.ts`, durable report.
|
||||
|
||||
- [x] Ensure the final focused selection explicitly includes the bare npm hook
|
||||
suppression table as well as prefix/environment policy and runner tests.
|
||||
- [x] Run the full workflow file and security/supply/local-promotion suites;
|
||||
classify only the known nested-spawn sandbox failures separately.
|
||||
- [x] Audit all current scripts through policy and graph, then run all TypeScript
|
||||
targets, ESLint, `check:ci`, and `git diff --check`; do not stage or commit.
|
||||
|
||||
**Verification evidence:** The nine shared shell-prefix reviewer commands began
|
||||
RED in both enforcement tables, producing 18 expected failures across
|
||||
loader/runner and policy/graph, then passed 18/18 after the common parser was
|
||||
connected. The prefix negatives plus harmless positives passed 39/39. The final
|
||||
focused selection explicitly combined prefix cases, dynamic environment cases,
|
||||
effective/bare npm hook suppression, and harmless positives and passed 106/106.
|
||||
The complete workflow file passed 353/355; its only two failures were the known
|
||||
nested child-spawn `EPERM` fixtures. Security, supply-chain, local-promotion, and
|
||||
promotion-readiness tests passed 71/71. The current package tree had zero policy
|
||||
failures across 109 scripts, zero graph failures across 108 entries after
|
||||
excluding `ci:gate`, and no sensitive inherited npm scope environment. All six
|
||||
TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. The shared
|
||||
workspace was preserved and no `.git` write was performed.
|
||||
|
||||
## Wave L: structural manager-prefix gap rejection
|
||||
|
||||
**Constraint:** Continue in the shared uncommitted tree. Do not write `.git`,
|
||||
stage, or commit. Generalize the existing parser; do not add wrapper names to an
|
||||
allowlist.
|
||||
|
||||
### Task L1: Reject unmodeled tokens before package managers
|
||||
|
||||
**Files:** `scripts/lib/package-script-graph.ts`,
|
||||
`tests/unit/ci-workflow-generation.test.ts`.
|
||||
|
||||
**Interface:** The common shell-prefix result identifies the first effective
|
||||
command after modeled assignments and `command`/`exec` wrappers. When manager
|
||||
scanning later finds a package manager, every token between that effective
|
||||
command position and the manager position must belong to a grammar explicitly
|
||||
consumed by immediate env or corepack parsing. Otherwise the invocation is
|
||||
unsupported. This structural rule covers `nice`, absolute-path `nice`, `nohup`,
|
||||
and future unknown wrappers without naming them.
|
||||
|
||||
- [x] Add policy/graph RED coverage for the four env-wrapper reviewer commands
|
||||
and direct unknown-wrapper manager commands (`nice npm`, `time pnpm`).
|
||||
- [x] Reuse the env-wrapper reviewer commands through contract loading and
|
||||
`withCiGatePreflight`, asserting the callback remains false.
|
||||
- [x] Implement one structural gap check in manager parsing and run RED cases
|
||||
GREEN without adding wrapper names.
|
||||
- [x] Retain modeled assignment, `command`/`exec`/env/corepack, current-tree,
|
||||
quoted echo, and harmless assignment positives.
|
||||
|
||||
### Task L2: Verification
|
||||
|
||||
- [x] Run a focused selection containing structural negatives and all modeled
|
||||
prefix/environment positives.
|
||||
- [x] Run the workflow and security/supply/local-promotion suites, current-tree
|
||||
policy/graph/environment audit, all TypeScript targets, ESLint, `check:ci`, and
|
||||
`git diff --check`; record sandbox-only failures and do not stage or commit.
|
||||
|
||||
**Verification evidence:** The six unmodeled-prefix reviewer commands began RED
|
||||
in both enforcement tables, producing 12 expected loader/runner and policy/graph
|
||||
failures, then passed 12/12 after one structural prefix-gap check was added. The
|
||||
unmodeled negatives plus harmless/modeled positives passed 36/36. The final
|
||||
focused Wave H–L environment/prefix and effective/bare hook selection passed
|
||||
121/121. The full workflow file passed 368/370, with only the two known nested
|
||||
child-spawn `EPERM` fixtures failing at the sandbox boundary. Security,
|
||||
supply-chain, local-promotion, and promotion-readiness tests passed 71/71. The
|
||||
current package tree had zero policy failures across 109 scripts, zero graph
|
||||
failures across 108 entries after excluding `ci:gate`, and no sensitive inherited
|
||||
npm scope environment. All six TypeScript targets, ESLint, `check:ci`, and
|
||||
`git diff --check` passed. No wrapper-name allowlist was added, the workspace was
|
||||
preserved, and no `.git` write was performed.
|
||||
@@ -0,0 +1,499 @@
|
||||
# Provider Evidence Guardian Transaction 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:** Make guardian startup cleanup derive authority only from identities allocated before spawn while preserving no-replace raw/sealed publication and immediate safe retry.
|
||||
|
||||
**Architecture:** Before spawn, the client pins the canonical raw/evidence directories and exclusively allocates nonce-private raw-staging/sealed-temp inodes whose handles and identities it retains. The guardian inherits directory fd 3/fd 4 and private-file fd 5/fd 6, binds strictly validated aliases to those inherited identities, and transfers raw authority with a no-replace hard link before authenticated READY. The client and guardian clean only pre-recorded identities; neither promotes a pathname-discovered inode to ownership.
|
||||
|
||||
**Tech Stack:** Node.js 24 TypeScript, Vitest, Linux file identities and procfs, systemd user scopes, bubblewrap, cgroup v2.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Tasks 1-7 are the historical round-four/five record. Round-six Task 8 runs only in `/tmp/guardian-race-fix-y05lvLi1/repo` on top of `d781692`; never modify the original workspace, `/tmp/task3-integration-mU4L7J2u`, or the security-finalizer repository.
|
||||
- Use RED-GREEN-REFACTOR for every production behavior change.
|
||||
- Guardian argv contains only `process.execPath` and the trusted guardian script; its environment is empty, fd 3/fd 4 are the identity-pinned raw/evidence directories, and fd 5/fd 6 are the identity-pinned private raw/sealed allocations.
|
||||
- Every request/ack is canonical length-prefixed JSON with exact ordered fields, strict UTF-8, no NUL, total bounds, a 32-byte nonce, and constant-time authentication.
|
||||
- Canonical raw and sealed paths are derived from guardian `cwd` and provider kind; paths and identities are not accepted in the guard request.
|
||||
- Provider wall timeout is at most 30 minutes and post-processing allowance is exactly 10 minutes; the guardian maximum lease is 40 minutes.
|
||||
- Publication is no-replace and directory-durable. Abort/death/deadline cleans every raw/temp/final path that still names a pinned owned inode.
|
||||
- Preserve all cleanup failures with the primary failure using `AggregateError`.
|
||||
- Do not add PID-exhaustion loops or claim `RLIMIT_NPROC` enforcement.
|
||||
- Do not run or report live systemd/bwrap tests as passing while the approval limit prevents execution.
|
||||
- Never forward raw provider stdout/stderr bytes to supervisor or CI logs.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Versioned Transaction Protocol
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-protocol.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
type ProviderGuardianGuard = Readonly<{
|
||||
kind: "vulnerability" | "provenance";
|
||||
nonce: Buffer;
|
||||
deadlineEpochMs: number;
|
||||
}>;
|
||||
type ProviderGuardianReady = Readonly<{
|
||||
nonce: Buffer;
|
||||
rawDev: number;
|
||||
rawIno: number;
|
||||
sealedTempLeaf: string;
|
||||
sealedDev: number;
|
||||
sealedIno: number;
|
||||
}>;
|
||||
type ProviderGuardianPublish = Readonly<{
|
||||
nonce: Buffer;
|
||||
sealedDev: number;
|
||||
sealedIno: number;
|
||||
size: number;
|
||||
sha256: string;
|
||||
}>;
|
||||
function encodeProviderGuardianGuard(input: ProviderGuardianGuard): Buffer;
|
||||
function decodeProviderGuardianReady(payload: Buffer, nonce: Buffer): ProviderGuardianReady;
|
||||
function encodeProviderGuardianPublish(input: ProviderGuardianPublish): Buffer;
|
||||
function decodeProviderGuardianPublished(payload: Buffer, nonce: Buffer): void;
|
||||
function encodeProviderGuardianCommit(nonce: Buffer): Buffer;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write failing exact-protocol tests**
|
||||
|
||||
Assert that guard contains no path or identity, READY returns authenticated identities, publish binds exact identity/size/SHA-256, PUBLISHED authenticates the same nonce, and duplicate/reordered/trailing/oversized/invalid UTF-8/NUL/short-nonce frames fail.
|
||||
|
||||
```ts
|
||||
expect(JSON.parse(encodeProviderGuardianGuard(guard).subarray(4).toString())).toEqual({
|
||||
type: "guard", version: 2, kind: "vulnerability",
|
||||
nonce: nonce.toString("hex"), deadlineEpochMs,
|
||||
});
|
||||
expect(() => decodeProviderGuardianReady(duplicateNoncePayload, nonce)).toThrow(/canonical|fields/u);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run focused RED**
|
||||
|
||||
Run: `node_modules/.bin/vitest run tests/unit/task3-selective-integration.test.ts --reporter=default --maxWorkers=1`
|
||||
|
||||
Expected: FAIL because the v2 guard/READY/publish/PUBLISHED APIs do not exist and the old guard still accepts identities.
|
||||
|
||||
- [ ] **Step 3: Implement the minimal v2 codecs**
|
||||
|
||||
Use one bounded prefix/strict decode utility, exact ordered key arrays, canonical re-encoding, lowercase 64-hex nonces/SHA-256, safe positive integers, and `timingSafeEqual` for every acknowledgement/authentication comparison.
|
||||
|
||||
- [ ] **Step 4: Run focused GREEN**
|
||||
|
||||
Run the Step 2 command and require the protocol tests to pass.
|
||||
|
||||
### Task 2: Guardian-Owned Raw and Sealed Transaction
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Modify: `scripts/lib/provider-raw-cleanup.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 codecs.
|
||||
- Produces: real process state machine `guard -> READY -> publish -> PUBLISHED -> commitPending -> EOF success`.
|
||||
|
||||
- [ ] **Step 1: Write failing real-process creation tests**
|
||||
|
||||
Cover no-frame and partial-frame EOF with no files, authenticated READY-created raw/temp identities and modes, full-frame parent EOF cleanup before commit, deadline cleanup, and a near-timeout successful transaction.
|
||||
|
||||
```ts
|
||||
child.stdin.end(partialFrame);
|
||||
await completion;
|
||||
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(await lstat(rawPath)).toMatchObject({ mode: expect.any(Number) });
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify creation RED**
|
||||
|
||||
Run the focused test and require failure because the existing guardian expects supervisor-created identity and emits line-based READY.
|
||||
|
||||
- [ ] **Step 3: Implement exclusive creation and READY**
|
||||
|
||||
Derive canonical leaves, create raw and random sealed sibling temp with `O_EXCL|O_NOFOLLOW`, set raw/temp `0600`, fstat identities, close raw, keep temp handle, and emit bounded READY. On every error, attempt all owned cleanup before nonzero exit.
|
||||
|
||||
- [ ] **Step 4: Write failing publish/state tests**
|
||||
|
||||
Write validated bytes to the pinned temp, request publish, require PUBLISHED and final mode/hash/identity, then verify commit waits for EOF. Send one later trailing byte after commit and require final cleanup/nonzero exit. Kill the parent after PUBLISHED and require raw/temp/final absence.
|
||||
|
||||
- [ ] **Step 5: Implement no-replace durable publish and serialized terminal cleanup**
|
||||
|
||||
Verify held descriptor/path identity, `nlink=1`, `0400`, size, and SHA-256. Use `link(temp, final)`, `unlink(temp)`, final lstat identity, and parent-directory fsync. Serialize frame and EOF handling so a publish/death race cannot bypass cleanup. Commit removes raw and sets `commitPending`; only clean EOF exits zero.
|
||||
|
||||
- [ ] **Step 6: Run real-process GREEN**
|
||||
|
||||
Run focused tests and require zero raw/temp/final/process residuals in every failure case.
|
||||
|
||||
### Task 3: Authenticated Client Lease and Fallback Cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-client.ts`
|
||||
- Modify: `scripts/lib/validated-json-artifact.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
- Modify: `tests/unit/validated-json-artifact.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
type ProviderGuardianLease = Readonly<{
|
||||
pid: number;
|
||||
rawPath: string;
|
||||
rawIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
sealedPath: string;
|
||||
sealedTempPath: string;
|
||||
sealedIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
prematureExit: Promise<Error>;
|
||||
publish(bytes: Buffer): Promise<void>;
|
||||
commit(): Promise<void>;
|
||||
abort(): Promise<void>;
|
||||
}>;
|
||||
function serializeValidatedJsonArtifact(input: ValidatedJsonArtifactInput): Buffer;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write failing client transaction tests**
|
||||
|
||||
Require exact guardian argv and empty environment, READY identity capture, pinned temp write/fsync/mode, PUBLISHED wait, exactly-one terminal action, post-READY guardian SIGKILL cleanup of raw/temp/final, and cleanup error aggregation.
|
||||
|
||||
- [ ] **Step 2: Verify client RED**
|
||||
|
||||
Run focused and validated-writer tests. Expect missing publish/identity/serializer APIs.
|
||||
|
||||
- [ ] **Step 3: Implement serialization and lease**
|
||||
|
||||
Extract the existing schema-parse/pretty-JSON/newline serialization without changing `writeValidatedJsonArtifact`. Open the returned temp with `O_NOFOLLOW`, fstat identity, truncate/write/chmod `0400`/fsync/fstat/close, send authenticated publish metadata, and wait for PUBLISHED. Fallback cleanup attempts raw, temp, and final using READY identities and aggregates failures.
|
||||
|
||||
- [ ] **Step 4: Run client GREEN**
|
||||
|
||||
Run the Step 2 tests and require exact bytes, identities, cleanup, and no residual child.
|
||||
|
||||
### Task 4: Supervisor Transaction and Scope-Active Latch
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/run-and-validate-provider.ts`
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 3 lease and serializer.
|
||||
- Produces: guardian-owned raw/provider execution, awaited publication, output append, commit/EOF, and scope-confined kill ownership.
|
||||
|
||||
- [ ] **Step 1: Write failing supervisor ordering/latch tests**
|
||||
|
||||
Require no `createProviderOutput`, lease start before provider, lease raw identity passed to scope, serialized bytes published before output append, commit after output append, and postprocess allowance included in lease. Add a pure scope-latch unit boundary or static contract proving the guardian callback can call `killProviderUnit` only while `scopeActive` is true.
|
||||
|
||||
- [ ] **Step 2: Verify supervisor RED**
|
||||
|
||||
Run focused tests and expect the old create/write/cleanup ordering assertions to fail.
|
||||
|
||||
- [ ] **Step 3: Integrate the lease transaction**
|
||||
|
||||
Start guardian in `executeProvider`, use READY raw path/identity for provider bind and capture, publish serialized validated evidence through the lease, append output, then commit. Remove supervisor raw creation and normal sealed writer publication. Keep only identity-bound lease fallback cleanup.
|
||||
|
||||
Set `PROVIDER_POSTPROCESS_TIMEOUT_MS = 600_000` and request `providerWallTimeoutMs + PROVIDER_POSTPROCESS_TIMEOUT_MS`.
|
||||
|
||||
- [ ] **Step 4: Implement scope-active guardian exit ownership**
|
||||
|
||||
Race an awaited scope-completion promise against termination. The guardian callback records its error and invokes termination only while `scopeActive`; the same function sets the latch false exactly once when kill/collection or normal collection completes. The callback never throws or creates an unobserved kill promise after the latch closes.
|
||||
|
||||
- [ ] **Step 5: Run supervisor GREEN**
|
||||
|
||||
Run focused tests and type/lint checks. Live systemd tests remain unexecuted and are not reported as passing.
|
||||
|
||||
### Task 5: Regression Fixtures and Documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/unit/task3-selective-integration.test.ts`
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
- Modify: `docs/operations/ci-quality-gates.md`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Modify: `docs/superpowers/specs/2026-08-02-provider-raw-guardian-design.md`
|
||||
|
||||
- [ ] **Step 1: Complete real-process regressions**
|
||||
|
||||
Cover no/partial frame, parent kill near READY, post-READY guardian kill, PUBLISHED parent death, publish/commit race, later-chunk commit trailing data, deadline/near-timeout, same-workspace retry, and zero guardian/raw/temp/final residuals.
|
||||
|
||||
- [ ] **Step 2: Specify live regressions**
|
||||
|
||||
Add active-scope guardian kill, post-scope/precommit guardian kill, supervisor hard death after PUBLISHED with same-workspace retry, and detached descendant attempts for both an external marker and raw append. Every case requires zero cgroup/process/file residuals. Do not execute these tests under the current approval limit.
|
||||
|
||||
- [ ] **Step 3: Correct operations and security docs**
|
||||
|
||||
Document guardian-owned creation/publication, READY/PUBLISHED identities, ten-minute postprocess lease, commitPending/EOF success, no-replace link publication, scopeActive kill ownership, regular-file `GITHUB_OUTPUT`, and explicit live-test limitation.
|
||||
|
||||
- [ ] **Step 4: Fresh verification**
|
||||
|
||||
Run focused real-process tests, validated artifact tests, direct Node/test/recipe TypeScript configs, full lint, artifact schemas, CI contract, generated workflow byte check, and `git diff --check`. Record broad-suite sandbox `EPERM` separately and never convert unexecuted live tests into PASS.
|
||||
|
||||
- [ ] **Step 5: Review and commit round four**
|
||||
|
||||
Confirm only the isolated worktree changed, no protocol secret/path enters argv, cleanup checks both sealed names by identity, and only the temp `node_modules` symlink remains untracked. Create a separate round-four implementation commit above the design/plan commit.
|
||||
|
||||
### Task 6: Round-Five Pre-READY Recovery Authority
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-protocol.ts`
|
||||
- Modify: `scripts/lib/provider-guardian-client.ts`
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Test: `tests/unit/provider-guardian-transaction.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `providerGuardianSealedTempLeaf(kind, nonce): string`, inherited raw/evidence directory fds 3/4, and descriptor-relative startup/lease cleanup.
|
||||
|
||||
- [x] **Step 1: Write failing pre-READY hard-death tests**
|
||||
|
||||
Start the real client without awaiting READY, observe its direct guardian child,
|
||||
kill the guardian when either deterministic transaction leaf first appears, and
|
||||
require startup rejection, zero raw/temp/final residuals, and a successful
|
||||
same-workspace `startProviderGuardian(...).abort()` retry. Also require the temp
|
||||
leaf computed before spawn to equal READY exactly and inherited fd 3/fd 4 to
|
||||
remain directories during the lease.
|
||||
|
||||
- [x] **Step 2: Run focused RED**
|
||||
|
||||
Run: `node_modules/.bin/vitest run tests/unit/provider-guardian-transaction.test.ts --reporter=default --maxWorkers=1`
|
||||
|
||||
Expected: FAIL because the client has neither pre-spawn directory handles nor a
|
||||
deterministic temp leaf and cannot clean a guardian killed before READY.
|
||||
|
||||
- [x] **Step 3: Implement pinned descriptor recovery**
|
||||
|
||||
Open and verify the canonical raw/evidence directories with
|
||||
`O_DIRECTORY|O_NOFOLLOW`; derive the temp leaf from provider kind and the first
|
||||
16 nonce bytes; spawn with those handles at fd 3/fd 4. Use only
|
||||
`/proc/self/fd/<fd>/<leaf>` for guardian creation, publication, sync, and cleanup.
|
||||
On startup failure, open each exact leaf through the still-live client
|
||||
descriptor, fstat a regular single-link inode, close the discovery handle, and
|
||||
run identity-bound quarantine/unlink. Aggregate primary, cleanup, and directory
|
||||
close errors. Retain both handles until commit/abort terminates.
|
||||
|
||||
- [x] **Step 4: Run focused GREEN**
|
||||
|
||||
Run the Step 2 command and require the pre-READY kill/retry and all round-four
|
||||
transaction tests to pass.
|
||||
|
||||
### Task 7: Round-Five Log Privacy and Terminal Fail-Closed Behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/run-and-validate-provider.ts`
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Test: `tests/unit/provider-guardian-transaction.test.ts`
|
||||
- Test: `tests/unit/ci-artifact-contract.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 6 descriptor-pinned transaction.
|
||||
- Produces: bounded discard of provider output and nonzero guardian termination even when diagnostic fds are closed.
|
||||
|
||||
- [x] **Step 1: Write failing privacy and closed-stderr tests**
|
||||
|
||||
Run a successful provider that both receives and prints a unique
|
||||
`VULNERABILITY_PROVIDER_*` credential, then assert the credential is absent from
|
||||
supervisor stdout/stderr while the sealed signed evidence succeeds. Replace the
|
||||
FD-limit provider's stderr marker expectations with evidence/side-channel state.
|
||||
Spawn a real guardian with stderr's read side destroyed, establish owned files,
|
||||
then abort or send invalid input and require zero files plus a nonzero exit.
|
||||
|
||||
- [x] **Step 2: Run targeted RED**
|
||||
|
||||
Run the focused guardian and selected CI artifact tests. Expect credential
|
||||
disclosure and the existing raw provider stderr marker assertions to fail the
|
||||
new contract; the EPIPE case can exit without the required nonzero terminal.
|
||||
The executable non-live RED observed six expected failures: missing deterministic
|
||||
leaf/fd inheritance/output limiter, retained pre-READY raw, and closed-stderr
|
||||
exit 0. The live credential-printing fixture is authored but remains NOT RUN.
|
||||
|
||||
- [x] **Step 3: Implement minimal privacy and terminal fixes**
|
||||
|
||||
Continue counting provider stdout/stderr bytes against the aggregate output
|
||||
limit but discard captured bytes instead of retaining or forwarding them. Make
|
||||
guardian fd-close and stderr diagnostics best effort, run cleanup first, and
|
||||
place `process.exit(exitCode)` or self-`SIGKILL` in an unconditional final
|
||||
branch that cannot be skipped by `EPIPE`/`EBADF`.
|
||||
|
||||
- [x] **Step 4: Run targeted GREEN and regression verification**
|
||||
|
||||
Run focused guardian tests, selected non-live privacy tests, Node/test
|
||||
TypeScript, affected ESLint, docs readiness, and `git diff --check`. Do not run
|
||||
live systemd/bwrap tests under the approval limit.
|
||||
|
||||
- [x] **Step 5: Commit round five implementation**
|
||||
|
||||
Commit production, tests, and operational/security documentation separately
|
||||
above this round-five design/plan commit. Record live systemd/bwrap as NOT RUN.
|
||||
|
||||
Round-five verification record:
|
||||
|
||||
- Focused real-process/unit GREEN: 4 files, 52 tests passed.
|
||||
- Direct Node and test TypeScript projects: PASS.
|
||||
- Affected ESLint with zero warnings: PASS.
|
||||
- Documentation readiness: `PASS_SCOPED`.
|
||||
- `git diff --check`: PASS.
|
||||
- Live systemd/bwrap credential, FD-limit, cgroup, and hard-death fixtures:
|
||||
**NOT RUN** because the active approval limit forbids those executions.
|
||||
|
||||
### Task 8: Round-Six Pre-READY Inode Ownership
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/lib/provider-guardian-protocol.ts`
|
||||
- Modify: `scripts/lib/provider-guardian-client.ts`
|
||||
- Modify: `scripts/lib/provider-raw-guardian.ts`
|
||||
- Test: `tests/unit/provider-guardian-transaction.test.ts`
|
||||
- Modify: `docs/security/supply-chain.md`
|
||||
- Modify: `docs/operations/ci-quality-gates.md`
|
||||
- Modify: `docs/superpowers/specs/2026-08-02-provider-raw-guardian-design.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
function providerGuardianRawStagingLeaf(
|
||||
kind: ProviderGuardianKind,
|
||||
nonce: Buffer,
|
||||
): string;
|
||||
|
||||
type RecoveryAuthority = Readonly<{
|
||||
rawDirectoryHandle: FileHandle;
|
||||
evidenceDirectoryHandle: FileHandle;
|
||||
rawStagingHandle: FileHandle;
|
||||
sealedTempHandle: FileHandle;
|
||||
rawIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
sealedIdentity: Readonly<{ dev: number; ino: number }>;
|
||||
rawStagingPinnedPath: string;
|
||||
rawPinnedPath: string;
|
||||
sealedTempPinnedPath: string;
|
||||
sealedPinnedPath: string;
|
||||
}>;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write the deterministic external-canary RED**
|
||||
|
||||
Create a temporary guardian fixture that writes a spawn marker and remains
|
||||
alive without producing READY. Start the real client, wait for that marker (so
|
||||
`assertRecoveryLeavesMissing` has completed), create a fixed-raw canary, kill
|
||||
the direct guardian, and require startup rejection without canary deletion or
|
||||
mutation.
|
||||
|
||||
```ts
|
||||
const canaryBytes = Buffer.from("external-canary\n");
|
||||
const canaryHandle = await open(rawPath, constants.O_CREAT | constants.O_EXCL |
|
||||
constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
||||
await canaryHandle.writeFile(canaryBytes);
|
||||
const canaryIdentity = await canaryHandle.stat();
|
||||
await canaryHandle.close();
|
||||
process.kill(guardianPid, "SIGKILL");
|
||||
await expect(starting).rejects.toThrow(/provider guardian/u);
|
||||
expect(await readFile(rawPath)).toEqual(canaryBytes);
|
||||
expect(await lstat(rawPath)).toMatchObject({
|
||||
dev: canaryIdentity.dev,
|
||||
ino: canaryIdentity.ino,
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the canary RED and confirm the ownership bug**
|
||||
|
||||
Run:
|
||||
`node_modules/.bin/vitest run tests/unit/provider-guardian-transaction.test.ts -t "preserves an external raw canary" --reporter=default --maxWorkers=1`
|
||||
|
||||
Expected: FAIL with `ENOENT` when reading the canary because
|
||||
`discoverAndCleanupOwnedLeaf` opens the current raw pathname and promotes the
|
||||
external inode to cleanup authority.
|
||||
|
||||
- [ ] **Step 3: Add private-leaf derivation and client allocations**
|
||||
|
||||
Derive raw staging and sealed temp from the same first 16 nonce bytes:
|
||||
|
||||
```ts
|
||||
return `.${baseLeaf(kind)}.guardian-${nonce.subarray(0, 16).toString("hex")}.raw.tmp`;
|
||||
```
|
||||
|
||||
Through the pinned directory paths, create raw staging and sealed temp with
|
||||
`O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW`, mode `0600`; require regular file, link count
|
||||
one, mode `0600`, and size zero; store identities before spawn. Spawn with fd
|
||||
3-fd 6. If allocation, validation, or spawn fails, identity-clean every private
|
||||
alias and close every opened handle while preserving primary and cleanup/close
|
||||
errors in one `AggregateError`.
|
||||
|
||||
- [ ] **Step 4: Add concurrency, bootstrap, and link-before-READY RED tests**
|
||||
|
||||
Add real-process tests that require:
|
||||
|
||||
```ts
|
||||
// no/partial frame: bootstrap-owned private aliases are removed
|
||||
child.stdin!.end(partialFrame);
|
||||
await expect(readdir(rawDirectory)).resolves.toEqual([]);
|
||||
|
||||
// same kind: exactly one READY lease, loser never removes winner raw
|
||||
const results = await Promise.allSettled([startProviderGuardian(input), startProviderGuardian(input)]);
|
||||
expect(results.filter(({ status }) => status === "fulfilled")).toHaveLength(1);
|
||||
expect(results.filter(({ status }) => status === "rejected")).toHaveLength(1);
|
||||
|
||||
// canonical raw link exists but READY has not been accepted
|
||||
process.kill(guardianPid, "SIGKILL");
|
||||
await expect(starting).rejects.toThrow(/provider guardian/u);
|
||||
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
```
|
||||
|
||||
The link-before-READY test watches only the fixed raw basename, obtains the
|
||||
direct child pid before the event, and kills on that exact link event so private
|
||||
allocation events cannot satisfy the synchronization point. Each test performs
|
||||
a same-workspace retry and requires no owned private/canonical residue.
|
||||
|
||||
- [ ] **Step 5: Implement guardian bootstrap identity binding**
|
||||
|
||||
At process bootstrap, fstat fd 5/fd 6 and read `/proc/self/fd/5|6`. Accept an
|
||||
alias only if `dirname(readlink)` is the canonical expected directory, basename
|
||||
is a direct child matching the exact raw-staging or sealed-temp lowercase-hex
|
||||
grammar, both names encode the same kind/nonce prefix, and descriptor-relative
|
||||
lstat equals the inherited fd identity/type/mode/size/link count. Store the fd
|
||||
identity before reading any pathname; the pathname only becomes an alias for
|
||||
that identity.
|
||||
|
||||
On valid guard, require exact `providerGuardianRawStagingLeaf(kind, nonce)` and
|
||||
`providerGuardianSealedTempLeaf(kind, nonce)` matches. Use
|
||||
`link(rawStaging, rawCanonical)` without replacement, check both aliases equal
|
||||
the inherited raw identity with link count two, unlink raw staging, fsync fd 3,
|
||||
and check raw canonical remains the same identity with link count one before
|
||||
READY. Use the inherited sealed identity for READY and publication.
|
||||
|
||||
- [ ] **Step 6: Replace discovery cleanup and close all private fds**
|
||||
|
||||
Delete `discoverAndCleanupOwnedLeaf`. Client pre-READY and fallback cleanup
|
||||
attempts raw staging/canonical with only `recovery.rawIdentity`, then sealed
|
||||
temp/final with only `recovery.sealedIdentity`. Guardian no/partial-frame and
|
||||
terminal cleanup uses only its bootstrap fd identities and bound aliases.
|
||||
|
||||
On success and every failure branch, attempt all cleanup first, close guardian
|
||||
fd 5/fd 6 duplicates and client fd 3-fd 6 handles exactly once, and append every
|
||||
close failure to the existing aggregate. Never open a current leaf to obtain a
|
||||
new cleanup identity.
|
||||
|
||||
- [ ] **Step 7: Run focused GREEN and regressions**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node_modules/.bin/vitest run tests/unit/provider-guardian-transaction.test.ts --reporter=default --maxWorkers=1
|
||||
node_modules/.bin/vitest run tests/unit/provider-output-limiter.test.ts tests/unit/ci-artifact-contract.test.ts --reporter=default --maxWorkers=1
|
||||
node_modules/.bin/tsc --project tsconfig.node.json
|
||||
node_modules/.bin/tsc --project tsconfig.test.json
|
||||
node_modules/.bin/eslint scripts/lib/provider-guardian-protocol.ts scripts/lib/provider-guardian-client.ts scripts/lib/provider-raw-guardian.ts tests/unit/provider-guardian-transaction.test.ts --max-warnings=0
|
||||
node scripts/verify-documentation-readiness.ts
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Require focused tests, Node/test TypeScript, affected ESLint, documentation
|
||||
readiness, and whitespace verification to pass. Live systemd/bwrap fixtures
|
||||
remain **NOT RUN** under the current approval limit.
|
||||
|
||||
- [ ] **Step 8: Review and commit round six**
|
||||
|
||||
Confirm the original workspace, `/tmp/task3-integration-mU4L7J2u`, and the
|
||||
security-finalizer repository are unchanged; only the temporary `node_modules`
|
||||
symlink is untracked. Commit production/tests/docs together above design commit
|
||||
`0b1a1db` and report the isolated path, commit SHA, RED evidence, and fresh GREEN
|
||||
evidence.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Security Finalizer 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:** Finalize a captured immutable candidate into one private random exact-five staging directory with strict v3 verification records and deterministic cleanup.
|
||||
|
||||
**Architecture:** `finalizeVerifiedPromotion` captures the archive, provider reports, and public keys before validation, evaluates only those captured bytes against archived local evidence, generates both v3 records in memory, and publishes five read-only files under a descriptor-relative runner-temp directory. The generated workflow consumes the returned staging path immediately and always invokes the token-bound cleanup CLI.
|
||||
|
||||
**Tech Stack:** Node.js 24, TypeScript, Zod, Vitest, bubblewrap-independent filesystem primitives, generated Gitea Actions YAML.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Never accept pre-existing provider- or promotion-verification JSON as an input.
|
||||
- Stage exactly archive, vulnerability report, provenance attestation, provider verification v3, and promotion verification v3.
|
||||
- Use injected time and randomness for deterministic tests.
|
||||
- Use a random runner-temp directory at mode `0700`, files at `0400`, and `O_EXCL | O_NOFOLLOW` creation.
|
||||
- Do not claim that TypeScript closes the Gitea upload action pathname-reopen issue or guarantees `renameat2` semantics.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Exact-five finalizer contract
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
- Modify: `scripts/lib/promotion-stager.ts`
|
||||
- Modify: `scripts/contracts/promotion-artifacts.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `finalizeVerifiedPromotion(input, dependencies)` with captured archive/report/key inputs.
|
||||
- Produces: `{ stagingRoot, cleanupToken, files }` where `files` is the canonical exact-five name/digest list.
|
||||
|
||||
- [ ] **Step 1: Write failing tests** for no pre-existing records, strict distinct v3 roles, exact provider-record and local-assessment hashes, full run/source/candidate/nonces/key/trust bindings, key rotation, captured-source mutation, and no output on failures.
|
||||
- [ ] **Step 2: Run RED:** `corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts -t "verified promotion finalizer" --maxWorkers=1` and retain the first contract failure.
|
||||
- [ ] **Step 3: Implement minimal finalizer changes** so all validation and record generation consume captured bytes and both PASS records are created only after local/provider PASS.
|
||||
- [ ] **Step 4: Run GREEN:** rerun the focused Vitest command and require zero failures.
|
||||
|
||||
### Task 2: Private staging and cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/unit/ci-artifact-contract.test.ts`
|
||||
- Modify: `scripts/lib/promotion-stager.ts`
|
||||
- Modify: `scripts/cleanup-verified-promotion.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: injected `randomBytes`, runner-temp root, cleanup token.
|
||||
- Produces: descriptor-relative random staging at `0700`, exact files at `0400`, and token-bound cleanup.
|
||||
|
||||
- [ ] **Step 1: Write failing tests** for deterministic naming, modes, stable-path absence, exclusive no-follow creation, parent/leaf substitution, success cleanup, and failure cleanup.
|
||||
- [ ] **Step 2: Run RED:** use the Task 1 focused Vitest command and retain the first filesystem-boundary failure.
|
||||
- [ ] **Step 3: Implement minimal private publication and cleanup changes** using `/proc/self/fd` where available, bounded writes, identity rechecks, and removal of owned partial roots.
|
||||
- [ ] **Step 4: Run GREEN:** rerun the focused Vitest command and require zero failures.
|
||||
|
||||
### Task 3: Workflow handoff
|
||||
|
||||
**Files:**
|
||||
- Modify: `config/ci/gates.json`
|
||||
- Modify: `scripts/contracts/ci-gates.ts`
|
||||
- Modify: `scripts/stage-verified-promotion.ts`
|
||||
- Modify: `.gitea/workflows/quality-gates.yml`
|
||||
- Modify: `tests/unit/ci-workflow-generation.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: finalizer step outputs `staging_root` and `cleanup_token`.
|
||||
- Produces: setup, three downloads, finalizer, immediate non-`always()` exact-five upload, and `always()` cleanup ordering.
|
||||
|
||||
- [ ] **Step 1: Write/update failing workflow assertions** that reject standalone extraction, stable staging paths, missing `--ignore-scripts`, upload indirection, or cleanup ordering drift.
|
||||
- [ ] **Step 2: Run RED:** `node scripts/generate-ci-workflow.ts --check` and the workflow snapshot test.
|
||||
- [ ] **Step 3: Update the CI contract/config and regenerate YAML** with the finalizer output path and cleanup environment.
|
||||
- [ ] **Step 4: Run GREEN:** require workflow byte check and snapshot test PASS.
|
||||
|
||||
### Task 4: Full verification and durable report
|
||||
|
||||
**Files:**
|
||||
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/task-3-report.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: focused finalizer, provider, workflow, type, and lint evidence.
|
||||
- Produces: durable RED/GREEN evidence and a commit-ready report without overclaiming platform handoff guarantees.
|
||||
|
||||
- [ ] **Step 1: Run verification:** focused finalizer/provider tests, `check:supply-chain:provider-fixtures`, workflow `--check`, `check:types`, and `lint`.
|
||||
- [ ] **Step 2: Append exact RED/GREEN commands and outcomes** to the task report, including the remaining Gitea upload and `renameat2` limitations.
|
||||
- [ ] **Step 3: Inspect diff/status** and report completion before committing.
|
||||
@@ -0,0 +1,572 @@
|
||||
# Test Assurance Frontend Capability 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:** Extend `test-assurance-platform` so it is the sole authority for selecting, planning, executing, normalizing, evidencing, and assessing every frontend product suite, including tests bound to an immutable static-site candidate.
|
||||
|
||||
**Architecture:** The platform keeps v2 JVM documents readable, emits v3 documents for new work, and requires an explicit `SOURCE` or `ARTIFACT` phase plus exact input-artifact identities throughout the execution/evidence chain. A normalizer registry dispatches strict report adapters by capability. Each frontend capability has an independent catalog contract and readiness card; no aggregate frontend readiness is introduced.
|
||||
|
||||
**Tech Stack:** Java 21, Gradle Kotlin DSL, Jackson, JSON Schema 2020-12, Node.js 24.14.0, pnpm 11.17.0, TypeScript 7.0.2, Vitest 4.1.10, Playwright 1.62.0.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Repository: `/home/donghyeon/workspace/desktop-server-git/test-assurance-platform`; every command runs from its isolated worktree root.
|
||||
- Test assurance owns test meaning; it does not create Gitea jobs, allocate remote runners, publish builds, or promote releases.
|
||||
- New frontend suites require `test-assurance.platform/v3`; there is no implicit phase or host-toolchain fallback.
|
||||
- `requiredInputArtifacts` is an ordered canonical list of `{artifactId, mediaType, sha256}`. Paths are execution-local data and never artifact identity.
|
||||
- v2 and v3 evidence cannot be bundled or assessed together.
|
||||
- Missing, empty, oversized, malformed, symlinked, mismatched, zero-discovery, all-skipped, or retry-only-green evidence fails closed.
|
||||
- The exact capability IDs are `unit-typescript-vitest`, `component-react-vitest`, `integration-http-msw`, `architecture-typescript`, `coverage-v8`, `e2e-playwright-chromium`, `e2e-playwright-firefox`, `e2e-playwright-webkit`, `accessibility-web`, and `visual-regression-web`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Introduce the v3 artifact-bound execution identity
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/ApiVersion.java`
|
||||
- Create: `modules/assurance-model/src/main/java/io/testassurance/model/ExecutionPhase.java`
|
||||
- Create: `modules/assurance-model/src/main/java/io/testassurance/model/InputArtifact.java`
|
||||
- Create: `modules/assurance-model/src/main/java/io/testassurance/model/ArtifactInputDeclaration.java`
|
||||
- Create: `modules/assurance-model/src/main/java/io/testassurance/model/ArtifactSuiteTemplate.java`
|
||||
- Create: `modules/assurance-model/src/main/java/io/testassurance/model/InputArtifactMap.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/SuiteDefinition.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/WorkItem.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/ExecutionRequest.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/ExecutionPlan.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/RawSuiteResult.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/RawResultSet.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/NormalizedSuiteResult.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/EvidenceBundle.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/Assessment.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/RepositoryManifest.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/CompiledPolicy.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/json/JsonReader.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/json/JsonWriter.java`
|
||||
- Create: `modules/assurance-model/src/main/java/io/testassurance/model/LegacyV2Decoder.java`
|
||||
- Modify: `modules/assurance-model/src/test/java/io/testassurance/model/ModelRoundTripTest.java`
|
||||
- Modify: `modules/assurance-model/src/test/java/io/testassurance/model/RepositoryFixtures.java`
|
||||
- Preserve unchanged: all 26 current v2 files under `schemas/*.schema.json`
|
||||
- Create: 27 complete v3 wire schemas under `schemas/v3/`, one for every current kind plus `artifact-suite-template.schema.json`
|
||||
- Create: `schemas/v3/input-artifact-map.schema.json` and register it as an execution-only v3 kind
|
||||
- Modify: `modules/assurance-schema/src/main/java/io/testassurance/schema/SchemaRegistry.java`
|
||||
- Modify: `modules/assurance-schema/src/test/java/io/testassurance/schema/SchemaRegistryTest.java`
|
||||
- Modify: `tools/validate_package.py`
|
||||
- Modify: `machine/example-schema-map.json`
|
||||
- Modify: `machine/adversarial-schema-map.json`
|
||||
- Create: `examples/v3/artifact-suite-template.json`
|
||||
- Create: `examples/v3/input-artifact-map.json`
|
||||
- Modify: `modules/assurance-schema/src/main/java/io/testassurance/schema/SchemaRegistry.java`
|
||||
- Modify: `modules/assurance-schema/src/test/java/io/testassurance/schema/SchemaRegistryTest.java`
|
||||
- Create: `conformance/adversarial/v3-artifact-suite-missing-input.json`
|
||||
- Create: `conformance/adversarial/v3-source-suite-with-input.json`
|
||||
- Create: `conformance/adversarial/v2-v3-evidence-mix.json`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```java
|
||||
public enum ExecutionPhase { SOURCE, ARTIFACT }
|
||||
|
||||
public record InputArtifact(String artifactId, String mediaType, String sha256) {
|
||||
public InputArtifact {
|
||||
Identifier.require(artifactId, "inputArtifact.artifactId");
|
||||
Values.requireText(mediaType, "inputArtifact.mediaType");
|
||||
Digests.require(sha256, "inputArtifact.sha256");
|
||||
}
|
||||
}
|
||||
|
||||
public record ArtifactInputDeclaration(String artifactId, String mediaType) {}
|
||||
|
||||
public record ArtifactSuiteTemplate(
|
||||
String id,
|
||||
String capabilityId,
|
||||
List<String> command,
|
||||
int caseTimeoutSeconds,
|
||||
int suiteTimeoutSeconds,
|
||||
List<String> requiredArtifacts,
|
||||
List<String> environmentAllowlist,
|
||||
List<ArtifactInputDeclaration> requiredInputArtifacts,
|
||||
Optional<ObjectNode> parameters) {}
|
||||
|
||||
public final class ApiVersion {
|
||||
public static final String V2 = "test-assurance.platform/v2";
|
||||
public static final String V3 = "test-assurance.platform/v3";
|
||||
public static final String CURRENT = V3;
|
||||
}
|
||||
```
|
||||
|
||||
All nine execution-chain records expose `apiVersion()`, `executionPhase()`, and `requiredInputArtifacts()`; `SOURCE` requires an empty input list and `ARTIFACT` requires at least one item. `JsonWriter.envelope(String kind, String apiVersion)` requires an explicit version. Existing `toJson()` methods remain v2-compatible; every new v3 output path calls `toJson(ApiVersion.V3)`. `LegacyV2Decoder` reads the complete v2 repository graph explicitly and ordinary v3 readers never guess a phase. `ExecutionRequest.applicationArtifactDigest` exists only in the frozen v2 decoder and is absent from v3.
|
||||
|
||||
In v3, `RawSuiteResult.artifacts` is `List<ArtifactReference>` rather than path strings. The executor computes each digest immediately after the child exits and before results become visible to normalization. This makes raw artifact identity, normalized references, and evidence chain equality directly verifiable.
|
||||
|
||||
The committed repository cannot know a future candidate digest. Therefore the approved design amendment adds `RepositoryManifest.artifactSuiteTemplateFiles`: those files declare only artifact ID and media type and are not executable wire `SuiteDefinition` documents. During artifact planning, testctl matches them to concrete `ExecutionRequest.requiredInputArtifacts` and materializes v3 `SuiteDefinition`/`WorkItem` values containing the actual SHA-256. The compiler rejects a digest or digest placeholder in a template. `LegacyV2Decoder` is the only API that maps an approved v2 JVM graph into the legacy SOURCE compatibility path.
|
||||
|
||||
- [ ] Add model and schema tests for valid SOURCE v3, valid materialized ARTIFACT v3, absent phase, blank digest, duplicate artifact ID, SOURCE with artifacts, ARTIFACT without artifacts, a template containing any SHA field, v2 JVM graph acceptance, v2 frontend rejection, v2/v3 evidence mixing rejection, and byte-identical frozen v2 schema files.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
./gradlew :modules:assurance-model:test :modules:assurance-schema:test
|
||||
```
|
||||
|
||||
Expected: RED because `ExecutionPhase`, `InputArtifact`, explicit-version envelopes, and the v3 schema registry do not exist.
|
||||
|
||||
- [ ] Implement explicit-version codecs and a complete v3 schema set. `SchemaRegistry` keys every schema by `(apiVersion, kind)`. `tools/validate_package.py` validates 26 v2 wire schemas, 28 v3 wire/execution schemas, and 11 report schemas as separate inventories rather than one hard-coded total.
|
||||
- [ ] Run `./gradlew :modules:assurance-model:test :modules:assurance-schema:test`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add modules/assurance-model modules/assurance-schema schemas conformance/adversarial tools/validate_package.py machine examples/v3
|
||||
git commit -m "feat(contracts): add artifact-bound execution v3"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Propagate phase and input identity through compile, selection, and planning
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `modules/assurance-compiler/src/main/java/io/testassurance/compiler/PolicyCompiler.java`
|
||||
- Modify: `modules/assurance-compiler/src/test/java/io/testassurance/compiler/PolicyCompilerTest.java`
|
||||
- Modify: `modules/assurance-selector/src/main/java/io/testassurance/selector/ChangeSelector.java`
|
||||
- Modify: `modules/assurance-selector/src/test/java/io/testassurance/selector/ChangeSelectorTest.java`
|
||||
- Modify: `modules/assurance-plan/src/main/java/io/testassurance/plan/PlanEngine.java`
|
||||
- Modify: `modules/assurance-plan/src/main/java/io/testassurance/plan/ResourceProfiles.java`
|
||||
- Modify: `modules/assurance-plan/src/test/java/io/testassurance/plan/PlanEngineTest.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/CompiledPolicy.java`
|
||||
- Modify: `modules/assurance-model/src/main/java/io/testassurance/model/SelectionResult.java`
|
||||
|
||||
**Contract:** The compiler rejects any new frontend capability on v2. The selector preserves the suite phase without changing selected obligations. The planner copies the exact ordered artifact tuple from the request into every selected ARTIFACT work item and rejects a request whose tuple differs from the suite declaration. Plan digest calculation includes phase and artifact tuple.
|
||||
|
||||
`PolicyCompiler` stores source `SuiteDefinition` values and artifact `ArtifactSuiteTemplate` values separately. `PlanEngine` exposes `materializeArtifactSuite(ArtifactSuiteTemplate, ExecutionRequest)`; it requires an exact one-to-one match on artifact ID/media type, supplies the request SHA-256, and returns an executable v3 `SuiteDefinition`. No template or unresolved artifact can be serialized into `ExecutionPlan`.
|
||||
|
||||
- [ ] Add RED tests named `frontendCapabilityRequiresV3`, `artifactTemplateRejectsDigestPlaceholder`, `artifactTemplateMaterializesConcreteSuite`, `selectorPreservesExecutionPhase`, `artifactPlanRejectsWrongCandidateDigest`, `sourcePlanRejectsArtifactInput`, and `planDigestChangesWithArtifactDigest`.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
./gradlew :modules:assurance-compiler:test :modules:assurance-selector:test :modules:assurance-plan:test
|
||||
```
|
||||
|
||||
Expected: RED because the current compiler and planner do not carry phase/input identity.
|
||||
|
||||
- [ ] Implement exact propagation. Do not infer phase from capability name or from `applicationArtifactDigest`.
|
||||
- [ ] Run `./gradlew :modules:assurance-compiler:test :modules:assurance-selector:test :modules:assurance-plan:test`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add modules/assurance-compiler modules/assurance-selector modules/assurance-plan modules/assurance-model
|
||||
git commit -m "feat(plan): preserve source and artifact execution identity"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Register the frontend capability family and immutable toolchains
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `machine/capability-catalog.json`
|
||||
- Modify: `machine/toolchain-catalog.json`
|
||||
- Modify: `gradle/libs.versions.toml`
|
||||
- Modify: `modules/assurance-catalog/src/main/java/io/testassurance/catalog/CapabilityCatalog.java`
|
||||
- Modify: `modules/assurance-catalog/src/main/java/io/testassurance/catalog/CapabilityContract.java`
|
||||
- Modify: `modules/assurance-catalog/src/main/java/io/testassurance/catalog/ToolchainCatalog.java`
|
||||
- Create: `modules/assurance-catalog/src/main/java/io/testassurance/catalog/FrontendToolchainInspector.java`
|
||||
- Create: `modules/assurance-catalog/src/test/java/io/testassurance/catalog/FrontendToolchainInspectorTest.java`
|
||||
- Modify: `modules/assurance-catalog/src/test/java/io/testassurance/catalog/CatalogsTest.java`
|
||||
- Modify: `modules/assurance-catalog/src/test/java/io/testassurance/catalog/ReadinessCardsTest.java`
|
||||
- Modify: `modules/assurance-plan/src/main/java/io/testassurance/plan/ResourceProfiles.java`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
- Modify: `modules/testctl-cli/src/test/java/io/testassurance/cli/TestctlMainTest.java`
|
||||
- Modify: `tools/validate_package.py`
|
||||
- Modify: `docs/04-test-type-contracts.md`
|
||||
- Modify: `README.md`
|
||||
- Create: `readiness/unit-typescript-vitest.json`
|
||||
- Create: `readiness/component-react-vitest.json`
|
||||
- Create: `readiness/integration-http-msw.json`
|
||||
- Create: `readiness/architecture-typescript.json`
|
||||
- Create: `readiness/coverage-v8.json`
|
||||
- Modify: `readiness/e2e-playwright-chromium.json`
|
||||
- Create: `readiness/e2e-playwright-firefox.json`
|
||||
- Create: `readiness/e2e-playwright-webkit.json`
|
||||
- Create: `readiness/accessibility-web.json`
|
||||
- Create: `readiness/visual-regression-web.json`
|
||||
|
||||
**Catalog entries:** The catalog grows from 16 to 25 capabilities: nine new entries plus the upgraded existing Chromium entry form the ten-capability frontend family. Every capability declares contract revision v3, execution phase, required raw artifact IDs, case/suite timeout bounds, isolation/resource claims, maximum evidence age, false-green rule IDs, and minimum readiness. Toolchain entries pin version plus observed SHA-256 for Node.js 24.14.0, pnpm 11.17.0, TypeScript 7.0.2, Vitest 4.1.10, Playwright 1.62.0, and independent Chromium/Firefox/WebKit payload IDs.
|
||||
|
||||
`FrontendToolchainInspector` reads `.nvmrc`, `package.json.packageManager`, exact devDependency versions, and the root importer in `pnpm-lock.yaml`. It rejects semver ranges, workspace/host fallbacks, missing artifact digests, and lock resolution drift with `TA-LOCK-002`/`TA-LOCK-003`. `Commands.lock()` uses this inspector for any selected frontend capability.
|
||||
|
||||
| Capability | Phase | Required raw artifacts |
|
||||
|---|---|---|
|
||||
| `unit-typescript-vitest` | SOURCE | JUnit XML, Vitest discovery JSON |
|
||||
| `component-react-vitest` | SOURCE | JUnit XML, Vitest discovery JSON |
|
||||
| `integration-http-msw` | SOURCE | JUnit XML, Vitest discovery JSON, typed HTTP scenario receipts |
|
||||
| `architecture-typescript` | SOURCE | TypeScript architecture graph/violations JSON |
|
||||
| `coverage-v8` | SOURCE | V8 coverage summary, production-module inventory |
|
||||
| `e2e-playwright-chromium` | ARTIFACT | Playwright report, provider identity, trace/screenshot/console/network indexes, mutation receipts |
|
||||
| `e2e-playwright-firefox` | ARTIFACT | Playwright report, provider identity, trace/screenshot/console/network indexes, mutation receipts |
|
||||
| `e2e-playwright-webkit` | ARTIFACT | Playwright report, provider identity, trace/screenshot/console/network indexes, mutation receipts |
|
||||
| `accessibility-web` | ARTIFACT | Playwright report, provider identity, accessibility findings, manual-review records |
|
||||
| `visual-regression-web` | ARTIFACT | Playwright report, provider identity, baseline identity, image-diff results |
|
||||
|
||||
- [ ] Add tests that require exactly 25 catalog capabilities and one readiness card per capability, and fail on a missing digest, Node 22 fallback, semver range, pnpm lock drift, capability aliasing, a combined browser readiness card, or missing Firefox/WebKit resource profile.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
./gradlew :modules:assurance-catalog:test :modules:assurance-plan:test
|
||||
python3 tools/validate_package.py
|
||||
```
|
||||
|
||||
Expected: RED because the capabilities and toolchains are absent.
|
||||
|
||||
- [ ] Add independent catalog/resource entries and keep all nine new cards plus Chromium at R0 with explicit `nonGuarantees`. R1 is set only in Task 10 after real adapter and conformance evidence exists.
|
||||
- [ ] Run `./gradlew :modules:assurance-catalog:test :modules:assurance-plan:test` and `python3 tools/validate_package.py`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add machine readiness gradle/libs.versions.toml modules/assurance-catalog modules/assurance-plan modules/testctl-cli tools/validate_package.py docs/04-test-type-contracts.md README.md
|
||||
git commit -m "feat(catalog): register frontend assurance capabilities"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Replace hard-coded JUnit normalization with a strict registry
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `modules/assurance-normalizer/src/main/java/io/testassurance/normalizer/SuiteResultNormalizer.java`
|
||||
- Modify: `modules/assurance-normalizer/src/main/java/io/testassurance/normalizer/NormalizationContext.java`
|
||||
- Create: `modules/assurance-normalizer/src/main/java/io/testassurance/normalizer/NormalizerRegistry.java`
|
||||
- Create: `modules/assurance-normalizer/src/main/java/io/testassurance/normalizer/NormalizationArtifactReader.java`
|
||||
- Create: `modules/assurance-normalizer/src/test/java/io/testassurance/normalizer/NormalizerRegistryTest.java`
|
||||
- Create: `modules/assurance-normalizer/src/test/java/io/testassurance/normalizer/NormalizationArtifactReaderTest.java`
|
||||
- Create: `modules/assurance-schema/src/main/java/io/testassurance/schema/ReportSchemaRegistry.java`
|
||||
- Create: `modules/assurance-schema/src/test/java/io/testassurance/schema/ReportSchemaRegistryTest.java`
|
||||
- Create report schemas under `schemas/reports/`: `vitest-discovery-report.schema.json`, `http-scenario-receipt-set.schema.json`, `v8-coverage-summary.schema.json`, `production-module-inventory.schema.json`, `typescript-architecture-report.schema.json`, `playwright-suite-report.schema.json`, `browser-provider-report.schema.json`, `browser-mutation-receipt-set.schema.json`, `accessibility-report.schema.json`, `accessibility-manual-review.schema.json`, `visual-regression-report.schema.json`
|
||||
- Modify: `modules/assurance-schema/build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
- Modify: `modules/testctl-cli/src/test/java/io/testassurance/cli/TestctlMainTest.java`
|
||||
- Modify: `modules/testctl-cli/build.gradle.kts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```java
|
||||
public interface SuiteResultNormalizer {
|
||||
String adapterId();
|
||||
Set<String> capabilityIds();
|
||||
NormalizedSuiteResult normalize(NormalizationContext context);
|
||||
}
|
||||
|
||||
public final class NormalizerRegistry {
|
||||
public NormalizerRegistry(Collection<SuiteResultNormalizer> normalizers);
|
||||
public SuiteResultNormalizer requireFor(String capabilityId);
|
||||
}
|
||||
|
||||
public final class NormalizationArtifactReader {
|
||||
public NormalizationArtifactReader(ReportSchemaRegistry schemas);
|
||||
public byte[] readRequired(Path outputRoot, String repositoryRelativePath, long maximumBytes);
|
||||
public JsonNode readRequiredJson(Path outputRoot, String repositoryRelativePath, long maximumBytes, ReportSchemaRegistry.ReportKind reportKind);
|
||||
public ArtifactReference reference(Path outputRoot, String repositoryRelativePath, String mediaType, long maximumBytes);
|
||||
}
|
||||
```
|
||||
|
||||
The registry rejects duplicate adapter IDs, duplicate capability ownership, and zero matches. `Commands.normalize()` asks the registry for exactly one adapter and no longer constructs `JunitXmlNormalizer` directly. The common reader uses `NOFOLLOW_LINKS`, checks a regular file before reading, bounds bytes, rejects empty or malformed UTF-8, canonicalizes repository-relative paths, and validates JSON against `ReportSchemaRegistry` before adapters inspect fields.
|
||||
|
||||
- [ ] Add RED tests for no match, two owners, stable registration order, adapter exception mapping, preservation of v3 input artifacts, path escape, symlink, empty file, oversized file, malformed UTF-8, unknown report schema, and `additionalProperties` rejection.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
./gradlew :modules:assurance-normalizer:test :modules:testctl-cli:test
|
||||
```
|
||||
|
||||
Expected: RED because dispatch is hard-coded.
|
||||
|
||||
- [ ] Implement registry wiring with explicit constructors in `testctl-cli`; do not use classpath scanning.
|
||||
- [ ] Run `./gradlew :modules:assurance-normalizer:test :modules:testctl-cli:test`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add modules/assurance-normalizer modules/assurance-schema modules/testctl-cli schemas/reports
|
||||
git commit -m "refactor(normalizer): dispatch by capability contract"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Normalize Vitest results and V8 production coverage
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `adapters/vitest/build.gradle.kts`
|
||||
- Create: `adapters/vitest/src/main/java/io/testassurance/adapter/vitest/VitestJunitNormalizer.java`
|
||||
- Create: `adapters/vitest/src/main/java/io/testassurance/adapter/vitest/VitestDiscoveryReport.java`
|
||||
- Create: `adapters/vitest/src/test/java/io/testassurance/adapter/vitest/VitestJunitNormalizerTest.java`
|
||||
- Create fixture directories: `adapters/vitest/src/test/resources/valid/`, `adapters/vitest/src/test/resources/zero-tests/`, `adapters/vitest/src/test/resources/all-skipped/`, `adapters/vitest/src/test/resources/retry-green/`, `adapters/vitest/src/test/resources/missing-discovery/`, `adapters/vitest/src/test/resources/malformed/`, `adapters/vitest/src/test/resources/oversized/`, `adapters/vitest/src/test/resources/symlink/`
|
||||
- Create: `adapters/v8-coverage/build.gradle.kts`
|
||||
- Create: `adapters/v8-coverage/src/main/java/io/testassurance/adapter/coverage/V8CoverageNormalizer.java`
|
||||
- Create: `adapters/v8-coverage/src/main/java/io/testassurance/adapter/coverage/ProductionModuleInventory.java`
|
||||
- Create: `adapters/v8-coverage/src/test/java/io/testassurance/adapter/coverage/V8CoverageNormalizerTest.java`
|
||||
- Create fixture directories: `adapters/v8-coverage/src/test/resources/valid/`, `adapters/v8-coverage/src/test/resources/missing-module/`, `adapters/v8-coverage/src/test/resources/zero-universe/`, `adapters/v8-coverage/src/test/resources/path-mismatch/`, `adapters/v8-coverage/src/test/resources/malformed/`, `adapters/v8-coverage/src/test/resources/oversized/`, `adapters/v8-coverage/src/test/resources/symlink/`
|
||||
- Modify: `settings.gradle.kts`
|
||||
- Modify: `build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
|
||||
**Module dependencies:** Both modules expose the model/normalizer APIs, implement report schema support, and are added to root `javaModules`. Vitest additionally depends on `:adapters:junit-gradle` to reuse secure XML parsing.
|
||||
|
||||
**Rules:** `VitestJunitNormalizer` owns only `unit-typescript-vitest` and `component-react-vitest`; it exports `VitestReportReader` for HTTP composition. `V8CoverageNormalizer` alone owns `coverage-v8`. Missing production module is `INCOMPLETE_DELETED_TEST`; non-empty inventory with zero counter-bearing modules is `INCOMPLETE_ZERO_TESTS`; summary/inventory count or digest mismatch is `INVALID_RESULT`; configured threshold miss is `FAIL_PRODUCT`.
|
||||
|
||||
- [ ] Write all adapter tests before implementation and confirm RED:
|
||||
|
||||
```bash
|
||||
./gradlew :adapters:vitest:test :adapters:v8-coverage:test
|
||||
```
|
||||
|
||||
- [ ] Implement bounded regular-file reads, fatal UTF-8, secure XML, canonical repository-relative paths, cross-file count reconciliation, and no symlink following.
|
||||
- [ ] Register the three capability owners in the CLI registry.
|
||||
- [ ] Re-run focused tests plus `:modules:testctl-cli:test`; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add adapters/vitest adapters/v8-coverage settings.gradle.kts build.gradle.kts modules/testctl-cli
|
||||
git commit -m "feat(normalizers): add vitest and v8 coverage evidence"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Normalize HTTP scenarios and TypeScript architecture
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `adapters/http-scenario/build.gradle.kts`
|
||||
- Create: `adapters/http-scenario/src/main/java/io/testassurance/adapter/http/HttpScenarioNormalizer.java`
|
||||
- Create: `adapters/http-scenario/src/test/java/io/testassurance/adapter/http/HttpScenarioNormalizerTest.java`
|
||||
- Create fixture directories: `adapters/http-scenario/src/test/resources/valid/`, `adapters/http-scenario/src/test/resources/missing-receipt/`, `adapters/http-scenario/src/test/resources/duplicate-receipt/`, `adapters/http-scenario/src/test/resources/unknown-scenario/`, `adapters/http-scenario/src/test/resources/status-mismatch/`, `adapters/http-scenario/src/test/resources/malformed/`
|
||||
- Create: `adapters/typescript-architecture/build.gradle.kts`
|
||||
- Create: `adapters/typescript-architecture/src/main/java/io/testassurance/adapter/architecture/TypeScriptArchitectureNormalizer.java`
|
||||
- Create: `adapters/typescript-architecture/src/test/java/io/testassurance/adapter/architecture/TypeScriptArchitectureNormalizerTest.java`
|
||||
- Create fixture directories: `adapters/typescript-architecture/src/test/resources/valid/`, `adapters/typescript-architecture/src/test/resources/empty-graph/`, `adapters/typescript-architecture/src/test/resources/unresolved-import/`, `adapters/typescript-architecture/src/test/resources/cycle/`, `adapters/typescript-architecture/src/test/resources/violation/`, `adapters/typescript-architecture/src/test/resources/path-escape/`, `adapters/typescript-architecture/src/test/resources/malformed/`
|
||||
- Modify: `settings.gradle.kts`
|
||||
- Modify: `build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
|
||||
**Module dependencies:** Both modules are added to root `javaModules`; HTTP depends on `:adapters:vitest`, and both depend on model, normalizer, and schema modules.
|
||||
|
||||
**Typed inputs:** `HttpScenarioNormalizer` is the sole owner of `integration-http-msw` and composes `VitestReportReader`. It compares exact declared/executed IDs; a missing receipt is `INCOMPLETE_DELETED_TEST`, duplicate/unknown receipt or catalog mismatch is `INVALID_RESULT`, and unmatched/catch-all behavior is `FAIL_TEST`. Architecture requires non-empty modules/dependencies; empty graph is `INCOMPLETE_ZERO_TESTS`, malformed/parser mismatch is `ERROR_TOOLING` or `INVALID_RESULT`, and unresolved import/cycle/rule violation is `FAIL_PRODUCT`.
|
||||
|
||||
- [ ] Add fixtures/tests and run focused Gradle tests; expected RED.
|
||||
- [ ] Implement bounded `ReportSchemaRegistry` parsing, exact scenario/graph cross-field reconciliation, and sole capability ownership for both adapters.
|
||||
- [ ] Re-run focused tests and CLI tests; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add adapters/http-scenario adapters/typescript-architecture settings.gradle.kts build.gradle.kts modules/testctl-cli
|
||||
git commit -m "feat(normalizers): add http and architecture evidence"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Upgrade Playwright into three independent browser capabilities
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `adapters/playwright/src/main/java/io/testassurance/adapter/playwright/PlaywrightResultNormalizer.java`
|
||||
- Create: `adapters/playwright/src/main/java/io/testassurance/adapter/playwright/BrowserArtifactIndex.java`
|
||||
- Create: `adapters/playwright/src/main/java/io/testassurance/adapter/playwright/MutationReceipt.java`
|
||||
- Modify: `adapters/playwright/src/main/java/io/testassurance/adapter/playwright/BrowserRunPolicy.java`
|
||||
- Create: `adapters/playwright/src/test/java/io/testassurance/adapter/playwright/PlaywrightResultNormalizerTest.java`
|
||||
- Create fixture directories: `adapters/playwright/src/test/resources/chromium/`, `adapters/playwright/src/test/resources/firefox/`, `adapters/playwright/src/test/resources/webkit/`, `adapters/playwright/src/test/resources/missing-project/`, `adapters/playwright/src/test/resources/retry-green/`, `adapters/playwright/src/test/resources/zero-tests/`, `adapters/playwright/src/test/resources/missing-trace/`, `adapters/playwright/src/test/resources/console-error/`, `adapters/playwright/src/test/resources/network-error/`, `adapters/playwright/src/test/resources/valid-mutation/`, `adapters/playwright/src/test/resources/missing-mutation-read/`, `adapters/playwright/src/test/resources/malformed/`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
|
||||
**Rules:** The selected browser capability must match the report project exactly. Required trace/screenshot/console/network indexes bind repository-relative files by digest. Retry-only pass is FLAKY, not PASS. Write journeys require observed response, mutation read, and reload read receipts bound to the same scenario/candidate.
|
||||
|
||||
- [ ] Add RED tests for each browser and every false-green case.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
./gradlew :adapters:playwright:test
|
||||
```
|
||||
|
||||
Expected: RED because only policy helpers exist.
|
||||
|
||||
- [ ] Implement one normalizer parameterized by three independently registered capability IDs; do not aggregate readiness or results.
|
||||
- [ ] Run focused and CLI tests; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add adapters/playwright modules/testctl-cli
|
||||
git commit -m "feat(playwright): normalize independent browser evidence"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Add accessibility and visual-regression evidence
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `adapters/accessibility-web/build.gradle.kts`
|
||||
- Create: `adapters/accessibility-web/src/main/java/io/testassurance/adapter/accessibility/AccessibilityNormalizer.java`
|
||||
- Create: `adapters/accessibility-web/src/test/java/io/testassurance/adapter/accessibility/AccessibilityNormalizerTest.java`
|
||||
- Create fixture directories: `adapters/accessibility-web/src/test/resources/valid/`, `adapters/accessibility-web/src/test/resources/violation/`, `adapters/accessibility-web/src/test/resources/missing-provider/`, `adapters/accessibility-web/src/test/resources/missing-manual-review/`, `adapters/accessibility-web/src/test/resources/expired-review/`, `adapters/accessibility-web/src/test/resources/malformed/`
|
||||
- Create: `adapters/visual-regression-web/build.gradle.kts`
|
||||
- Create: `adapters/visual-regression-web/src/main/java/io/testassurance/adapter/visual/VisualRegressionNormalizer.java`
|
||||
- Create: `adapters/visual-regression-web/src/test/java/io/testassurance/adapter/visual/VisualRegressionNormalizerTest.java`
|
||||
- Create fixture directories: `adapters/visual-regression-web/src/test/resources/valid/`, `adapters/visual-regression-web/src/test/resources/diff/`, `adapters/visual-regression-web/src/test/resources/missing-baseline/`, `adapters/visual-regression-web/src/test/resources/wrong-baseline-digest/`, `adapters/visual-regression-web/src/test/resources/missing-provider/`, `adapters/visual-regression-web/src/test/resources/malformed/`
|
||||
- Modify: `settings.gradle.kts`
|
||||
- Modify: `build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
|
||||
**Module dependencies:** Both modules are added to root `javaModules` and depend on model, normalizer, schema, and Playwright evidence types.
|
||||
|
||||
**Rules:** Accessibility evidence carries scanner/provider identity, rule-set digest, findings, and required manual-review records. Manual records require `reviewedAt`, `expiresAt`, `sourceRevision`, `candidateSha256`, `reviewerId`, and `rulesetDigest`; expiry is evaluated against the raw result's `finishedAt`, never wall-clock time. Visual evidence carries baseline ID/digest, provider identity, current image digest, diff digest, and threshold outcome. Absent identities can never PASS.
|
||||
|
||||
- [ ] Add RED tests `rejectsAccessibilityWithoutProvider`, `rejectsExpiredManualReviewAtFinishedAt`, `reportsUnwaivedAccessibilityViolation`, `rejectsVisualWithoutBaseline`, `rejectsWrongVisualProvider`, and `reportsVisualDiffOverThreshold`, each backed by the named fixture directory above.
|
||||
- [ ] Implement and register both adapters.
|
||||
- [ ] Run adapter and CLI tests; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add adapters/accessibility-web adapters/visual-regression-web settings.gradle.kts build.gradle.kts modules/testctl-cli
|
||||
git commit -m "feat(normalizers): add accessibility and visual evidence"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Enforce artifact materialization in bounded execution
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `modules/assurance-executor/src/main/java/io/testassurance/executor/WorkItemExecutor.java`
|
||||
- Modify: `modules/assurance-executor/src/main/java/io/testassurance/executor/LocalProcessExecutor.java`
|
||||
- Create: `modules/assurance-executor/src/main/java/io/testassurance/executor/InputArtifactVerifier.java`
|
||||
- Modify: `modules/assurance-executor/src/test/java/io/testassurance/executor/LocalProcessExecutorTest.java`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
- Modify: `modules/testctl-cli/src/test/java/io/testassurance/cli/TestctlMainTest.java`
|
||||
- Modify: `docs/06-testctl-external-executor-contract.md`
|
||||
|
||||
**External contract:** `testctl execute-one` retains `--plan`, `--work-item-id`, and `--output`, and v3 adds `--artifact-map`. The artifact map resolves artifact IDs to local regular archive files, while expected media type and SHA-256 come only from the signed v3 work item. Verification occurs before the product command starts.
|
||||
|
||||
The compatible public syntax is:
|
||||
|
||||
```text
|
||||
testctl execute-one --plan <plan.json> --work-item-id <sha256> --artifact-map <input-artifact-map.json> --output <work-dir>
|
||||
testctl execute-plan --plan <plan.json> --artifact-map <input-artifact-map.json> --output <run-dir>
|
||||
```
|
||||
|
||||
SOURCE plans reject `--artifact-map`; ARTIFACT plans require it. V3 maps only regular archive files—directories must already be represented by a canonical archive subject. `InputArtifactVerifier` copies each file into a private staging directory while hashing it, then exports only the verified copy path to the child.
|
||||
|
||||
- [ ] Add RED tests for missing artifact, wrong digest, wrong media type, symlink, path escape, directory input, mutation between verification and use, SOURCE receiving an artifact map, ARTIFACT missing a map, unknown work-item ID, and work item not belonging to the plan digest.
|
||||
- [ ] Run executor and CLI tests; expected RED.
|
||||
- [ ] Implement verification with opened handles or verified private copies, bounded hashing, and exact environment projection; never trust a caller-supplied digest.
|
||||
- [ ] Re-run tests; expected PASS.
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add modules/assurance-executor modules/testctl-cli docs/06-testctl-external-executor-contract.md
|
||||
git commit -m "feat(executor): verify artifact-bound work inputs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Prove the v3 chain, frontend fixture, and independent R1 readiness
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `modules/assurance-normalizer/src/main/java/io/testassurance/normalizer/FalseGreenRules.java`
|
||||
- Modify: `modules/assurance-evidence/src/main/java/io/testassurance/evidence/EvidenceBundler.java`
|
||||
- Modify: `modules/assurance-evidence/src/test/java/io/testassurance/evidence/EvidenceBundlerTest.java`
|
||||
- Modify: `modules/assurance-assessor/src/main/java/io/testassurance/assessor/Assessor.java`
|
||||
- Modify: `modules/assurance-assessor/src/test/java/io/testassurance/assessor/AssessorTest.java`
|
||||
- Create: `modules/assurance-catalog/src/main/java/io/testassurance/catalog/ReadinessCatalog.java`
|
||||
- Create: `modules/assurance-catalog/src/test/java/io/testassurance/catalog/ReadinessCatalogTest.java`
|
||||
- Modify: `modules/assurance-catalog/build.gradle.kts`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/ConformanceRunner.java`
|
||||
- Modify: `modules/testctl-cli/src/main/java/io/testassurance/cli/Commands.java`
|
||||
- Modify: `modules/testctl-cli/src/test/java/io/testassurance/cli/TestctlMainTest.java`
|
||||
- Create: `conformance/golden-v3/source/case.json`
|
||||
- Create: `conformance/golden-v3/source/01-execution-request.json`
|
||||
- Create: `conformance/golden-v3/source/02-execution-plan.json`
|
||||
- Create: `conformance/golden-v3/source/03-raw-result-set.json`
|
||||
- Create: `conformance/golden-v3/source/normalized.json`
|
||||
- Create: `conformance/golden-v3/source/04-evidence-bundle.json`
|
||||
- Create: `conformance/golden-v3/source/05-assessment.json`
|
||||
- Create: `conformance/golden-v3/artifact/case.json`
|
||||
- Create: `conformance/golden-v3/artifact/01-execution-request.json`
|
||||
- Create: `conformance/golden-v3/artifact/02-execution-plan.json`
|
||||
- Create: `conformance/golden-v3/artifact/03-raw-result-set.json`
|
||||
- Create: `conformance/golden-v3/artifact/normalized.json`
|
||||
- Create: `conformance/golden-v3/artifact/04-evidence-bundle.json`
|
||||
- Create: `conformance/golden-v3/artifact/05-assessment.json`
|
||||
- Create: `conformance/adversarial/v3-wrong-candidate-normalized-result.json`
|
||||
- Create: `conformance/adversarial/v3-missing-browser-result.json`
|
||||
- Create: `fixtures/frontend-reference/test-assurance.yaml`
|
||||
- Create: `fixtures/frontend-reference/test-assurance.lock.json`
|
||||
- Create: `fixtures/frontend-reference/.nvmrc`
|
||||
- Create: `fixtures/frontend-reference/package.json`
|
||||
- Create: `fixtures/frontend-reference/pnpm-lock.yaml`
|
||||
- Create: `fixtures/frontend-reference/config/test-assurance/risks/frontend.json`
|
||||
- Create: `fixtures/frontend-reference/config/test-assurance/obligations/frontend.json`
|
||||
- Create: `fixtures/frontend-reference/config/test-assurance/suites/source.json`
|
||||
- Create: `fixtures/frontend-reference/config/test-assurance/suites/artifact-templates.json`
|
||||
- Create: `fixtures/frontend-reference/config/test-assurance/change-surfaces.json`
|
||||
- Create: `fixtures/frontend-reference/artifacts/frontend-static.tar`
|
||||
- Create: `fixtures/frontend-reference/results/source-valid/raw-result-set.json`
|
||||
- Create: `fixtures/frontend-reference/results/source-valid/normalized.json`
|
||||
- Create: `fixtures/frontend-reference/results/artifact-valid/raw-result-set.json`
|
||||
- Create: `fixtures/frontend-reference/results/artifact-valid/normalized.json`
|
||||
- Create: `fixtures/frontend-reference/results/adversarial-zero-discovery/raw-result-set.json`
|
||||
- Create: `fixtures/frontend-reference/results/adversarial-missing-browser/raw-result-set.json`
|
||||
- Create: `fixtures/frontend-reference/results/adversarial-wrong-candidate/raw-result-set.json`
|
||||
- Modify: all ten frontend files under `readiness/`
|
||||
- Modify: `docs/03-manifest-schema-contracts.md`
|
||||
- Modify: `docs/04-test-type-contracts.md`
|
||||
- Modify: `docs/06-testctl-external-executor-contract.md`
|
||||
- Modify: `docs/07-evidence-flaky-contract.md`
|
||||
- Modify: `docs/09-implementation-handoff-contract.md`
|
||||
- Modify: `README.md`
|
||||
- Modify: `validation.json`
|
||||
|
||||
**Identity rule:** Bundling requires exact equality of API major, request digest, plan digest, execution phase, ordered input artifacts, work-item IDs, attempt identities, and raw artifact digests. Assessment emits the same tuple plus `readinessDigest` and never converts an integrity error into an unsatisfied product obligation. `ReadinessCatalog` packages the independent cards into the distribution; `Commands.assess()` supplies it to `AssessmentInputs`, and the assessor applies the higher of capability-contract minimum and obligation minimum readiness.
|
||||
|
||||
- [ ] Add RED chain tests for v2/v3 mixing, source/artifact mixing, wrong candidate, missing work item, duplicate attempt, retry-only green, absent terminal result, empty readiness map, readiness digest drift, and an aggregate frontend readiness card.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
./gradlew :modules:assurance-evidence:test :modules:assurance-assessor:test :modules:assurance-catalog:test :modules:testctl-cli:test
|
||||
```
|
||||
|
||||
Expected: RED until the entire tuple is checked.
|
||||
|
||||
- [ ] Implement fail-closed chain verification, readiness binding, and a `ConformanceRunner` case format that validates the five canonical stages plus listed `normalizedFiles`.
|
||||
- [ ] Build the frontend fixture with all ten capabilities. Compute the artifact SHA from the committed `frontend-static.tar` bytes and use it consistently in artifact request, materialized suites, plan, raw results, normalized results, bundle, and assessment; no repeated-character or hand-entered digest is allowed.
|
||||
- [ ] Run focused tests; expected PASS.
|
||||
- [ ] Install the CLI and run both golden cases:
|
||||
|
||||
```bash
|
||||
./gradlew :modules:testctl-cli:installDist
|
||||
modules/testctl-cli/build/install/testctl-cli/bin/testctl conformance --case conformance/golden-v3/source/case.json
|
||||
modules/testctl-cli/build/install/testctl-cli/bin/testctl conformance --case conformance/golden-v3/artifact/case.json
|
||||
```
|
||||
|
||||
- [ ] Run the fixture twice, once for SOURCE and once for ARTIFACT, through `validate → lock → compile → select → plan → normalize → bundle → assess`; assert five selected source capability IDs and five selected artifact capability IDs with separate plan/evidence/assessment digests.
|
||||
- [ ] Update each frontend card from R0 to R1 only when its own adapter fixture and chain evidence digest exists. Keep missing evidence at R0 with explicit `nonGuarantees`.
|
||||
- [ ] Add documentation checks naming every capability, exact toolchain pin, v3 CLI argument, phase/template/materialization rule, false-green rule, readiness digest, and v2 compatibility limit.
|
||||
- [ ] Run full verification:
|
||||
|
||||
```bash
|
||||
./gradlew clean build
|
||||
python3 tools/validate_package.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: PASS. If a toolchain artifact cannot be observed locally, keep affected cards at R0; do not fabricate a digest.
|
||||
|
||||
- [ ] Commit:
|
||||
|
||||
```bash
|
||||
git add modules/assurance-normalizer modules/assurance-evidence modules/assurance-assessor modules/assurance-catalog modules/testctl-cli conformance fixtures/frontend-reference readiness docs README.md validation.json
|
||||
git commit -m "test(conformance): publish frontend v3 readiness"
|
||||
```
|
||||
|
||||
## Handoff to CI/CD
|
||||
|
||||
The consuming CI/CD plan may begin only from the immutable commit produced by Task 11. It imports the v3 schemas and CLI distribution by digest, invokes only documented argument-array commands, treats normalized/evidence/assessment documents as opaque test semantics, and carries both source and artifact plan/evidence/assessment digests into release identity.
|
||||
@@ -0,0 +1,176 @@
|
||||
# V8 Coverage Counter Contract 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:** Publish risk-coverage artifact schema version 3 and continuously verify the installed Vitest/V8 producer's counter-bearing/counterless row semantics in an isolated child run.
|
||||
|
||||
**Architecture:** A real CLI contract test owns the serialized artifact assertion. A standalone producer checker copies fixed source templates into one OS-temp root, creates its child config and report there, validates an exact JSON summary, bounds child diagnostics, and removes the owned root in `finally`. `test:coverage` invokes the checker before repository coverage, which also carries it into CI and sample removal.
|
||||
|
||||
**Tech Stack:** TypeScript 7, Node.js 24 child processes and filesystem APIs, Vitest 4, V8 coverage.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Policy schema remains version 2; serialized risk-coverage artifact schema becomes version 3.
|
||||
- Child root, config, and reports directory are all below one owned OS temporary directory.
|
||||
- Main Vitest must not discover the child `.fixture.ts` file.
|
||||
- Child exit, summary absence, malformed/missing/additional rows, counterless nonzero drift, and runtime all-zero drift fail closed.
|
||||
- Child stdout/stderr included in diagnostics is bounded.
|
||||
- Cleanup uses `finally` and targets only the exact owned temporary root.
|
||||
- Source edits use `apply_patch`; behavior changes follow RED-GREEN TDD.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Version the real serialized artifact
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/unit/risk-coverage.test.ts`
|
||||
- Modify: `scripts/check-risk-coverage.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the real `check-risk-coverage.ts` CLI, current policy structure, and an exact temporary coverage summary.
|
||||
- Produces: serialized artifact schema version 3 with `counterBearingTotal`, `instrumentedCounterBearingTotal`, `counterlessTotal`, and `counterlessModules` only.
|
||||
|
||||
- [x] **Step 1: Add the actual CLI serialization contract test.**
|
||||
|
||||
Create a temporary repository with the 19 current policy paths, write each as `export const covered = true`, set the cloned policy baseline to 19, write one full counter row per module, run the CLI with `process.execPath`, and assert:
|
||||
|
||||
```ts
|
||||
expect(artifact).toMatchObject({
|
||||
schemaVersion: 3,
|
||||
counterBearingTotal: 19,
|
||||
instrumentedCounterBearingTotal: 19,
|
||||
counterlessTotal: 0,
|
||||
counterlessModules: [],
|
||||
});
|
||||
expect(artifact).not.toHaveProperty("executableTotal");
|
||||
expect(artifact).not.toHaveProperty("instrumentedExecutableTotal");
|
||||
expect(artifact).not.toHaveProperty("nonExecutableTotal");
|
||||
expect(artifact).not.toHaveProperty("nonExecutableModules");
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run the single test and verify RED.**
|
||||
|
||||
Run: `./node_modules/.bin/vitest run tests/unit/risk-coverage.test.ts -t "publishes artifact schema version 3" --reporter=dot`
|
||||
|
||||
Expected: FAIL because the actual artifact contains `schemaVersion: 2`.
|
||||
|
||||
- [x] **Step 3: Change only the serialized envelope to version 3.**
|
||||
|
||||
Change `schemaVersion: 2` to `schemaVersion: 3` in the value passed to `writeRiskCoverageArtifactAtomic`; do not change policy parsing.
|
||||
|
||||
- [x] **Step 4: Re-run the single test and verify GREEN.**
|
||||
|
||||
Run the Step 2 command and expect one passing test.
|
||||
|
||||
### Task 2: Lock actual Vitest/V8 counter semantics
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/counter-semantics.fixture.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/runtime.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/import-type-empty.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/import-empty.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/import-side-effect.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/import-value.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/reexport-named.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/reexport-star.ts`
|
||||
- Create: `tests/fixtures/v8-coverage-counter-semantics/src/type-only.ts`
|
||||
- Create: `tests/unit/v8-coverage-counter-semantics.test.ts`
|
||||
- Create: `scripts/lib/v8-coverage-counter-semantics.ts`
|
||||
- Create: `scripts/check-v8-coverage-counter-semantics.ts`
|
||||
- Modify: `vitest.config.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `assertV8CoverageCounterSemantics(summary, fixtureRoot)` and `checkV8CoverageCounterSemantics(options?)`.
|
||||
- Consumes: fixed fixture templates, owned temp paths, a shell-free Vitest child result, and `coverage-summary.json`.
|
||||
|
||||
- [x] **Step 1: Add fixture templates and failing checker tests.**
|
||||
|
||||
The fixture test imports the seven counterless modules and observes the direct/named/star runtime values. The unit tests use literal summaries to require exact rows and mutate them for missing row, extra row, counterless nonzero, and runtime all-zero failures. Runner tests inject child exit and successful-without-summary results and require bounded diagnostics plus removal of the owned root.
|
||||
|
||||
- [x] **Step 2: Run the new unit file and verify RED.**
|
||||
|
||||
Run: `./node_modules/.bin/vitest run tests/unit/v8-coverage-counter-semantics.test.ts --reporter=dot`
|
||||
|
||||
Expected: FAIL because `scripts/lib/v8-coverage-counter-semantics.ts` does not exist.
|
||||
|
||||
- [x] **Step 3: Implement exact summary validation and owned child execution.**
|
||||
|
||||
The default runner executes:
|
||||
|
||||
```ts
|
||||
execFile(process.execPath, [
|
||||
path.join(repositoryRoot, "node_modules/vitest/vitest.mjs"),
|
||||
"run",
|
||||
"--config",
|
||||
configPath,
|
||||
"--coverage",
|
||||
"--reporter=dot",
|
||||
"--no-color",
|
||||
], { cwd: ownedRoot, timeout: 30_000, maxBuffer: 256 * 1024 });
|
||||
```
|
||||
|
||||
The generated config has `root`, `include`, `coverage.reportsDirectory`, and `coverage.include` paths inside the owned root. Always remove the root in `finally`.
|
||||
|
||||
- [x] **Step 4: Add a behavioral main-discovery assertion.**
|
||||
|
||||
Run main `vitest list` filtered to the fixture directory with `--filesOnly --passWithNoTests`; require empty stdout. Add an explicit fixture-directory exclude in `vitest.config.ts`.
|
||||
|
||||
- [x] **Step 5: Run the new unit file and standalone checker for GREEN.**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
./node_modules/.bin/vitest run tests/unit/v8-coverage-counter-semantics.test.ts --reporter=dot
|
||||
node scripts/check-v8-coverage-counter-semantics.ts
|
||||
```
|
||||
|
||||
Expected checker output: `V8 coverage counter semantics: PASS (1 counter-bearing, 7 counterless)`.
|
||||
|
||||
### Task 3: Wire coverage/CI and refresh documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `docs/testing/frontend-platform-testing-strategy.md`
|
||||
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/task-1-report.md`
|
||||
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/progress.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `check:v8-coverage-counter-semantics` and existing FE-GATE-005 `test:coverage` step.
|
||||
- Produces: package/CI/sample-removal execution and current 19-module/80-threshold documentation.
|
||||
|
||||
- [x] **Step 1: Add the package checker and prepend it to `test:coverage`.**
|
||||
|
||||
```json
|
||||
"check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts",
|
||||
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run ..."
|
||||
```
|
||||
|
||||
- [x] **Step 2: Synchronize documentation.**
|
||||
|
||||
Replace stale `12개 high-risk module` and `52개 scoped threshold` with `19개` and `80개`; document artifact schema 3 and policy schema 2 separately.
|
||||
|
||||
- [x] **Step 3: Run full relevant verification.**
|
||||
|
||||
```sh
|
||||
./node_modules/.bin/vitest run tests/unit/risk-coverage.test.ts tests/unit/risk-coverage-files.test.ts tests/unit/v8-coverage-counter-semantics.test.ts tests/unit/bounded-body-reader.test.ts --reporter=dot
|
||||
./node_modules/.bin/tsc --noEmit -p tsconfig.node.json
|
||||
./node_modules/.bin/tsc --noEmit -p tsconfig.test.json
|
||||
./node_modules/.bin/eslint scripts/check-risk-coverage.ts scripts/check-v8-coverage-counter-semantics.ts scripts/lib/v8-coverage-counter-semantics.ts tests/unit/risk-coverage.test.ts tests/unit/v8-coverage-counter-semantics.test.ts vitest.config.ts --max-warnings=0
|
||||
corepack pnpm check:v8-coverage-counter-semantics
|
||||
node scripts/check-risk-coverage.ts
|
||||
corepack pnpm test:sample-removal
|
||||
git diff --check
|
||||
```
|
||||
|
||||
- [x] **Step 4: Commit the verified closeout.**
|
||||
|
||||
```sh
|
||||
git add package.json vitest.config.ts scripts/check-risk-coverage.ts scripts/check-v8-coverage-counter-semantics.ts scripts/lib/v8-coverage-counter-semantics.ts tests/fixtures/v8-coverage-counter-semantics tests/unit/risk-coverage.test.ts tests/unit/v8-coverage-counter-semantics.test.ts docs/testing/frontend-platform-testing-strategy.md docs/superpowers/plans/2026-08-02-v8-coverage-counter-contract.md
|
||||
git commit -m "test: lock V8 coverage counter semantics"
|
||||
```
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: artifact versioning, actual producer rows, discovery isolation, every fail-closed path, bounded diagnostics, cleanup, coverage/CI linkage, sample-removal preservation, and documentation counts are assigned.
|
||||
- Placeholder scan: no deferred implementation remains.
|
||||
- Type consistency: parser and runner names match in tests, script, and plan.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,712 @@
|
||||
# 어댑터 배럴 공개 경계 확립 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:** 어댑터 16개 그룹 전부에 `index.ts` 공개 경계를 세우고, 그룹 바깥은 배럴만 import하도록 게이트로 강제한다.
|
||||
|
||||
**Architecture:** 각 어댑터 그룹의 `index.ts`가 그 그룹의 공개 표면이 된다. 그룹 내부 파일끼리, 그리고 어댑터→커널(`platform/**`) 간선은 지금처럼 파일 직접 import를 유지한다 — 커널의 정체성은 파일이고 `check:adapter-inventory`가 그것을 파일 경로로 검증하기 때문이다. `.dependency-cruiser.json`에 규칙 하나를 추가하고 회귀 fixture로 그 거부를 고정한다.
|
||||
|
||||
**Tech Stack:** TypeScript (NodeNext/Bundler), dependency-cruiser, 자체 TypeScript 인지 import 그래프(`scripts/check-architecture.ts`), Vite, Vitest
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-09-16-adapter-barrel-boundary-design.md`](../specs/2026-09-16-adapter-barrel-boundary-design.md)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 기준선(2026-09-16, `develop` `5434760`): `check:architecture` PASS(297 모듈 / 897 의존 / 미해결 0), `check:types:app` PASS, `check:adapter-inventory` PASS(120 파일). **이 셋을 깨면 안 된다.**
|
||||
- `docs/reviews/adapters/INVENTORY.md`는 `git ls-files src/adapters`와 **집합이 정확히 일치**해야 한다. 소스 파일을 추가/이동하면 같은 커밋에서 이 표와 하단 합계를 고친다.
|
||||
- 어댑터→커널 import는 **파일 직접 경로를 유지**한다. `scripts/check-adapter-inventory.ts:63-83`이 4개 소비자에게 `platform/abortable-operation.ts`로 해석되는 specifier를 정규식으로 요구한다.
|
||||
- `src/adapters/storage/index.ts`는 `indexeddb/`·`opfs/` 서브배럴을 **재수출하지 않는다.** `scripts/test-browser-file-storage-runtime-removal.ts:23-34`가 그 두 폴더만 삭제한 뒤 잔존 import를 예외로 잡는다.
|
||||
- `src/adapters/service-worker/index.ts`는 `service-worker-entry.ts`를 참조하지 않는다. `tsconfig.app.json:18`이 제외한 파일이다.
|
||||
- 번들 예산: `config/performance/budgets.json`의 `bundle.initialJsGzipBytes = 204800`.
|
||||
- 프로젝트 소스는 TypeScript/TSX만. `allowJs` 비활성. 로컬 import는 명시적 `.ts`/`.tsx` 확장자 필수.
|
||||
- 커밋 메시지 끝에 붙일 것:
|
||||
`Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 배럴 8개 생성 + INVENTORY 등록
|
||||
|
||||
없는 8개 그룹에 `index.ts`를 만든다. 이 태스크는 **파일 추가만** 한다 — 기존 import는 건드리지 않으므로 런타임 동작이 바뀌지 않는다.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapters/auth/index.ts`
|
||||
- Create: `src/adapters/diagnostics/index.ts`
|
||||
- Create: `src/adapters/telemetry/index.ts`
|
||||
- Create: `src/adapters/platform/index.ts`
|
||||
- Create: `src/adapters/query-cache/index.ts`
|
||||
- Create: `src/adapters/service-worker/index.ts`
|
||||
- Create: `src/adapters/storage/index.ts`
|
||||
- Create: `src/adapters/http/index.ts`
|
||||
- Modify: `docs/reviews/adapters/INVENTORY.md` (행 8개 추가 + 합계)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: 위 8개 배럴이 내보내는 심볼 82개. Task 2가 이 경로들로 import를 바꾼다. 심볼 존재는 spec §7에서 82/82 기계 대조 완료(누락 0, 오타 0).
|
||||
- Consumes: 없음 (첫 태스크)
|
||||
|
||||
- [x] **Step 1: 배럴 5개 생성 (단일 파일 그룹 + query-cache)**
|
||||
|
||||
`src/adapters/auth/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createAnonymousSessionAdapter,
|
||||
createDemoSessionAdapter,
|
||||
createExternalAuthSessionAdapter,
|
||||
createUnavailableSessionAdapter,
|
||||
DEMO_AUTHORIZATION_MARKER,
|
||||
type DemoSessionAdapter,
|
||||
type ExternalSessionOwner,
|
||||
} from "./external-session-adapter.ts";
|
||||
```
|
||||
|
||||
`src/adapters/diagnostics/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createDiagnosticsAdapter,
|
||||
getLastBootEvidence,
|
||||
MAX_DIAGNOSTIC_ENTRIES,
|
||||
noOpDiagnostics,
|
||||
recordBootFailure,
|
||||
} from "./bounded-diagnostics.ts";
|
||||
```
|
||||
|
||||
`src/adapters/telemetry/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createTelemetryAdapter,
|
||||
MAX_TELEMETRY_QUEUE,
|
||||
noOpTelemetry,
|
||||
type TelemetryAdapter,
|
||||
type TelemetryAdapterOptions,
|
||||
} from "./best-effort-telemetry.ts";
|
||||
```
|
||||
|
||||
`src/adapters/storage/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* 최상위 storage 배럴은 Web Storage 어댑터만 내보낸다.
|
||||
*
|
||||
* `./indexeddb/index.ts`와 `./opfs/index.ts`를 여기서 재수출하지 말 것.
|
||||
* `scripts/test-browser-file-storage-runtime-removal.ts:23-34`가 그 두 폴더만
|
||||
* 삭제한 뒤 잔존 import를 예외로 잡는다 — 재수출하면 제거 드릴이 죽는다.
|
||||
* 두 서브배럴이 각자 제거 가능한 런타임의 경계다.
|
||||
*/
|
||||
export {
|
||||
createBrowserStorageAdapter,
|
||||
type BrowserStorageDependencies,
|
||||
} from "./browser-storage-adapter.ts";
|
||||
```
|
||||
|
||||
`src/adapters/query-cache/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createConditionalValidatorStore,
|
||||
type ConditionalValidatorBinding,
|
||||
type ConditionalValidatorStore,
|
||||
} from "./conditional-validator-store.ts";
|
||||
export { createCursorPaginationRuntime } from "./cursor-pagination-runtime.ts";
|
||||
export {
|
||||
createServerStateScopeRuntime,
|
||||
type ScopeResetParticipant,
|
||||
type ServerStateScopeDependencies,
|
||||
} from "./server-state-scope-runtime.ts";
|
||||
export {
|
||||
createTanStackCacheCoordinator,
|
||||
type TanStackCacheCoordinatorDependencies,
|
||||
} from "./tanstack-cache-coordinator.ts";
|
||||
export {
|
||||
createQueryCacheAdapter,
|
||||
createQueryClient,
|
||||
QUERY_CACHE_DEFAULTS,
|
||||
type QueryCacheDependencies,
|
||||
} from "./tanstack-query-cache.ts";
|
||||
```
|
||||
|
||||
- [x] **Step 2: 배럴 3개 생성 (주석이 계약인 것들)**
|
||||
|
||||
`src/adapters/platform/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* 어댑터 커널의 공개 경계.
|
||||
*
|
||||
* `src/adapters/**` 안에서는 이 배럴을 쓰지 않는다. 커널 프리미티브는 파일
|
||||
* 경로로 직접 import한다 — `scripts/check-adapter-inventory.ts:63-83`이
|
||||
* `platform/abortable-operation.ts`로 해석되는 specifier를 네 소비자에게
|
||||
* 요구하고, `.dependency-cruiser.json`의 kernel carve-out도 폴더 단위다.
|
||||
* 이 배럴은 bootstrap·features·tests 같은 그룹 바깥 소비자를 위한 문이다.
|
||||
*/
|
||||
export {
|
||||
compensateLateHandle,
|
||||
createAbortableOperation,
|
||||
snapshotAbortTimers,
|
||||
type AbortableOperation,
|
||||
type AbortableOperationInput,
|
||||
type AbortRace,
|
||||
type AbortTerminalReason,
|
||||
type AbortTimerSnapshot,
|
||||
} from "./abortable-operation.ts";
|
||||
export { assertBoundedCapacity } from "./bounded-capacity.ts";
|
||||
export {
|
||||
createBrowserLifecycleRuntime,
|
||||
type BrowserLifecycleEvent,
|
||||
type BrowserLifecycleRuntime,
|
||||
type BrowserLifecycleSnapshot,
|
||||
} from "./browser-lifecycle.ts";
|
||||
export {
|
||||
createBrowserMutationIntentFactory,
|
||||
type BrowserMutationIntentFactoryDependencies,
|
||||
} from "./browser-mutation-intent-factory.ts";
|
||||
export { systemClock } from "./system-clock.ts";
|
||||
```
|
||||
|
||||
`src/adapters/service-worker/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* `service-worker-entry.ts`는 여기서 참조하지 않는다. `tsconfig.app.json:18`이
|
||||
* 제외한 파일이라 참조하면 app 타입체크가 제외 대상을 끌어들인다. 그 파일은
|
||||
* export가 0개이므로 넣을 것도 없다.
|
||||
*/
|
||||
export {
|
||||
createServiceWorkerRuntime,
|
||||
type WorkerClientLike,
|
||||
type WorkerRuntimeConfig,
|
||||
type WorkerScopeLike,
|
||||
} from "./service-worker-lifecycle.ts";
|
||||
export {
|
||||
createServiceWorkerPageController,
|
||||
type ActivationBlocker,
|
||||
type PageControllerDependencies,
|
||||
} from "./service-worker-page-controller.ts";
|
||||
export {
|
||||
createNonceRegistry,
|
||||
createServiceWorkerMessage,
|
||||
parseServiceWorkerMessage,
|
||||
type ParsedMessage,
|
||||
} from "./service-worker-protocol.ts";
|
||||
```
|
||||
|
||||
`src/adapters/http/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* §7–§8. 권장 경로는 V3 계약 실행기(`createContractHttpExecutor`)다. 설치된
|
||||
* 계약과 타입 입력을 받아 상한·전체 데드라인·재시도 권한·효과 확실성 판정을
|
||||
* 런타임이 소유한다.
|
||||
*/
|
||||
export {
|
||||
createContractHttpExecutor,
|
||||
type AuthIntegrationFailureReason,
|
||||
type AuthOperationContext,
|
||||
type CancellationOwner,
|
||||
type ContractHttpExecutor,
|
||||
type ContractHttpExecutorDependencies,
|
||||
type HttpContractViolation,
|
||||
type HttpContractViolationKind,
|
||||
type HttpEffectCertainty,
|
||||
type HttpExecutionContext,
|
||||
type HttpExecutionObservation,
|
||||
type HttpExecutionOutcome,
|
||||
type HttpTransportFailure,
|
||||
type SafeResponseMetadata,
|
||||
} from "./http-execution-v3.ts";
|
||||
/** V3 `attachCredentials` 콜백이 반환해야 하는 결과 타입. */
|
||||
export type { CredentialPatchOutcome } from "./http-contract-bridge.ts";
|
||||
/**
|
||||
* V2 legacy. operationId + `LegacyHttpInput`으로 호출하는 범용 클라이언트다.
|
||||
* 새 코드는 위의 V3 실행기를 쓴다. 남아 있는 이유는 계약이 아직 없는
|
||||
* 오퍼레이션을 위한 이행 경로이기 때문이다.
|
||||
*/
|
||||
export {
|
||||
createHttpClient,
|
||||
type HttpClient,
|
||||
type HttpClientDependencies,
|
||||
type HttpFailure,
|
||||
type HttpResult,
|
||||
type LegacyHttpInput,
|
||||
type Scheduler,
|
||||
} from "./client.ts";
|
||||
/** V2 `HttpClient.execute`의 첫 인자 타입. */
|
||||
export type { OperationRequestInput } from "./request-builder.ts";
|
||||
```
|
||||
|
||||
- [x] **Step 3: 타입체크로 심볼 존재를 검증한다**
|
||||
|
||||
Run: `corepack pnpm check:types:app`
|
||||
Expected: PASS. 실패하면 존재하지 않는 심볼을 재수출한 것이다 — 에러가 지목한 이름을 해당 소스 파일에서 확인하고 배럴에서 빼라. spec §7의 대조표와 대조할 것.
|
||||
|
||||
- [x] **Step 4: 게이트가 새 파일 8개를 거부하는 것을 확인한다 (의도된 실패)**
|
||||
|
||||
Run: `git add -A && corepack pnpm check:adapter-inventory`
|
||||
Expected: **FAIL.** `git ls-files src/adapters`가 128개를 보고하는데 INVENTORY.md는 120행이므로 불일치를 보고한다. 이 실패를 본 뒤 Step 5로 간다. (실패하지 않으면 `git add`가 안 된 것이다.)
|
||||
|
||||
- [x] **Step 5: INVENTORY.md에 행 8개 추가**
|
||||
|
||||
`docs/reviews/adapters/INVENTORY.md`의 표에 경로 알파벳 순서 위치로 끼워 넣고 번호를 다시 매긴다. 상세 리뷰 링크는 같은 그룹의 기존 행과 동일하게 쓴다.
|
||||
|
||||
| 추가할 경로 | 상세 리뷰 링크 |
|
||||
|---|---|
|
||||
| `src/adapters/auth/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/diagnostics/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/http/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/platform/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/query-cache/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/service-worker/index.ts` | `[Worker/push](./05-service-worker-and-web-push.md)` |
|
||||
| `src/adapters/storage/index.ts` | `[Storage/files](./03-storage-and-browser-files.md)` |
|
||||
| `src/adapters/telemetry/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
|
||||
그리고 `docs/reviews/adapters/INVENTORY.md:132`의 `합계: **120/120**` → `합계: **128/128**`.
|
||||
|
||||
- [x] **Step 6: 게이트 3종 통과 확인**
|
||||
|
||||
Run: `corepack pnpm check:adapter-inventory && corepack pnpm check:types:app && corepack pnpm check:architecture`
|
||||
Expected: 셋 다 PASS. `check:adapter-inventory`가 `128 files PASS`를 출력한다.
|
||||
|
||||
- [x] **Step 7: 워커 realm 타입체크 — 실행 중 발견한 필수 단계**
|
||||
|
||||
> 2026-09-16 실행 중 발견. 계획 초안에는 없었다.
|
||||
> `tsconfig.service-worker.json`은 `src/adapters/service-worker` 폴더를 통째로
|
||||
> WebWorker lib로 컴파일하면서 페이지 realm 파일 2개만 `exclude`로 뺀다.
|
||||
> 새 `index.ts`가 그 폴더 안에서 `service-worker-page-controller.ts`를
|
||||
> import하므로, 제외하지 않으면 페이지 realm 파일이 WebWorker lib 컴파일에
|
||||
> 끌려 들어와 `Cannot find name 'document'`로 실패한다.
|
||||
> (M6 리뷰의 "realm 경계가 폴더가 아니라 tsconfig exclude 2줄로만 표현된다"가
|
||||
> 그대로 발현된 것이다.)
|
||||
|
||||
`tsconfig.service-worker.json`의 `exclude` 배열 맨 앞에 추가한다:
|
||||
|
||||
```json
|
||||
"src/adapters/service-worker/index.ts",
|
||||
```
|
||||
|
||||
타입 커버리지는 `tsconfig.app.json`이 이 배럴을 포함하므로 유지된다. 워커 진입점
|
||||
`service-worker-entry.ts`는 배럴을 쓰지 않고 파일을 직접 import하므로 워커 번들에
|
||||
영향이 없다.
|
||||
|
||||
Run: `corepack pnpm check:types:service-worker`
|
||||
Expected: PASS
|
||||
|
||||
- [x] **Step 8: 제거 드릴이 살아 있는지 확인한다**
|
||||
|
||||
`storage/index.ts`를 새로 만들었으므로 제거 드릴을 돌려 서브폴더 삭제가 여전히 성립하는지 본다.
|
||||
|
||||
Run: `corepack pnpm test:browser-file-storage-removal`
|
||||
Expected: 드릴 출력에 `error TS`가 0건이어야 한다. `storage/index.ts`가 `indexeddb/`나 `opfs/`를 참조하면 `still imported`로 죽는다 — Step 1의 주석대로 재수출을 제거하라.
|
||||
|
||||
> **이 환경의 알려진 제약.** `tests/unit/ci-artifact-contract.test.ts`의 16건은
|
||||
> `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`로 실패한다.
|
||||
> 샌드박스가 네트워크 네임스페이스를 못 만들기 때문이며 `develop` `5434760`
|
||||
> 기준선에서도 동일하게 16건 실패한다(2026-09-16 확인). 이 드릴은 내부적으로
|
||||
> `test:unit`을 돌리므로 그 16건 때문에 항상 exit 1이 된다.
|
||||
> **판정 기준은 드릴의 exit code가 아니라 `error TS` 0건과 `still imported` 부재다.**
|
||||
> CI 환경에서는 전체 PASS를 확인할 것.
|
||||
|
||||
- [x] **Step 9: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/adapters/*/index.ts docs/reviews/adapters/INVENTORY.md tsconfig.service-worker.json
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: give every adapter group a public barrel
|
||||
|
||||
각 어댑터 그룹의 공개 표면을 index.ts로 선언한다. 소비자는 아직 바꾸지
|
||||
않았으므로 런타임 동작은 그대로다. storage 배럴은 서브배럴을 재수출하지
|
||||
않는다 — 제거 드릴이 indexeddb/와 opfs/만 삭제하기 때문이다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 그룹 바깥 소비자 15줄을 배럴 경로로 치환
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts:6-29` (12줄, 블록 병합 포함)
|
||||
- Modify: `src/bootstrap/main.tsx:3`
|
||||
- Modify: `src/bootstrap/optional-runtime-host.ts:4`
|
||||
- Modify: `src/bootstrap/register-service-worker.ts:5`
|
||||
- Modify: `src/features/reference-feature/adapters/create-reference-feature-input.ts:9`
|
||||
- Modify: `.storybook/preview.tsx:5-6` (게이트 범위 밖, 일관성 목적)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1이 만든 8개 배럴의 심볼 82개
|
||||
- Produces: `src/` 안에 어댑터 내부 파일을 직접 겨누는 import 0건. Task 3의 게이트 규칙이 이 상태를 전제로 통과한다.
|
||||
|
||||
- [x] **Step 1: 치환 전 위반 건수를 기록한다**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -rn 'from "[^"]*adapters/[^"]*"' src/ | grep -v '^src/adapters/' | grep -v 'index\.ts"' | wc -l
|
||||
```
|
||||
Expected: `15`. 이 숫자가 Step 4에서 `0`이 되어야 한다.
|
||||
|
||||
- [x] **Step 2: `runtime-adapters.ts`의 import 블록을 다시 쓴다**
|
||||
|
||||
`src/bootstrap/runtime-adapters.ts`의 `:6`~`:29` 구간이 한 덩어리다. 아래에서 위로 편집하거나 블록 전체를 한 번에 교체한다 — 위에서부터 고치면 줄 번호가 밀린다.
|
||||
|
||||
치환 내용:
|
||||
|
||||
| 현재 specifier | 바뀔 specifier |
|
||||
|---|---|
|
||||
| `../adapters/auth/external-session-adapter.ts` | `../adapters/auth/index.ts` |
|
||||
| `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` |
|
||||
| `../adapters/http/client.ts` | `../adapters/http/index.ts` |
|
||||
| `../adapters/http/http-execution-v3.ts` | `../adapters/http/index.ts` (위와 **한 블록으로 병합**) |
|
||||
| `../adapters/query-cache/tanstack-cache-coordinator.ts` | `../adapters/query-cache/index.ts` |
|
||||
| `../adapters/query-cache/tanstack-query-cache.ts` | `../adapters/query-cache/index.ts` |
|
||||
| `../adapters/query-cache/server-state-scope-runtime.ts` | `../adapters/query-cache/index.ts` |
|
||||
| `../adapters/query-cache/conditional-validator-store.ts` | `../adapters/query-cache/index.ts` (위 넷을 **한 블록으로 병합**) |
|
||||
| `../adapters/storage/browser-storage-adapter.ts` | `../adapters/storage/index.ts` |
|
||||
| `../adapters/platform/browser-mutation-intent-factory.ts` | `../adapters/platform/index.ts` |
|
||||
| `../adapters/telemetry/best-effort-telemetry.ts` | `../adapters/telemetry/index.ts` |
|
||||
|
||||
`../adapters/cross-context-invalidation/index.ts` 2줄은 이미 배럴이므로 **건드리지 않는다.**
|
||||
|
||||
- [x] **Step 3: 나머지 4개 파일을 치환한다**
|
||||
|
||||
| 파일:줄 | 현재 | 바뀔 것 |
|
||||
|---|---|---|
|
||||
| `src/bootstrap/main.tsx:3` | `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` |
|
||||
| `src/bootstrap/optional-runtime-host.ts:4` | `../adapters/platform/browser-lifecycle.ts` | `../adapters/platform/index.ts` |
|
||||
| `src/bootstrap/register-service-worker.ts:5` | `../adapters/service-worker/service-worker-page-controller.ts` | `../adapters/service-worker/index.ts` |
|
||||
| `src/features/reference-feature/adapters/create-reference-feature-input.ts:9` | `../../../adapters/http/http-execution-v3.ts` | `../../../adapters/http/index.ts` |
|
||||
| `.storybook/preview.tsx:5` | `../src/adapters/auth/external-session-adapter.ts` | `../src/adapters/auth/index.ts` |
|
||||
| `.storybook/preview.tsx:6` | `../src/adapters/query-cache/tanstack-query-cache.ts` | `../src/adapters/query-cache/index.ts` |
|
||||
|
||||
- [x] **Step 4: 위반이 0이 된 것을 확인한다**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -rn 'from "[^"]*adapters/[^"]*"' src/ | grep -v '^src/adapters/' | grep -v 'index\.ts"' | wc -l
|
||||
```
|
||||
Expected: `0`
|
||||
|
||||
- [x] **Step 5: 타입·린트·아키텍처 게이트**
|
||||
|
||||
Run: `corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture`
|
||||
Expected: 셋 다 PASS.
|
||||
|
||||
- [x] **Step 6: 번들 예산을 확인한다 — 이 태스크의 최대 위험**
|
||||
|
||||
배럴 재수출이 트리셰이킹을 무력화하면 초기 청크가 커진다. `package.json`에 `sideEffects` 선언이 없어 번들러가 모든 모듈을 부작용 있는 것으로 본다.
|
||||
|
||||
Run: `corepack pnpm check:bundle`
|
||||
Expected: PASS (`bundle.initialJsGzipBytes` 상한 204800).
|
||||
|
||||
**FAIL한 경우 순서대로 시도한다:**
|
||||
1. `package.json`에 `"sideEffects": false`를 추가한다. 추가 전에 `src/adapters` 아래 최상위 부작용이 있는지 확인하고, CSS import가 있으면 `"sideEffects": ["*.css"]` 형태로 보존한다.
|
||||
2. 그래도 넘치면 `src/adapters/service-worker/index.ts`에서 `service-worker-lifecycle.ts` 블록을 빼고, 워커 realm은 파일 직접 import를 유지한다. 그리고 Task 3의 게이트 규칙 `from.pathNot`에 워커 진입점을 추가한다. (근거: realm이 다르면 문도 다르다 — `platform`과 같은 논리.)
|
||||
3. 1·2로 안 되면 이 태스크를 중단하고 보고한다. 예산 초과를 안고 진행하지 않는다.
|
||||
|
||||
- [x] **Step 7: 단위·통합 테스트**
|
||||
|
||||
Run: `corepack pnpm test:unit && corepack pnpm test:integration`
|
||||
Expected: PASS. 이 태스크는 import 경로만 바꿨으므로 테스트 결과가 달라질 이유가 없다. 깨지면 배럴이 내보내는 심볼이 원본과 다른 것이다.
|
||||
|
||||
- [x] **Step 8: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/bootstrap src/features .storybook
|
||||
git commit -m "$(cat <<'EOF'
|
||||
refactor: reach adapter groups through their barrel
|
||||
|
||||
bootstrap과 feature 어댑터가 어댑터 내부 파일을 직접 겨누던 15곳을 그룹
|
||||
배럴로 바꾼다. 커널(platform/**) 간선과 그룹 내부 import는 파일 경로를
|
||||
유지한다 — check:adapter-inventory가 그 경로를 직접 검증한다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 게이트 규칙 + 회귀 fixture
|
||||
|
||||
규칙만 추가하면 다음 사람이 규칙을 지워도 아무도 모른다. 이 레포는 거부 규칙마다 회귀 fixture를 두는 방식(`docs/architecture/layers.md`)이므로 fixture까지 같이 넣는다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `.dependency-cruiser.json` (`:193`과 `:194` 사이에 규칙 1개)
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts`
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts`
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts`
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts`
|
||||
- Modify: `scripts/check-architecture.ts` (`runGraphFixtureChecks`에 그래프 1개 + assertion 2개)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2가 만든 "위반 0건" 상태. 위반이 남아 있으면 이 규칙 추가가 곧바로 게이트를 깬다.
|
||||
- Produces: `adapter-groups-are-reached-through-their-barrel` 규칙과 그 회귀 fixture 2종
|
||||
|
||||
- [x] **Step 1: fixture 트리를 만든다 (규칙보다 먼저 — 실패를 먼저 본다)**
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts`:
|
||||
```ts
|
||||
export function createFixtureHttpClient(): string {
|
||||
return "fixture";
|
||||
}
|
||||
```
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts`:
|
||||
```ts
|
||||
export { createFixtureHttpClient } from "./client.ts";
|
||||
```
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts` — 규칙이 **잡아야 할** 형태:
|
||||
```ts
|
||||
import { createFixtureHttpClient } from "../adapters/http/client.ts";
|
||||
|
||||
export const deepComposition = createFixtureHttpClient;
|
||||
```
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts` — 규칙이 **통과시켜야 할** 형태:
|
||||
```ts
|
||||
import { createFixtureHttpClient } from "../adapters/http/index.ts";
|
||||
|
||||
export const barrelComposition = createFixtureHttpClient;
|
||||
```
|
||||
|
||||
> fixture는 `analyzeSourceGraph(dir, "src")`로 분석되어 경로가 `src/...`로 보고된다(`scripts/check-architecture.ts:821-833`). 그래서 `^src/`로 시작하는 규칙이 fixture 트리에 그대로 적용된다.
|
||||
|
||||
- [x] **Step 2: `.dependency-cruiser.json`에 규칙을 추가한다**
|
||||
|
||||
`forbidden` 배열의 `adapters-do-not-know-other-concrete-adapters` **바로 다음**, `no-circular-dependencies` **앞**에 넣는다.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "adapter-groups-are-reached-through-their-barrel",
|
||||
"comment": "어댑터 그룹의 공개 표면은 그 그룹의 index.ts다. 그룹 바깥(bootstrap, features, presentation)은 배럴만 import한다. 배럴이 없던 시절 bootstrap은 어댑터 내부 파일 15곳을 직접 겨눴고, 그래서 어떤 파일이 공개이고 어떤 파일이 내부 헬퍼인지 아무 데도 적혀 있지 않았다. 출발점에서 src/adapters를 뺀 이유는 어댑터끼리의 간선은 바로 위 adapters-do-not-know-other-concrete-adapters가 이미 담당하고, 커널(platform/**)은 파일 단위로 공유되기 때문이다 — scripts/check-adapter-inventory.ts가 네 소비자에게 platform/abortable-operation.ts로 해석되는 specifier를 직접 요구한다. 도착점에서 1단계 중첩 index.ts를 허용한 이유는 storage/indexeddb와 storage/opfs가 각자 독립적으로 제거 가능한 런타임이고(scripts/test-browser-file-storage-runtime-removal.ts), 그래서 각자의 배럴이 곧 경계이기 때문이다.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/",
|
||||
"pathNot": "^src/adapters/"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/[^/]+/",
|
||||
"pathNot": "^src/adapters/[^/]+/(?:[^/]+/)?index\\.ts$"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
> 규칙 형태 적합성: `scripts/check-architecture.ts:799-815`의 `validateArchitectureRules`는 `from`에 `path`/`pathNot`, `to`에 `path`/`pathNot`/`circular`만 허용한다. 정규식은 `new RegExp(pattern, "u")`로 평가되므로 비캡처 그룹 `(?:...)`이 허용된다. `$1` 역참조는 쓰지 않았다.
|
||||
|
||||
- [x] **Step 3: 규칙이 실제 소스에서 위반 0인지 확인한다**
|
||||
|
||||
Run: `corepack pnpm check:architecture`
|
||||
Expected: PASS. FAIL하면 Task 2에서 놓친 import가 있다는 뜻이다 — 출력이 지목한 파일을 배럴 경로로 고쳐라.
|
||||
|
||||
- [x] **Step 4: fixture 검사를 `check-architecture.ts`에 배선한다**
|
||||
|
||||
`runGraphFixtureChecks()`의 `Promise.all` 블록(`scripts/check-architecture.ts:826-833`)에 `barrelGraph`를 추가한다:
|
||||
|
||||
```ts
|
||||
const [allowedGraph, unresolvedGraph, layerGraph, cycleGraph, barrelGraph] =
|
||||
await Promise.all([
|
||||
analyzeSourceGraph(allowedRoot, "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "unresolved"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "layer"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "cycle"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "barrel"), "src"),
|
||||
]);
|
||||
```
|
||||
|
||||
그리고 `assertions` 배열에 두 항목을 추가한다:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: "deep adapter import from outside the group is rejected",
|
||||
passed: blockingViolations(barrelGraph).some(
|
||||
({ rule, source, target }) =>
|
||||
rule === "adapter-groups-are-reached-through-their-barrel" &&
|
||||
source === "src/bootstrap/compose-deep.ts" &&
|
||||
target === "src/adapters/http/client.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "barrel import from outside the group is accepted",
|
||||
passed: !blockingViolations(barrelGraph).some(
|
||||
({ source }) => source === "src/bootstrap/compose-barrel.ts",
|
||||
),
|
||||
},
|
||||
```
|
||||
|
||||
> 필드명 근거: `ArchitectureViolation`은 `rule` / `severity` / `source` / `target`을 갖는다 (`scripts/check-architecture.ts:35`, 생성부 `:734-739`). `from`/`to`가 아니다.
|
||||
|
||||
- [x] **Step 5: fixture 회귀 검사가 통과하는지 확인한다**
|
||||
|
||||
Run: `corepack pnpm check:architecture`
|
||||
Expected: `Architecture graph fixtures: 14 regression checks PASS` (기존 12 + 신규 2). 그리고 전체 PASS.
|
||||
|
||||
- [x] **Step 6: fixture가 실제로 무언가를 잡는지 역검증한다**
|
||||
|
||||
규칙을 잠시 무력화해서 fixture가 FAIL하는지 본다. fixture가 항상 통과하면 회귀 검사가 아니다.
|
||||
|
||||
```bash
|
||||
# 규칙 이름을 일시적으로 바꿔 매칭되지 않게 한다
|
||||
sed -i 's/"adapter-groups-are-reached-through-their-barrel"/"temporarily-disabled-barrel-rule"/' .dependency-cruiser.json
|
||||
corepack pnpm check:architecture; echo "EXIT=$?"
|
||||
# 되돌린다
|
||||
sed -i 's/"temporarily-disabled-barrel-rule"/"adapter-groups-are-reached-through-their-barrel"/' .dependency-cruiser.json
|
||||
corepack pnpm check:architecture; echo "EXIT=$?"
|
||||
```
|
||||
Expected: 첫 번째 EXIT는 0이 아니어야 하고(assertion 실패), 되돌린 뒤 EXIT는 0이어야 한다.
|
||||
|
||||
- [x] **Step 7: 커밋**
|
||||
|
||||
```bash
|
||||
git add .dependency-cruiser.json scripts/check-architecture.ts tests/fixtures/architecture/dependency-graph/barrel
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: enforce the adapter barrel boundary in the architecture gate
|
||||
|
||||
그룹 바깥에서 어댑터 내부 파일을 직접 import하면 check:architecture가
|
||||
거부한다. 회귀 fixture 2개가 거부와 허용을 각각 고정한다 — 규칙을 지우면
|
||||
fixture 검사가 먼저 깨진다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 규칙을 문서에 명문화
|
||||
|
||||
코드로 강제되는 규칙이 문서에 없으면 다음 사람은 게이트 에러를 보고서야 규칙을 알게 된다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/architecture/layers.md` (배럴 경계 절 추가)
|
||||
- Modify: `docs/reviews/adapters/README.md` (테스트 이관 규칙)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 3의 규칙 이름 `adapter-groups-are-reached-through-their-barrel`
|
||||
- Produces: 없음 (문서만)
|
||||
|
||||
- [x] **Step 1: `layers.md`에 배럴 경계 절을 추가한다**
|
||||
|
||||
"The adapter kernel" 절 **다음에** 아래를 넣는다:
|
||||
|
||||
```markdown
|
||||
## 어댑터 그룹의 공개 경계
|
||||
|
||||
각 어댑터 그룹의 공개 표면은 그 그룹의 `index.ts`다. 그룹 바깥
|
||||
(`bootstrap`, `features`, `presentation`)은 배럴만 import한다.
|
||||
`adapter-groups-are-reached-through-their-barrel` 규칙이 이를 강제하고,
|
||||
`tests/fixtures/architecture/dependency-graph/barrel`이 거부와 허용을
|
||||
각각 고정한다.
|
||||
|
||||
두 가지 예외가 있고 둘 다 의도된 것이다.
|
||||
|
||||
- **그룹 내부 파일끼리**는 파일 경로로 직접 import한다. 배럴은 바깥을 위한
|
||||
문이지 내부 규율이 아니다.
|
||||
- **어댑터 → 커널(`platform/**`)** 간선도 파일 경로를 유지한다. 커널은
|
||||
런타임 합성물이 아니라 프리미티브이고, `check:adapter-inventory`가 네
|
||||
소비자에게 `platform/abortable-operation.ts`로 해석되는 specifier를
|
||||
직접 요구한다. 커널 배럴(`platform/index.ts`)은 bootstrap과 테스트를
|
||||
위한 것이다.
|
||||
|
||||
`storage`는 최상위 배럴이 `indexeddb/`·`opfs/` 서브배럴을 재수출하지
|
||||
않는다. 두 런타임이 각자 독립적으로 제거 가능하고
|
||||
(`test:browser-file-storage-removal`), 그래서 각 서브배럴이 곧 경계다.
|
||||
규칙의 도착점 정규식이 1단계 중첩 `index.ts`를 배럴로 인정하는 이유가
|
||||
이것이다.
|
||||
```
|
||||
|
||||
- [x] **Step 2: 테스트 이관 규칙을 적는다**
|
||||
|
||||
`docs/reviews/adapters/README.md` 끝에 추가:
|
||||
|
||||
```markdown
|
||||
## 테스트의 어댑터 import
|
||||
|
||||
`tests/` 아래 어댑터 import는 배럴로 일괄 이관하지 않는다. `check:architecture`는
|
||||
`src`만 스캔하므로 강제되지 않고, 단위 테스트의 상당수가 배럴에 없는 내부
|
||||
심볼을 의도적으로 겨눈다.
|
||||
|
||||
어떤 테스트 파일을 **다른 이유로** 수정하거나 분할할 때, 그 파일이 쓰는
|
||||
심볼이 해당 그룹 배럴에 있으면 그 파일 안에서만 배럴 경로로 바꾼다.
|
||||
배럴에 없는 심볼이면 깊은 경로를 유지한다. 배럴에 추가하고 싶으면 그 심볼이
|
||||
공개 표면임을 먼저 논증한다 — 테스트 편의로 배럴을 키우면 배럴이 경계가
|
||||
아니라 재수출 덤프가 된다.
|
||||
```
|
||||
|
||||
- [x] **Step 3: 문서 게이트 확인**
|
||||
|
||||
Run: `corepack pnpm lint && corepack pnpm check:architecture`
|
||||
Expected: PASS. (문서 링크 검증이 있으면 `corepack pnpm verify:documentation`도 돌린다.)
|
||||
|
||||
- [x] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add docs/architecture/layers.md docs/reviews/adapters/README.md
|
||||
git commit -m "$(cat <<'EOF'
|
||||
docs: write down the adapter barrel boundary and its two exceptions
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 완료 판정
|
||||
|
||||
네 태스크가 끝나면 아래가 전부 PASS여야 한다.
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types
|
||||
corepack pnpm lint
|
||||
corepack pnpm check:architecture
|
||||
corepack pnpm check:adapter-inventory
|
||||
corepack pnpm check:bundle
|
||||
corepack pnpm test:unit
|
||||
corepack pnpm test:integration
|
||||
corepack pnpm test:browser-file-storage-removal
|
||||
corepack pnpm test:realtime-removal
|
||||
```
|
||||
|
||||
그리고 아래가 `0`이어야 한다.
|
||||
```bash
|
||||
grep -rn 'from "[^"]*adapters/[^"]*"' src/ | grep -v '^src/adapters/' | grep -v 'index\.ts"' | wc -l
|
||||
```
|
||||
|
||||
## 이 계획이 하지 않는 것
|
||||
|
||||
- `tests/` 166줄의 배럴 이관 — Task 4 Step 2의 규칙대로 파일을 손댈 때만 한다.
|
||||
- 대형 파일 분할 — 별도 계획. 이 계획이 그 선행조건이다.
|
||||
- spec §6이 남긴 부수 발견 4건(`telemetry:49`의 잉여 재수출, `browser-files/index.ts`의 포트 재수출, `opfs/index.ts` 누락 심볼 3개, `.dependency-cruiser.json:141`의 존재하지 않는 `web-worker` 경로) — 각각 별도 티켓.
|
||||
|
||||
---
|
||||
|
||||
## 실행 기록 (2026-09-16 완료)
|
||||
|
||||
네 태스크 전부 실행했다. 커밋 5개: `a42d961` (spec·plan), `b29a471` (배럴 8개),
|
||||
`1606d9b` (소비자 15줄), `27ab17d` (게이트 규칙 + fixture), `eb40bc9` (문서).
|
||||
|
||||
**계획과 달랐던 것 2건.**
|
||||
|
||||
1. **워커 realm 타입체크 실패** — 계획 초안에 없던 Task 1 Step 7이 여기서 나왔다.
|
||||
`service-worker/index.ts`가 `tsconfig.service-worker.json`의 WebWorker lib
|
||||
컴파일에 페이지 realm 파일을 끌어들여 `Cannot find name 'document'`로 실패했다.
|
||||
배럴을 그 tsconfig의 `exclude`에 추가해 해결.
|
||||
2. **spec의 정규식이 게이트에 거부당함** — spec §5.1이 제시한
|
||||
`^src/adapters/[^/]+/(?:[^/]+/)?index\.ts$`를 dependency-cruiser가
|
||||
"unsafe regular expression"으로 거부했다(`(?:[^/]+/)?` 안의 `+`가 star height 2).
|
||||
중첩 없는 교대로 바꿔 통과시켰다:
|
||||
`^src/adapters/[^/]+/index\.ts$|^src/adapters/[^/]+/[^/]+/index\.ts$`
|
||||
spec은 이 규칙을 실제로 실행해 본 적이 없었다.
|
||||
|
||||
**측정 결과.**
|
||||
|
||||
- 번들 영향 **0바이트**: 배럴 도입 전후 모두 초기 JS 181114 / 204800 gzip bytes.
|
||||
`sideEffects` 선언이 없는데도 Rollup이 재수출을 트리셰이킹했다. 계획이 지목한
|
||||
최대 위험은 현실화되지 않았고 대응 3단계는 쓰지 않았다.
|
||||
- import 그래프: 297 모듈 / 897 의존 → 305 모듈 / 914 의존 (배럴 8개 + fixture 4개).
|
||||
- 회귀 검사: 12 → 14. 규칙 이름을 바꿔 fixture가 실제로 깨지는 것을 확인했다.
|
||||
- 배럴 미경유 import: `src/` 15건 → **0건**.
|
||||
|
||||
**PASS:** `check:types` `lint` `check:architecture` `check:adapter-inventory`
|
||||
`check:bundle` `test:integration`(81) `test:component`(130) `verify:documentation`
|
||||
|
||||
**이 환경에서 판정 불가:** `test:unit`, `test:browser-file-storage-removal`,
|
||||
`test:realtime-removal`. 셋 다 `tests/unit/ci-artifact-contract.test.ts`의 16건이
|
||||
`bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`로 실패해 exit 1이
|
||||
된다. `develop` `5434760` 기준선에서도 동일하게 16건 실패함을 stash 후 실행해
|
||||
확인했다. 어댑터 관련 테스트는 전부 통과했고(`test:unit` 1796 passed), 제거 드릴의
|
||||
`error TS`는 0건이다. **CI 환경에서 이 셋의 전체 PASS를 확인해야 한다.**
|
||||
@@ -0,0 +1,464 @@
|
||||
# IndexedDB 커널 승격 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:** 어댑터 4곳이 각자 구현한 IndexedDB 연결·트랜잭션 메커니즘을 `src/adapters/platform/`의 커널 2파일로 모으고, 4벌이 서로 다른 답을 내던 지점을 하나로 만든다.
|
||||
|
||||
**Architecture:** 커널은 **메커니즘만** 갖는다 — open 요청을 Promise로 바꾸기, blocked 데드라인, upgrade/error/success 라우팅, 늦게 도착한 연결 닫기, 트랜잭션 상태기계, 커서 펌프. 데이터베이스 이름·스키마·마이그레이션·governance·**실패 분류(taxonomy)**는 각 서브시스템에 남는다. 실패 매핑은 `translate` 콜백으로 주입하므로 `mapIndexedDbException`과 `mapBrowserDataException`이 서로 다른 답을 내는 현 상태가 보존된다.
|
||||
|
||||
**Tech Stack:** TypeScript, IndexedDB, Vitest, `tests/helpers/memory-indexeddb.ts`(가짜 IDB)
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-09-16-indexeddb-kernel-promotion-design.md`](../specs/2026-09-16-indexeddb-kernel-promotion-design.md)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **`src/adapters/platform/`은 이미 dependency-cruiser의 kernel carve-out이다.** 커널에 파일을 추가하는 데 규칙 변경이 필요 없다(spec §5.3).
|
||||
- **`IDBFactory`는 필수 주입 파라미터다.** `globalThis.indexedDB`를 읽으면 안 된다 — `eslint.config.ts:40-63`이 모든 브라우저 루트에서 그 속성을 막고, 예외는 `platform/browser-lifecycle.ts` **한 파일**에만 부여돼 있다. 주입받으면 `eslint.config.ts`를 건드릴 필요가 없다(spec §5.1).
|
||||
- **소스 파일을 추가하면 `docs/reviews/adapters/INVENTORY.md`에 행을 추가하고 하단 합계를 고친다.** `check:adapter-inventory`가 `git ls-files src/adapters`와 집합을 정확히 대조한다.
|
||||
- **`check:adapter-inventory`에 abort 래칫이 있다.** `addEventListener("abort")`를 쓰는 어댑터 파일이 24개를 넘으면 실패한다. `platform/`은 세지 않는다.
|
||||
- **실패 매핑 4벌을 통일하지 마라.** `mapIndexedDbException`(RT/MT/OP)과 `mapBrowserDataException`(CP)은 같은 에러에 다른 답을 낸다 — `ConstraintError` recovery가 NONE vs REOPEN, `QuotaExceeded` retryable이 false vs true, `NotFound`가 MIGRATION_FAILED vs NOT_FOUND. 통일은 별건이고 이 계획의 범위가 아니다.
|
||||
- **`deleteDatabase`를 나머지 3벌에 추가하지 마라.** CP에만 있는 것이 의도다(spec §2.3).
|
||||
- **이 환경의 알려진 제약:** `tests/unit/ci-artifact-contract.test.ts` 16건이 `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`로 실패한다. `develop` `5434760` 기준선에서도 동일하다. **판정 기준은 그 파일 외의 실패가 0인지**다.
|
||||
- 커밋 메시지 끝에 붙일 것: `Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
|
||||
|
||||
## 왜 이 작업을 하는가 — LOC로 정당화하지 않는다
|
||||
|
||||
> **2026-09-16 갱신 — 사양의 추정이 빗나갔다.** 커널을 실제로 구현하니
|
||||
> 2파일 **1,132줄**(코드 751줄)이다. 사양 추정 470줄의 2.4배다. 사양 §3.1이
|
||||
> "470 LOC라 2분할한다"고 쓴 논거도 사실이 아니었다(2분할 자체는 테스트 셋업이
|
||||
> 갈린다는 별도 근거로 유지). 그래서 아래 표의 순증감이 뒤집힌다.
|
||||
|
||||
| | 사양 추정 | 실제 |
|
||||
|---|---:|---:|
|
||||
| 4벌에서 삭제 | ~974 | (미측정, 이행 후 확정) |
|
||||
| 4벌에 추가 (커널 호출부·콜백) | ~281 | (미측정) |
|
||||
| 4벌 순감 | −693 | (미측정) |
|
||||
| 커널 신규 2파일 | +470 | **+1,132** |
|
||||
| 신규 커널 테스트 2파일 | (미기재) | **+1,602** |
|
||||
| **레포 순증감 (소스만)** | **−223** | **약 +439** |
|
||||
|
||||
**이 리팩토링은 줄 수를 줄이지 않는다. 늘린다.** 소스 약 +439줄, 테스트까지 하면
|
||||
약 +2,041줄이다. 사양은 "절감이 작다"고 썼지만 실제로는 절감이 아니라 증가다.
|
||||
|
||||
**그래서 이 작업의 근거는 오로지 하나다: 트랜잭션 상태기계가 4개에서 1개가 되는 것.**
|
||||
줄 수로 정당화하려는 시도는 이제 불가능하다. 근거가 성립하는 이유는 오늘 그 4개가
|
||||
이미 서로 다른 답을 내고 있기 때문이다:
|
||||
|
||||
| 상황 | RT | MT | OP | CP |
|
||||
|---|---|---|---|---|
|
||||
| 트랜잭션 안 개별 요청 실패 | 본다 | 기록만 | **안 본다** | 즉시 abort |
|
||||
| 값 없이 완료 | UNAVAILABLE | UNAVAILABLE | UNAVAILABLE | **CORRUPT_DATA** |
|
||||
| `deleteDatabase` | 없음 | 없음 | 없음 | **있음** |
|
||||
|
||||
리뷰가 "이미 갈라졌다"고 판정한 근거가 이 표다.
|
||||
|
||||
**그리고 이행을 멈추면 최악이다.** 커널만 넣고 사본을 안 옮기면 +1,132줄의
|
||||
쓰이지 않는 코드가 남는다. 이 레포가 이미 `abortable-operation.ts`로 겪고 있는
|
||||
병(커널은 있는데 24개 파일이 안 씀)을 하나 더 만드는 것이다. 되돌리려면 지금이
|
||||
가장 싸다 — 커널 커밋 하나를 revert하면 끝이고 사본은 아직 안 건드렸다.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 커널 2파일 + 단위 테스트
|
||||
|
||||
사본은 **손대지 않는다.** 코드 추가만 하므로 런타임 동작이 바뀌지 않는다.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapters/platform/indexeddb-connection.ts` (~240 LOC)
|
||||
- Create: `src/adapters/platform/indexeddb-transaction.ts` (~230 LOC)
|
||||
- Create: `tests/unit/indexeddb-connection.test.ts`
|
||||
- Create: `tests/unit/indexeddb-transaction.test.ts`
|
||||
- Modify: `docs/reviews/adapters/INVENTORY.md` (행 2개 + 합계 `128/128` → `130/130`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: spec §3.2·§3.3의 전체 export 시그니처. Task 2~5가 이것만 쓴다.
|
||||
- Consumes: 기존 커널 `snapshotAbortTimers`(`platform/abortable-operation.ts`), `contracts/result.ts`의 `Result`
|
||||
|
||||
- [x] **Step 1: 기존 커널 관례를 읽는다**
|
||||
|
||||
`src/adapters/platform/`의 5파일을 읽고 주석 스타일(왜 이 규칙이 있는지를 근거와 함께 적는 방식), 에러 처리, 의존성 주입 방식을 파악한다. 새 파일은 그 관례를 따른다.
|
||||
|
||||
- [x] **Step 2: 가짜 IDB의 오류 배선을 확인한다**
|
||||
|
||||
`tests/helpers/memory-indexeddb.ts`를 읽는다. 커널은 요청 레벨 `onerror`를 새로 보게 되므로 가짜가 `request.error`를 채우는지가 전제다.
|
||||
|
||||
확인됨(2026-09-16): 채운다. `:167-170`이 `request.error = asException(error)` 후 `queueMicrotask`로 `onerror` 발화, `:309-311`이 같은 일을 **동기로** 한다. **두 경로의 타이밍이 다르므로** 테스트에서 주의한다.
|
||||
|
||||
- [x] **Step 3: 실패하는 테스트를 먼저 쓴다**
|
||||
|
||||
최소한 아래를 고정한다. 각각 먼저 실패하는 것을 확인한 뒤 구현한다.
|
||||
|
||||
연결(`tests/unit/indexeddb-connection.test.ts`):
|
||||
- open 성공 / `onerror` / native throw
|
||||
- `onblocked` — deadline 미설정 시 `BLOCKED`, deadline 경과 시 `BLOCKED_DEADLINE`
|
||||
- upgrade `APPLIED`
|
||||
- upgrade `REJECTED` — **versionchange 트랜잭션이 abort되어 스키마가 커밋되지 않는 것까지** 확인
|
||||
- upgrade가 throw → `REJECTED` + `detail`에 thrown value
|
||||
- `newVersion`이 null → `upgrade` 실행 **전에** `UPGRADE_REJECTED`
|
||||
- admission `ADMIT` / `REJECT` / `FAIL` — 거부 시 연결이 **닫히는지**
|
||||
- 호출자가 포기한 뒤 늦게 도착한 연결이 닫히는지
|
||||
- `CALLER_ABORT`
|
||||
- `translate`가 각 cause에 대해 호출되는지
|
||||
|
||||
트랜잭션(`tests/unit/indexeddb-transaction.test.ts`):
|
||||
- 커밋 / abort
|
||||
- `succeed()` 없이 완료 → `NO_VALUE_PRODUCED`
|
||||
- 첫 결과가 이긴다(`succeed` 이후 `fail` 무시)
|
||||
- 커서 순회와 `SUSPEND` 스텝(중첩 요청 체인)
|
||||
- 예산 콜백이 "삭제 행 기준"과 "스캔 행 기준" 양쪽을 표현할 수 있는지
|
||||
- **CP-4 필수 요건:** `abort()`가 throw하면 caller-abort 표시를 세우지 않고 transaction 이벤트가 결과를 정한다
|
||||
|
||||
- [x] **Step 4: 구현한다 — 좁은 커널 함정을 피한다**
|
||||
|
||||
기존 커널 `abortable-operation.ts:11`은 `AbortTerminalReason` 3멤버를 **반환 타입**에 박아서 5종이 필요한 `http-execution-v3`가 아예 못 썼다. 같은 실수를 반복하면 이 작업은 실패다.
|
||||
|
||||
구현 후 아래를 확인한다:
|
||||
- `IndexedDbFailureCause`가 어떤 공개 **반환 타입**에도 나타나지 않는가 (`translate`의 입력으로만 쓰이는가)
|
||||
- spec §3.4의 4개 사본 예시(RT/MT/OP/CP)가 **전부** 수용되는가
|
||||
|
||||
하나라도 수용되지 않으면 **구현을 멈추고 보고한다.** spec에 억지로 맞추지 않는다.
|
||||
|
||||
- [x] **Step 5: INVENTORY 갱신**
|
||||
|
||||
`docs/reviews/adapters/INVENTORY.md`에 두 행을 알파벳 위치에 넣고 번호를 다시 매긴다. 링크는 같은 그룹(`platform/`) 기존 행과 동일하게 `[Network/state](./01-network-and-state.md)`. 하단 합계를 `130/130`으로.
|
||||
|
||||
- [x] **Step 6: 게이트**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types:app && corepack pnpm check:types:test \
|
||||
&& corepack pnpm lint && corepack pnpm check:architecture \
|
||||
&& corepack pnpm check:adapter-inventory
|
||||
corepack pnpm test:unit
|
||||
```
|
||||
Expected: 앞 묶음 전부 PASS. `test:unit`은 `ci-artifact-contract.test.ts` 외 실패 0.
|
||||
|
||||
- [x] **Step 7: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/adapters/platform tests/unit/indexeddb-*.test.ts docs/reviews/adapters/INVENTORY.md
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: add the shared IndexedDB connection and transaction kernel
|
||||
|
||||
네 어댑터가 각자 구현한 open/blocked/upgrade/트랜잭션 메커니즘을 커널로
|
||||
모은다. 사본은 아직 이행하지 않았으므로 런타임 동작은 그대로다.
|
||||
|
||||
실패 분류는 커널에 넣지 않고 translate 콜백으로 주입한다. RT/MT/OP의
|
||||
mapIndexedDbException과 CP의 mapBrowserDataException이 같은 에러에 다른
|
||||
답을 내며, 그 차이를 통일하는 것은 별건이기 때문이다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: CP 이행 — `indexeddb-checkpoint-store.ts` (712 → 약 570)
|
||||
|
||||
**CP를 먼저 하는 이유:** 가장 작고, 동작 변화 지점이 가장 명확하며, 커널의 CP-4 요건(abort가 throw하면 caller-abort를 세우지 않는다)을 조기에 검증한다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts`
|
||||
- Modify: `tests/unit/resumable-upload-checkpoint.test.ts` (동작 변화 지점 고정)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1의 `openIndexedDbDatabase`, `runIndexedDbTransaction`, `deleteIndexedDbDatabase`
|
||||
- Produces: 없음 (공개 포트 형태 불변)
|
||||
|
||||
- [x] **Step 1: 동작 변화 지점을 테스트로 먼저 고정한다**
|
||||
|
||||
구현 전에 아래 5개가 현재 동작대로 통과하는지 확인한다. 이행 후에도 같아야 한다.
|
||||
|
||||
| # | 지점 | 현재 동작 | 깨지면 |
|
||||
|---|---|---|---|
|
||||
| CP-1 | 값 없는 완료 | `CORRUPT_DATA/RECONCILE` (L541-547) | 번역기가 `NO_VALUE_PRODUCED → CORRUPT_DATA`를 명시 매핑 안 하면 UNAVAILABLE로 바뀜 |
|
||||
| CP-2 | `durability` | 옵션 bag 없음 (L512) | 커널 기본이나 `"strict"`를 넣으면 체크포인트 쓰기가 조용히 느려짐 (성능 회귀) |
|
||||
| CP-3 | `nativeFailure` | 기록 후 **즉시 abort** (L577-588) | `requestFailed`(abort 안 함)로 바꾸면 요청 실패 후에도 뒤 요청이 커밋됨. `compareAndSwap`의 `get→put` 체인(L321-343)에서 특히 위험 |
|
||||
| CP-4 | `abort()`가 throw | `failure`를 **되돌린다** (L528, L536) | 커밋된 체크포인트를 ABORTED로 보고 |
|
||||
| CP-5 | `deletePartition` blocked 타이머 | 네이티브 `setTimeout` (L450) | 주입형 `timers`로 바뀜. `tests/unit/resumable-upload-checkpoint.test.ts:194`가 이 경로를 봄 |
|
||||
|
||||
기존 단언 위치: `:142` CONFLICT/RECONCILE, `:190` UNAVAILABLE/RESUME, `:194` blocked PENDING, `:238` false-abort 금지.
|
||||
|
||||
- [x] **Step 2: 삭제 대상 블록을 커널 호출로 바꾼다**
|
||||
|
||||
| 블록 | 줄 | 대체 |
|
||||
|---|---|---|
|
||||
| `TransactionContext` 타입 | L491-495 | 커널 타입 |
|
||||
| `openAndBind`의 open 요청 Promise 배선 | L163-175, L198-217 | `openIndexedDbDatabase` (upgrade 본문 L176-197은 `upgrade` 콜백으로, `bindScope` 호출은 `admit`으로) |
|
||||
| `runCheckpointTransaction` | L497-596 | `runIndexedDbTransaction` |
|
||||
| `bindScope`의 트랜잭션 배선 | L602-623 | `runIndexedDbTransaction` (검증 로직 L624-652는 `queue`로 그대로) |
|
||||
| `deletePartition`의 blocked/settle 배선 | L431-462, L472-483 | `deleteIndexedDbDatabase` |
|
||||
|
||||
**남길 것:** `PENDING_DELETIONS` 레지스트리(L31-41) + 생성 시 검사(L116-122), `uploadCheckpointDatabaseName`(L73-83), `sameScopeBinding`(L656-672), `snapshotScope`(L674-691), `snapshotCheckpoint`(L693-711).
|
||||
|
||||
- [x] **Step 3: 게이트 + 표적 테스트**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture
|
||||
npx vitest run tests/unit/resumable-upload-checkpoint.test.ts --reporter=default
|
||||
```
|
||||
Expected: 전부 PASS. CP-1~CP-5가 이행 전과 같은 답을 내야 한다.
|
||||
|
||||
- [x] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/adapters/browser-transfer tests/unit/resumable-upload-checkpoint.test.ts
|
||||
git commit -m "refactor: move the upload checkpoint store onto the IndexedDB kernel
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: MT 이행 — `indexeddb-maintenance.ts` (1558 → 약 1370)
|
||||
|
||||
**MT를 두 번째로 하는 이유:** 커널 사용자 중 연결 핸들을 안 쓰는 유일한 사본이라 `openIndexedDbDatabase` 단독 사용 경로를 검증한다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapters/storage/indexeddb/indexeddb-maintenance.ts`
|
||||
- Modify: `tests/unit/indexeddb-maintenance.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1의 `openIndexedDbDatabase`, `openIndexedDbTransaction`, `runIndexedDbTransaction`, `walkIndexedDbCursor`
|
||||
|
||||
- [x] **Step 1: 두 개의 함정을 먼저 이해한다**
|
||||
|
||||
**MT-1 (가장 위험).** `blockedTimeoutMs`를 **넘기지 마라.** 안 넘겨야 오늘 동작(blocked 이벤트 즉시 `BLOCKED`, L485-492)이 유지된다. 넘기면 배치가 최대 그 시간만큼 매달린다. `tests/unit/indexeddb-maintenance.test.ts:289-290`이 `BLOCKED/retryable:true/RELOAD_OTHER_CONTEXTS`를 **즉시** 받길 기대하므로 값을 넣으면 타임아웃으로 실패한다.
|
||||
|
||||
**MT-2.** `upgrade` 콜백을 **생략하라.** 생략해야 오늘의 "upgrade는 곧 실패"(L477-484)가 유지된다. 커널은 생략을 `UPGRADE_REJECTED`로 해석한다. 실수로 `upgrade: () => ({kind:"APPLIED"})`를 넣으면 **잘못된 스키마로 열린다.**
|
||||
|
||||
- [x] **Step 2: 삭제 대상 블록을 커널 호출로 바꾼다**
|
||||
|
||||
| 블록 | 줄 | 대체 |
|
||||
|---|---|---|
|
||||
| `TransactionContext` 타입 | L97-101 | 커널 타입 |
|
||||
| `openExactVersion`의 배선분 | L434-508, L551-585 | `openIndexedDbDatabase` (L509-550을 `admit`으로 이식) |
|
||||
| `createTransaction` | L587-604 | `openIndexedDbTransaction` |
|
||||
| `runTransaction` | L606-705 | `runIndexedDbTransaction` |
|
||||
| 커서 2곳의 deadline/maxRows/abort 보일러플레이트 | L799-833, L1451-1479 | `walkIndexedDbCursor` |
|
||||
|
||||
**MT-3.** `database.onversionchange = () => database.close()`(L543)를 `admit` 안으로 옮긴다. `admit`은 성공 경로에서만 실행되므로 등록 시점이 오늘과 같다.
|
||||
|
||||
**남길 것:** 체크포인트 상태기계(`readCheckpoint` L707-756, `commitPrepared` L969-1251), `prepareRecords`(L863-967), `clock`/`epochClock`(L401-421), `countBucket`(L239-245), 저장 술어(L118-223).
|
||||
|
||||
- [x] **Step 3: 게이트 + 표적 테스트**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture
|
||||
npx vitest run tests/unit/indexeddb-maintenance.test.ts --reporter=default
|
||||
```
|
||||
Expected: 전부 PASS. 특히 `:289-290`이 **즉시** BLOCKED를 받아야 한다(타임아웃이 아니라).
|
||||
|
||||
- [x] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/adapters/storage tests/unit/indexeddb-maintenance.test.ts
|
||||
git commit -m "refactor: move IndexedDB maintenance onto the kernel
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: OP 이행 — `indexeddb-opfs-journal.ts` (1817 → 약 1690)
|
||||
|
||||
**이 계획에서 동작 변화가 가장 큰 태스크다.**
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapters/storage/opfs/indexeddb-opfs-journal.ts`
|
||||
- Modify: `tests/unit/indexeddb-opfs-journal.test.ts`
|
||||
- 가능성: `tests/helpers/memory-indexeddb.ts` 보강
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1의 `openIndexedDbDatabase`, `createIndexedDbConnection`, `runIndexedDbTransaction`, `openIndexedDbTransaction`, `walkIndexedDbCursor`
|
||||
|
||||
- [x] **Step 1: OP-1을 테스트로 먼저 드러낸다 — 이 태스크의 핵심**
|
||||
|
||||
오늘 OP는 트랜잭션 안의 개별 요청에 `.onerror`를 **하나도** 안 단다(파일 전체에서 request `.onerror`는 L1042의 open 요청 하나뿐). 요청 실패는 `transaction.onabort`로만 흘러 `mapIndexedDbException(transaction.error)`(L1158)가 된다.
|
||||
|
||||
커널을 쓰면 **요청 자신의 오류가 보고된다.** 구체적으로 `LOGICAL_KEY_INDEX`가 `unique: true`(L1000-1004)이므로 **중복 put은 요청 레벨 `ConstraintError` → `CONFLICT`**가 되고, 오늘은 `transaction.error`가 무엇이냐에 따라 달라진다.
|
||||
|
||||
이행 **전에** 중복 put 테스트를 `tests/unit/indexeddb-opfs-journal.test.ts`에 추가해 현재 답을 기록하고, 이행 후 달라진 답을 의도된 변경으로 승인한다. 가짜 IDB가 이 경로를 표현하지 못하면 `tests/helpers/memory-indexeddb.ts`를 먼저 보강한다.
|
||||
|
||||
- [x] **Step 2: OP-2를 확인한다**
|
||||
|
||||
오늘 OP의 `runTransaction`은 `succeed` 이후 `fail`이 와도 `explicitFailure`가 이기지만(L1133-1141, `hasValue`는 true 유지) `oncomplete`는 값을 반환한다(L1143-1153). 커널의 "첫 결과가 이긴다" 규칙을 따르면 **`succeed` 후의 `fail`이 무시된다.**
|
||||
|
||||
현재 OP 코드에 그 순서가 실제로 발생하는 경로가 있는지 **확인하라**(spec은 미확인으로 남겼다). 없으면 변화 없음으로 기록하고 넘어간다.
|
||||
|
||||
- [x] **Step 3: 삭제 대상 블록을 커널 호출로 바꾼다**
|
||||
|
||||
| 블록 | 줄 | 대체 |
|
||||
|---|---|---|
|
||||
| `TransactionContext` 타입 | L48-51 | 커널 타입 |
|
||||
| scheduler 기본값 인라인 | L144-153 | `snapshotAbortTimers` |
|
||||
| `openDatabase`의 배선분 | L961-996, L1031-1072 | `openIndexedDbDatabase` + `createIndexedDbConnection` (upgrade 본문 L997-1029는 `upgrade` 콜백으로 그대로 이동) |
|
||||
| `runTransaction` | L1098-1174 | `runIndexedDbTransaction` |
|
||||
| `strictReadwriteTransaction` | L1176-1190 | `openIndexedDbTransaction(…, "strict")` |
|
||||
| 커서 2곳의 limit 루프 | L604-633, L677-712 | `walkIndexedDbCursor` |
|
||||
|
||||
**OP-4.** `signal`은 **넣지 않는다.** 오늘 없는 취소를 새로 만들지 않는다.
|
||||
|
||||
- [x] **Step 4: 게이트 + 표적 테스트**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture
|
||||
npx vitest run tests/unit/indexeddb-opfs-journal.test.ts tests/unit/opfs-byte-store.test.ts --reporter=default
|
||||
```
|
||||
Expected: PASS. OP-1로 인한 답 변화는 Step 1에서 승인한 것만 있어야 한다.
|
||||
|
||||
- [x] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/adapters/storage tests
|
||||
git commit -m "refactor: move the OPFS journal onto the IndexedDB kernel
|
||||
|
||||
요청 레벨 오류를 처음으로 보게 된다. unique 인덱스 위반이 트랜잭션 abort
|
||||
사유가 아니라 요청 자신의 ConstraintError로 보고된다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: RT 이행 — `indexeddb-runtime.ts` (2902 → 약 2665)
|
||||
|
||||
**가장 크고 마지막이다.** 다른 셋이 커널을 전부 검증한 뒤에 옮긴다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapters/storage/indexeddb/indexeddb-runtime.ts`
|
||||
- Modify: `tests/unit/indexeddb-runtime.test.ts`
|
||||
|
||||
- [x] **Step 1: RT-1을 번역기에 명시한다**
|
||||
|
||||
`close()`가 진행 중 open을 끝내는 원인이 `unavailable(operation)`(L743)에서 `translate({kind:"CLOSED"})`로 바뀐다. 번역기가 **`CLOSED → unavailable`을 명시 매핑**해야 오늘 동작이 유지된다. 빠뜨리면 `close()` 중 open이 ABORTED로 보고된다.
|
||||
|
||||
기존 실패 코드 단언 위치: `tests/unit/indexeddb-runtime.test.ts:339-340, :371-372, :454, :534, :549-550, :734-735`.
|
||||
|
||||
- [x] **Step 2: 삭제 대상 블록을 커널 호출로 바꾼다**
|
||||
|
||||
| 블록 | 줄 | 대체 |
|
||||
|---|---|---|
|
||||
| `defaultScheduler` | L108-117 | `snapshotAbortTimers(scheduler)` |
|
||||
| `TransactionContext` 타입 | L85-89 | `IndexedDbTransactionContext` |
|
||||
| `waitForOpeningAttempt` | L659-682 | `connection.acquire(signal)` |
|
||||
| `startOpeningAttempt`의 배선분 | L684-745, L776-843, L872-921 중 배선분 | `openIndexedDbDatabase` (upgrade/admit 콜백 본문은 그대로) |
|
||||
| `createTransaction` | L957-974 | `openIndexedDbTransaction` |
|
||||
| `runTransaction` | L976-1074 | `runIndexedDbTransaction` |
|
||||
| 커서 5곳의 보일러플레이트 | L1374-1382, L1454-1471, L2364-2389, L2531-2548, L2573-2590, L2644-2661, L2718-2735 | `walkIndexedDbCursor` + `budget.admit` |
|
||||
|
||||
**RT-2.** `settleNativeRequest`/`activeOpeningGeneration`(L595, L691-697)이 사라진다. 커널의 settle-once와 단일 비행이 같은 역할을 한다. **의미는 같지만 경합 순서가 달라질 수 있다** — 동시 open 테스트를 주의해서 본다.
|
||||
|
||||
**RT-3.** `openingRequest` 필드(L593)는 오늘도 L712·L2878의 대입 외에 읽는 곳이 없다. 삭제한다.
|
||||
|
||||
**남길 것:** 코덱/영수증/보존/예산(bytes)/governance/migration 목록/상태 브로드캐스트/`monotonicClock`/`countBucket`/저장 레코드 술어/`purgePartitionRecords`의 스토어 순서 로직 — 전부 정책이다.
|
||||
|
||||
- [x] **Step 3: 게이트 + 전체 테스트**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types && corepack pnpm lint && corepack pnpm check:architecture \
|
||||
&& corepack pnpm check:adapter-inventory
|
||||
corepack pnpm test:unit
|
||||
corepack pnpm test:integration
|
||||
```
|
||||
Expected: `ci-artifact-contract.test.ts` 외 실패 0.
|
||||
|
||||
- [x] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/adapters/storage tests/unit/indexeddb-runtime.test.ts
|
||||
git commit -m "refactor: move the IndexedDB runtime onto the kernel
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 완료 판정
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types
|
||||
corepack pnpm lint
|
||||
corepack pnpm check:architecture
|
||||
corepack pnpm check:adapter-inventory
|
||||
corepack pnpm check:bundle
|
||||
corepack pnpm test:unit # ci-artifact-contract.test.ts 외 실패 0
|
||||
corepack pnpm test:integration
|
||||
corepack pnpm test:component
|
||||
corepack pnpm test:browser-file-storage-removal # error TS 0건
|
||||
corepack pnpm test:realtime-removal # error TS 0건
|
||||
```
|
||||
|
||||
그리고 아래가 **0**이어야 한다 — 커널 밖에 남은 open 요청 배선:
|
||||
```bash
|
||||
grep -rn "createObjectStore\|onupgradeneeded" src/adapters --include='*.ts' \
|
||||
| grep -v "src/adapters/platform/" | grep -v "upgrade" | wc -l
|
||||
```
|
||||
|
||||
**실브라우저 확인이 필요하다.** 이 계획은 가짜 IDB 위에서만 검증된다. `tests/browser-capabilities/indexeddb-runtime.spec.ts`(1033 LOC), `opfs-runtime.spec.ts`(221), `resumable-upload.spec.ts`(526)를 실브라우저에서 돌려야 blocked/versionchange 실동작 회귀를 잡는다.
|
||||
|
||||
## 이 계획이 하지 않는 것
|
||||
|
||||
- **실패 매핑 4벌 통일** — `mapIndexedDbException`과 `mapBrowserDataException`이 같은 에러에 다른 답을 내는 것은 별건이다. 이 계획은 그 차이를 `translate` 주입으로 **보존**한다.
|
||||
- **`deleteDatabase`를 3벌에 추가** — CP에만 있는 것이 의도다(spec §2.3).
|
||||
- 대형 파일 분할 — 커널 이행으로 RT가 2902 → 2665가 되지만 여전히 크다. 분할은 별도 계획이다.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 실행 기록 (2026-09-16 — 5개 태스크 전부 완료)
|
||||
|
||||
커밋 5개: `cb62bfb`(커널) · `bb6080b`(CP) · `3366a81`(MT) · `217c1dd`(OP) · `a91e78f`(RT).
|
||||
|
||||
### 실측 LOC — 사양 추정은 전부 빗나갔다
|
||||
|
||||
| 사본 | 사양 추정 | 실제(전체 줄) | 실제(코드 줄) |
|
||||
|---|---:|---:|---:|
|
||||
| CP `indexeddb-checkpoint-store.ts` | −142 | 712 → 612 = **−100** | |
|
||||
| MT `indexeddb-maintenance.ts` | −188 | 1558 → 1430 = **−128** | |
|
||||
| OP `indexeddb-opfs-journal.ts` | −127 | 1817 → 1881 = **+64** | 1744 → 1724 = −20 |
|
||||
| RT `indexeddb-runtime.ts` | −235 | 2902 → 2744 = **−158** | 2807 → 2563 = −244 |
|
||||
| 4벌 합 | **−693** | **−322** | |
|
||||
| 커널 2파일 | +470 | **+1,132** | +751 |
|
||||
| **소스 순증감** | **−223** | **+810** | |
|
||||
| 커널 테스트 2파일 | (미기재) | +1,602 | |
|
||||
|
||||
**공통 원인:** 사양이 `translate`/실패 헬퍼 어댑터 함수 비용을 세지 않았다. CP는 4개(~37줄), MT는 6개가 필요했다. 그리고 이행된 파일들의 주석이 크게 늘었다(OP 6→90줄, RT 12→111줄) — 이 레포 관례상 부풀림이 아니라 개선이지만, 줄 수 예측은 무너뜨린다.
|
||||
|
||||
**결론은 바뀌지 않는다.** 이 작업의 근거는 처음부터 줄 수가 아니라 상태기계 통합이었고, 그건 달성됐다. 다만 **줄 수가 준다는 기대는 완전히 틀렸다**는 것을 기록해 둔다.
|
||||
|
||||
### 부수 성과
|
||||
|
||||
- **abort 래칫이 24 → 21로 세 칸 조여졌다.** CP·MT·RT가 각각 자기 abort 리스너를 지웠다. 래칫이 설계대로 작동했다.
|
||||
- **가짜 IndexedDB가 unique 인덱스를 전혀 강제하지 않는 것을 찾아 고쳤다**(`tests/helpers/memory-indexeddb.ts`). 보강 전에는 중복 `begin`이 `ok:true`로 성공했다 — 브라우저가 거부할 상태를 테스트가 조용히 허용하고 있었다. 이 발견이 커널 이행 자체보다 가치가 클 수 있다.
|
||||
- 사양이 빠뜨린 함정 하나를 CP에서 막았다: `bindScope`는 `succeed()`에 해당하는 것이 없어 그대로 옮기면 정상 바인딩이 `CORRUPT_DATA`로 보고된다. 이후 MT·OP·RT는 성공 출구를 전수 대조했다.
|
||||
|
||||
### 사양이 틀린 것으로 판명된 항목 3건
|
||||
|
||||
1. **OP-1** — 사양은 "이 리팩토링에서 가장 큰 동작 변화"로 지목하며 중복 put의 답이 달라진다고 봤다. **틀렸다.** 이행 전후 모두 `CONFLICT / retryable:false / recovery:NONE`이다. 요청 오류를 아무도 처리하지 않으면 스토어가 바로 그 에러로 abort해서 `transaction.error === request.error`이기 때문이다. 바뀐 것은 답이 아니라 출처다.
|
||||
2. **RT-2** — 사양은 "의미 동일"이라고 썼다. **틀렸다.** 아래 참조.
|
||||
3. **§3.1의 파일 분할 논거** — "470 LOC라 1파일은 폴더 관례를 깬다"고 썼으나 실제 구현은 1,132줄이다. 2분할 자체는 유지할 값이 있지만(테스트 셋업이 갈린다) 그 논거는 사실이 아니었다.
|
||||
|
||||
### 보존하지 못한 동작 1건 — RT-2 (의도적으로 남김)
|
||||
|
||||
blocked 데드라인 이후 재시도가 새 `factory.open()`을 띄운다. 이행 전에는 안 띄웠다. 커널에 "settle 이후에도 살아 있는 요청"을 알려줄 훅이 없기 때문이다.
|
||||
|
||||
**복원하지 않기로 했다.** 근거:
|
||||
- 이행 전후 모두 10초 blocked 데드라인이 있고, 차이는 두 번째 네이티브 open을 띄우는지뿐이다.
|
||||
- 늦게 도착한 연결은 커널이 `closeQuietly`로 닫으므로 **연결 누수도 데이터 위험도 없다.**
|
||||
- 재시도가 10초 데드라인에 게이트되므로 쌓이는 속도가 제한적이다.
|
||||
- 반면 지금 커널을 고치면 **이미 검증이 끝난 4개 사본을 전부 재검증**해야 한다. 이익 대비 위험이 맞지 않는다.
|
||||
|
||||
복원하려면 `openIndexedDbDatabase`에 `onSettled`를 추가하면 된다(`deleteIndexedDbDatabase`에는 이미 있다). 다만 그러면 "blocked 데드라인 실패가 버려진 요청이 끝날 때까지 다음 acquire를 막는가"라는 설계 질문이 따라온다 — 막는다면 다른 탭이 영영 안 닫힐 때 재시도가 영구 차단되어 **지금보다 나쁘다.** 착수 전 그 답부터 정해야 한다.
|
||||
|
||||
### 테스트로 덮지 못한 경로 1건
|
||||
|
||||
RT의 `POLICY_REJECTED` upgrade 거절 경로. `queueIndexedDbUpgradeBinding`의 `onRejected`가 비동기라 `.then` 후처리로 보존했으나, **가짜 IDB의 upgrade 트랜잭션이 동기라 단위 테스트로 검증할 수 없다.** 레포 전체에 이 경로를 덮는 테스트가 없다. **실브라우저 확인이 필요하다.**
|
||||
|
||||
### 검증 결과
|
||||
|
||||
**PASS:** `check:types` `lint` `check:architecture` `check:adapter-inventory`(21/21 래칫) `check:bundle`(181114/204800, 이행 전과 동일) `test:integration`(81) `test:component`(130)
|
||||
|
||||
**`test:unit`:** 실패 파일은 `tests/unit/ci-artifact-contract.test.ts` 하나뿐(16~18건, 실행마다 흔들림 — `bwrap: loopback: Failed RTM_NEWADDR`). `develop` `5434760` 기준선에서도 동일함을 stash 후 실행해 확인했다. **그 파일 밖 실패 0.**
|
||||
|
||||
**실브라우저 미검증:** `tests/browser-capabilities/indexeddb-runtime.spec.ts`(1033) · `opfs-runtime.spec.ts`(221) · `resumable-upload.spec.ts`(526). 전부 가짜 IDB 위에서만 검증됐다. blocked/versionchange 실동작과 위 upgrade 경로는 여기서만 잡힌다. **CI 또는 로컬 브라우저 실행이 남은 과제다.**
|
||||
@@ -0,0 +1,322 @@
|
||||
# Refactoring Review Remediation Design
|
||||
|
||||
## Purpose
|
||||
|
||||
Complete the existing runtime-integrity work, preserve the current uncommitted
|
||||
implementation snapshot, and then close the correctness and evidence gaps found
|
||||
by the repository-wide module, script, schema, and test review.
|
||||
|
||||
This design supplements `2026-08-01-runtime-integrity-refactor-design.md`. It
|
||||
does not replace the existing release/boot or scope-owned server-state plans.
|
||||
Those changes remain the baseline on which the remediation phases build.
|
||||
|
||||
## Chosen approach
|
||||
|
||||
Use a staged, in-place compatibility migration with test-first behavior fixes.
|
||||
|
||||
- Preserve every pre-existing dirty-worktree change. Do not reset, restore, or
|
||||
replace the current implementation with a clean-branch rewrite.
|
||||
- Complete and verify the existing release/boot and scope-generation work
|
||||
before changing its public contracts.
|
||||
- Add a failing regression test before every production behavior change.
|
||||
- Fix active correctness and release blockers before structural extraction.
|
||||
- Keep public facades stable while removing duplicate internal authorities.
|
||||
- Do not enable the Service Worker capability until its bounded marker reader,
|
||||
full identity handshake, and ACTIVE fixture build are verified.
|
||||
|
||||
A big-bang rewrite is rejected because query ownership, HTTP execution,
|
||||
release evidence, and optional runtimes have independent failure modes. A
|
||||
P0/P1-only patch is also rejected because it would leave duplicate registries
|
||||
and misleading quality gates that can recreate the same defects.
|
||||
|
||||
## Program phases
|
||||
|
||||
The work is delivered as independently testable sub-projects in this order.
|
||||
|
||||
1. Existing implementation baseline
|
||||
2. Query invalidation correctness
|
||||
3. Mutation intent and effect settlement
|
||||
4. Immutable release promotion
|
||||
5. Release/evidence contract enforcement
|
||||
6. Quality and architecture gate accuracy
|
||||
7. HTTP contract and layer consolidation
|
||||
8. Service Worker hardening
|
||||
9. Characterized adapter decomposition
|
||||
|
||||
Each phase must leave type checking, linting, and its focused tests green. A
|
||||
later phase may depend only on explicit interfaces produced by an earlier one.
|
||||
|
||||
## Existing implementation baseline
|
||||
|
||||
The current dirty snapshot contains the in-progress Release Manifest V2, exact
|
||||
boot pairing, scope-generation ownership, HTTP V3 executor, and optional runtime
|
||||
foundation. It is the source of truth for this program.
|
||||
|
||||
Before remediation begins, run the focused suites from the existing
|
||||
release/boot and scope-owned server-state plans. Resolve failures in those
|
||||
plans without changing the remediation contracts below. Record browser- or
|
||||
artifact-producing gates separately when they cannot run in the current
|
||||
environment.
|
||||
|
||||
## Query invalidation architecture
|
||||
|
||||
One query-key module owns both bound keys and invalidation prefixes. Query-key
|
||||
schema version 2 is:
|
||||
|
||||
```text
|
||||
["query", 2, namespaceId, namespaceVersion, scopeFingerprint,
|
||||
definitionVersion, identityToken]
|
||||
```
|
||||
|
||||
The invalidation prefix for the same namespace is exactly:
|
||||
|
||||
```text
|
||||
["query", 2, namespaceId, namespaceVersion]
|
||||
```
|
||||
|
||||
Features declare namespace identities and topic-to-namespace edges; they never
|
||||
copy TanStack key shapes. Bootstrap composes one `InvalidationRegistry`, calls
|
||||
`indexInvalidationRegistry` once, and passes the resulting many-to-many index
|
||||
to the cache coordinator. Cross-context messages continue to carry opaque
|
||||
topics only. The receiving coordinator resolves every namespace for that
|
||||
topic locally.
|
||||
|
||||
The legacy flat query registry and the empty registry in `query-keys.ts` are
|
||||
removed after static usage reaches zero. Query persistence is disabled, so the
|
||||
key-version change requires no persisted-cache migration.
|
||||
|
||||
## Mutation intent architecture
|
||||
|
||||
`MutationIntent` is an application-level command identity, not an HTTP adapter
|
||||
detail. The contracts layer defines its immutable shape, the application layer
|
||||
defines `MutationIntentFactory`, and a browser adapter implements the factory
|
||||
with `crypto.randomUUID()`.
|
||||
|
||||
```ts
|
||||
type MutationIntent = Readonly<{
|
||||
intentId: string;
|
||||
operationId: string;
|
||||
canonicalInputIdentity: string;
|
||||
idempotencyKey?: string;
|
||||
createdAtMonotonicMs: number;
|
||||
}>;
|
||||
```
|
||||
|
||||
`useApplicationMutation` creates exactly one intent after duplicate admission
|
||||
and before optimistic projection. The intent travels through the bound mutation
|
||||
execution context and feature input to `ContractHttpExecutor`. Every physical
|
||||
retry and reconciliation of that logical submit reuses the same intent. A new
|
||||
submit always creates a new intent.
|
||||
|
||||
For a `KEYED` descriptor, the executor requires a bounded, non-empty
|
||||
idempotency key before credential resolution or fetch admission. Missing or
|
||||
invalid keys return a pre-dispatch contract violation with `NOT_STARTED` and
|
||||
perform zero fetches. Query operations never emit an idempotency header. Intent
|
||||
IDs and keys are forbidden in URLs, query keys, diagnostics, and telemetry.
|
||||
|
||||
## Mutation effect settlement
|
||||
|
||||
Command failures carry mandatory effect certainty. The mutation bridge derives
|
||||
settlement before changing optimistic state:
|
||||
|
||||
```text
|
||||
NOT_STARTED | NOT_APPLIED -> rollback
|
||||
APPLIED_CONFIRMED -> commit, then invalidate
|
||||
MAYBE_APPLIED -> retain uncertain layer; do not retry or invalidate
|
||||
```
|
||||
|
||||
`OptimisticLayerLease` gains an uncertain state and an explicit reconciliation
|
||||
operation. An uncertain layer remains ordered with later layers and cannot be
|
||||
collapsed into the base until inspection resolves it as applied or not applied.
|
||||
The application async state exposes `mutation-effect-unknown`, and the
|
||||
controller exposes a reconciliation action associated with the original
|
||||
intent. Unknown effect never silently maps to conflict, success, or generic
|
||||
retryable failure.
|
||||
|
||||
## Immutable release promotion
|
||||
|
||||
The release workflow has one byte-producing authority.
|
||||
|
||||
1. `immutable_build` creates `dist`, build manifest, module inventory, and the
|
||||
local supply-chain documents once.
|
||||
2. The job publishes one immutable bundle and its `distSha256`.
|
||||
3. Provider jobs scan and attest that exact digest.
|
||||
4. The promotion job downloads the bundle and provider evidence, supplies
|
||||
`VULNERABILITY_REPORT_PATH` and `PROVENANCE_ATTESTATION_PATH`, and verifies
|
||||
schemas, signatures, and digest identity.
|
||||
5. Promotion consumes the verified bundle without rebuilding it.
|
||||
|
||||
The current `build:release` command is split into byte production, local
|
||||
evidence generation, provider verification, and promotion verification.
|
||||
Missing provider evidence remains `FAIL_UNVERIFIED`; it is never converted to a
|
||||
local pass. An attestation for a different digest, or any rebuild after
|
||||
attestation, fails promotion.
|
||||
|
||||
This repository validates but does not fabricate external provider evidence.
|
||||
The CI environment must supply a vulnerability report and signed provenance
|
||||
attestation produced for the published candidate digest. Until that external
|
||||
integration is configured, candidate build and local verification may pass but
|
||||
promotion remains intentionally unavailable.
|
||||
|
||||
## Release and evidence contracts
|
||||
|
||||
One async `verifyReleaseRuntimeCoherence` policy is used by `verify-release`
|
||||
and the rollback runbook. V1 verifies the legacy scalar tuple. V2 verifies the
|
||||
exact package set and recomputes the contract-set digest. V2 never synthesizes
|
||||
an API contract version.
|
||||
|
||||
All machine-readable evidence writers call a common validated writer before
|
||||
touching the destination:
|
||||
|
||||
```ts
|
||||
writeValidatedJsonArtifact({ path, schema, value }): Promise<void>
|
||||
```
|
||||
|
||||
Executable Zod schemas are authoritative. Checked JSON schemas are generated
|
||||
views and a `--check` command fails on drift. CI maps every evidence path to an
|
||||
executable schema and validates content before upload, rather than checking
|
||||
existence only.
|
||||
|
||||
`verifyBuildManifestOutputs` confines declared paths to their approved roots,
|
||||
parses the module inventory, and compares its raw SHA-256 with
|
||||
`moduleInventoryHash`.
|
||||
|
||||
Repository file discovery is fail-closed. Required roots fail on absence,
|
||||
permissions, or read errors; optional roots permit only explicit `ENOENT`.
|
||||
Provenance and secret scanning share the tracked repository inventory so build
|
||||
inputs such as `index.html`, Vite configs, TypeScript configs, `.nvmrc`, and the
|
||||
provider workflow cannot be silently omitted.
|
||||
|
||||
## Quality and architecture gates
|
||||
|
||||
Coverage reports distinguish `selectedTotal` from `repositoryTotal`.
|
||||
`repositoryTotal` contains every production TypeScript module and starts with a
|
||||
non-decreasing baseline. A critical-module registry immediately includes HTTP
|
||||
V3, request/response bounds, boot bounds, service-worker lifecycle, scope
|
||||
generation, and release loading. A changed high-risk module must have a policy
|
||||
entry or an owned, expiring waiver.
|
||||
|
||||
The HTTP scenario catalog is executable input to table-driven contract tests.
|
||||
Declaring a scenario without executing its status, effect, retry, fetch-count,
|
||||
media-type, body-bound, and scope-fence expectations does not satisfy the gate.
|
||||
|
||||
`config/ci/gates.json` is parsed by one shared schema.
|
||||
`scripts/generate-ci-workflow.ts` deterministically emits the complete provider
|
||||
workflow, and its `--check` mode fails when the checked-in workflow differs.
|
||||
Token and regex presence checks are not authoritative. Playwright configurations inherit
|
||||
`forbidOnly: true`, Vitest rejects `.only`, and fake timers are restored by the
|
||||
common test setup.
|
||||
|
||||
The Babel/Node resolver graph is the sole authoritative architecture analyzer
|
||||
while dependency-cruiser cannot parse TypeScript 7. A zero-module graph fails.
|
||||
Rules prohibit contracts from importing outer application/runtime layers and
|
||||
feature adapters from importing global concrete adapters.
|
||||
|
||||
## HTTP contract consolidation
|
||||
|
||||
The installed contract contribution is the single source for method, path,
|
||||
input/output validators, retry semantics, effect classification, deadlines,
|
||||
and byte bounds. The reference DTO schema is defined once and requires a valid
|
||||
datetime when `createdAt` is present. Legacy registries and codecs are generated
|
||||
from the contribution during the compatibility window, then removed after
|
||||
production and tests have no callers.
|
||||
|
||||
Provider-neutral operation outcomes and typed operation maps live in contracts
|
||||
or application ports. Feature adapters do not import `HttpExecutionOutcome`
|
||||
from a concrete HTTP adapter and do not accept `operationId: string` paired with
|
||||
`input: unknown`. Runtime `REQUEST_TIMEOUT_MS` is a global maximum applied on
|
||||
top of descriptor deadlines.
|
||||
|
||||
Raw query and mutation overloads are removed from production exports after
|
||||
callers migrate to `BoundQuery` and `BoundMutation`. Test-only legacy harnesses
|
||||
remain outside the production public index until their tests migrate.
|
||||
|
||||
## Service Worker hardening
|
||||
|
||||
Activation markers are read through a realm-safe bounded response reader. It
|
||||
checks declared length, reads at most `maxBytes + 1`, cancels an oversized or
|
||||
non-terminating stream, decodes fatal UTF-8, and validates the marker record.
|
||||
`response.text()` is not used for bounded worker protocol data.
|
||||
|
||||
Page and worker exchange a canonical identity digest covering protocol version,
|
||||
cache schema version, build, release, contract set, and static asset set. This
|
||||
wire-shape change increments the Service Worker protocol version to 2. Every
|
||||
tuple-field mutation changes the digest and rejects activation.
|
||||
The ACTIVE fixture build validates asset entries and recomputes the static set
|
||||
digest without changing the product's default `null` selection.
|
||||
|
||||
## Adapter decomposition
|
||||
|
||||
Structural extraction begins only after behavior is characterized.
|
||||
|
||||
- IndexedDB runtime and maintenance share one persisted-row schema containing
|
||||
record, receipt, retention, budget types, guards, and golden fixtures.
|
||||
- OPFS worker keeps its public facade while browser bootstrap, message host,
|
||||
core state machine, Web Lock lease, and physical I/O move into focused files.
|
||||
- Public response cache extracts manifest codec/digest and generic lock logic.
|
||||
- Download delivery extracts browser-managed, picker-streaming, and object-URL
|
||||
strategies behind the existing facade.
|
||||
|
||||
Resumable upload, websocket, reconnect, stream coordinator, HTTP V3, and
|
||||
Browser RPC remain intact unless a behavior test demonstrates an independent
|
||||
change reason. File length alone is not a split criterion.
|
||||
|
||||
## Error handling
|
||||
|
||||
- Active correctness failures use stable failure kinds and preserve effect
|
||||
certainty.
|
||||
- Mandatory release evidence, file inventory, schema, or digest failures fail
|
||||
closed.
|
||||
- Optional diagnostics cannot change command, cache, or release outcomes.
|
||||
- Cleanup continues through all participants but publishes no READY state after
|
||||
any mandatory failure.
|
||||
- Tool crashes, null exit status, signals, and timeouts never count as expected
|
||||
negative-fixture rejection.
|
||||
|
||||
## Testing and verification
|
||||
|
||||
Every behavior change follows red-green-refactor. The minimum regression set
|
||||
includes:
|
||||
|
||||
- Actual `bindQuery` keys for local, remote, fan-out, and generation-isolated
|
||||
invalidation.
|
||||
- Independent runtime intents, same-submit retry identity, missing-key
|
||||
pre-dispatch rejection, and secret-free diagnostics.
|
||||
- Optimistic NOT_APPLIED, APPLIED_CONFIRMED, and MAYBE_APPLIED settlement,
|
||||
including out-of-order layers and reconciliation.
|
||||
- V1/V2 release coherence and every contract-set tamper in both verifier and
|
||||
rollback drill.
|
||||
- Provider evidence absence, digest mismatch, post-attestation rebuild, and a
|
||||
valid immutable promotion fixture.
|
||||
- Required-root and unreadable-file failures, schema drift, invalid evidence,
|
||||
and module-inventory hash mismatch.
|
||||
- Complete V3 response/effect scenario execution and production read/write E2E.
|
||||
- Headerless oversized and non-terminating Service Worker marker streams.
|
||||
- Shared persisted-row acceptance/rejection across IndexedDB runtime and
|
||||
maintenance before extraction.
|
||||
|
||||
Repository completion requires fresh evidence from type checking, linting,
|
||||
non-browser tests, coverage, architecture, build, release verification, CI
|
||||
contract checks, browser capabilities, E2E, accessibility, visual tests where
|
||||
the environment supports them, schema parity, and diff hygiene. Unsupported
|
||||
browser gates are reported explicitly and are never claimed as passing.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- The existing release/boot and scope-generation work passes its original
|
||||
focused suites before remediation contracts are changed.
|
||||
- Mutation success invalidates every matching bound query locally and remotely.
|
||||
- No two independent logical commands reuse an idempotency key; missing KEYED
|
||||
intent prevents network admission.
|
||||
- MAYBE_APPLIED never causes arbitrary optimistic rollback, commit, retry, or
|
||||
invalidation.
|
||||
- Release promotion verifies and promotes the exact same immutable bytes.
|
||||
- V2 rollback verification cannot bypass contract-set package or digest checks.
|
||||
- Required files, artifact schemas, and manifest output hashes fail closed.
|
||||
- Coverage and scenario gates measure production behavior rather than declared
|
||||
subsets or source tokens.
|
||||
- Production uses one executable HTTP contract registry and respects clean
|
||||
architecture dependency direction.
|
||||
- Service Worker activation is bounded and full-identity coherent before the
|
||||
capability can become ACTIVE.
|
||||
- Adapter splits preserve their public facade and pass shared golden tests.
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
# Platform-owned Frontend Assurance and Delivery Design
|
||||
|
||||
## Purpose
|
||||
|
||||
This design moves test assurance and CI/CD orchestration to the two platforms
|
||||
that own those responsibilities while keeping product behavior and product
|
||||
tests in the frontend template. It covers the complete vertical path from risk
|
||||
selection through immutable static-site promotion.
|
||||
|
||||
The three repositories are:
|
||||
|
||||
- `/home/donghyeon/workspace/desktop-server-git/test-assurance-platform`
|
||||
- `/home/donghyeon/workspace/desktop-server-git/cicd-platform`
|
||||
- `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template`
|
||||
|
||||
The selected approach is platform-native integration. A CI-first translation
|
||||
bridge and a test-assurance-only migration were rejected because each would
|
||||
leave one platform temporarily owning semantics assigned to the other.
|
||||
|
||||
## Authority boundaries
|
||||
|
||||
### Frontend product repository
|
||||
|
||||
The frontend repository owns:
|
||||
|
||||
- production source and product contracts;
|
||||
- test implementations, fixtures, mocks, scenario catalogs, and assertions;
|
||||
- package scripts that run one declared suite;
|
||||
- raw product-specific reports and codecs for product artifacts;
|
||||
- repository declarations: risks, obligations, suites, change surfaces, build
|
||||
components, outputs, and selected platform capabilities.
|
||||
|
||||
It does not own remote scheduling, pipeline DAG generation, evidence
|
||||
assessment, provider orchestration, retention, signing, or promotion.
|
||||
|
||||
### Test assurance platform
|
||||
|
||||
The test assurance platform owns:
|
||||
|
||||
- risk-to-obligation selection;
|
||||
- scheduler-neutral execution plans;
|
||||
- suite execution contracts and bounded local execution;
|
||||
- report normalization and false-green prevention;
|
||||
- evidence digest chains and obligation assessment;
|
||||
- flaky, quarantine, waiver, and capability-readiness semantics.
|
||||
|
||||
It does not own runner allocation, workflow fan-out/fan-in, build publication,
|
||||
release approval, or promotion.
|
||||
|
||||
### CI/CD platform
|
||||
|
||||
The CI/CD platform owns:
|
||||
|
||||
- required workflows, source materialization, runner trust, and toolchain pins;
|
||||
- remote projection of test-assurance work items;
|
||||
- install, lint, typecheck, deterministic build, and artifact publication;
|
||||
- immutable candidate assembly and artifact identity;
|
||||
- vulnerability, SBOM, provenance, signature, and provider evidence;
|
||||
- release approval, promotion, retention, and terminal status publication.
|
||||
|
||||
It consumes test-assurance results without reinterpreting their test meaning.
|
||||
|
||||
## End-to-end authority flow
|
||||
|
||||
```text
|
||||
frontend declarations and tests
|
||||
-> testctl validate/lock/compile/select/plan
|
||||
-> CI projects source work items to untrusted runners
|
||||
-> testctl normalize/bundle/assess source results
|
||||
-> CI builds one immutable frontend candidate
|
||||
-> CI supplies the candidate digest to artifact-bound work items
|
||||
-> testctl normalize/bundle/assess artifact results
|
||||
-> CI obtains vulnerability/SBOM/provenance/signature evidence
|
||||
-> release-control promotes the exact assessed candidate
|
||||
```
|
||||
|
||||
Every transition carries canonical identities. No stage may replace a missing
|
||||
identity with a path, timestamp, branch name, or mutable tag.
|
||||
|
||||
## Test assurance platform changes
|
||||
|
||||
### Frontend capability family
|
||||
|
||||
Add the following independent capabilities:
|
||||
|
||||
- `unit-typescript-vitest`
|
||||
- `component-react-vitest`
|
||||
- `integration-http-msw`
|
||||
- `architecture-typescript`
|
||||
- `coverage-v8`
|
||||
- `e2e-playwright-firefox`
|
||||
- `e2e-playwright-webkit`
|
||||
- `accessibility-web`
|
||||
- `visual-regression-web`
|
||||
|
||||
The existing `e2e-playwright-chromium` capability is upgraded to the same
|
||||
contract revision. Browser capabilities retain separate readiness cards; no
|
||||
aggregate frontend-readiness card or score is introduced.
|
||||
|
||||
Each capability defines its own artifacts, timeouts, isolation, false-green
|
||||
rules, max evidence age, and minimum readiness. A repository obligation may
|
||||
require all three browser capabilities without merging their readiness.
|
||||
|
||||
### Toolchain catalog
|
||||
|
||||
Create a new catalog revision containing the frontend template's supported
|
||||
toolchain:
|
||||
|
||||
- Node.js `24.14.0`
|
||||
- pnpm `11.17.0`
|
||||
- TypeScript `7.0.2`
|
||||
- Vitest `4.1.10`
|
||||
- Playwright `1.62.0`
|
||||
|
||||
The exact installed versions in `package.json`, `.nvmrc`, and the lockfile are
|
||||
validated against the catalog. The lock generator records immutable artifact
|
||||
digests. A missing digest or version mismatch is `TA-LOCK-003`/exit 30 and does
|
||||
not fall back to the host toolchain.
|
||||
|
||||
### Adapters and normalizers
|
||||
|
||||
Add adapters and normalizers for:
|
||||
|
||||
- Vitest JUnit XML and Vitest-discovered test counts;
|
||||
- V8 coverage summary plus the repository production-module inventory;
|
||||
- typed HTTP scenario execution receipts;
|
||||
- TypeScript architecture graph and violation JSON;
|
||||
- Playwright JSON/JUnit, trace, screenshot, console, and network summaries;
|
||||
- accessibility findings and manual-review records;
|
||||
- visual baseline identity and image-diff results;
|
||||
- production-shaped read/write E2E mutation receipts.
|
||||
|
||||
Normalizers validate regular non-symlink files, bounded byte sizes, fatal UTF-8,
|
||||
strict schemas, canonical repository-relative paths, and report-specific
|
||||
cross-field invariants.
|
||||
|
||||
### False-green rules
|
||||
|
||||
The following outcomes can never normalize to PASS:
|
||||
|
||||
- zero discovered tests or an all-skipped required suite;
|
||||
- a pass created only by retry;
|
||||
- a missing, empty, malformed, oversized, or mismatched report;
|
||||
- a declared HTTP scenario without an exact executed receipt;
|
||||
- a missing required browser project;
|
||||
- a non-empty source tree with zero architecture modules or dependencies;
|
||||
- an unresolved import or dependency cycle;
|
||||
- a production module absent from the coverage universe;
|
||||
- a browser write test without observed response, mutation, and reload reads;
|
||||
- an accessibility or visual result whose baseline/provider identity is absent.
|
||||
|
||||
Negative product fixtures may be schema-valid FAIL evidence; command outcome and
|
||||
assessment, not artifact shape alone, determine satisfaction.
|
||||
|
||||
### Artifact-bound execution contract
|
||||
|
||||
The current v2 execution schemas cannot bind a work item to an immutable build
|
||||
input. The platform therefore adds v3 execution contracts rather than silently
|
||||
changing v2 semantics.
|
||||
|
||||
`SuiteDefinition` and `WorkItem` gain required fields:
|
||||
|
||||
```text
|
||||
executionPhase: SOURCE | ARTIFACT
|
||||
requiredInputArtifacts[]:
|
||||
artifactId
|
||||
mediaType
|
||||
sha256
|
||||
```
|
||||
|
||||
An artifact-bound `ExecutionRequest` carries the same artifact references. The
|
||||
plan, raw result set, normalized result, evidence bundle, and assessment all
|
||||
bind the input artifact tuple. Evidence from v2 and v3 cannot be merged.
|
||||
|
||||
The platform dual-reads existing v2 JVM manifests during migration. New
|
||||
frontend capabilities require v3, and all new platform outputs are v3. There is
|
||||
no implicit phase default.
|
||||
|
||||
### Artifact suite declaration amendment
|
||||
|
||||
A repository cannot know the SHA-256 of a candidate that CI has not built yet.
|
||||
The repository contract therefore separates a static declaration from an
|
||||
executable suite:
|
||||
|
||||
- `ArtifactSuiteTemplate` is repository-owned and declares the suite command,
|
||||
raw artifacts, `executionPhase: ARTIFACT`, and required input artifact IDs
|
||||
and media types. Its schema forbids `sha256` and digest placeholders.
|
||||
- `SuiteDefinition` remains the executable v3 contract required above. For an
|
||||
artifact suite it always contains the concrete `requiredInputArtifacts`
|
||||
tuple including SHA-256.
|
||||
- after `ci-frontend` freezes the candidate, CI creates the artifact
|
||||
`ExecutionRequest` with that candidate tuple; testctl matches it to the
|
||||
template and materializes the executable `SuiteDefinition` and `WorkItem`.
|
||||
|
||||
CI supplies artifact identity but does not construct or reinterpret test suite
|
||||
semantics. An unmatched artifact ID/media type, an unresolved template, or a
|
||||
digest in a committed template stops planning. Templates never enter raw,
|
||||
normalized, evidence, or assessment documents.
|
||||
|
||||
## CI/CD platform changes
|
||||
|
||||
### Test-assurance integration
|
||||
|
||||
Add `ci-test-assurance`. It depends on `ci-standard-core` and invokes a
|
||||
digest-pinned `testctl` distribution using argument arrays only.
|
||||
|
||||
Its responsibilities are:
|
||||
|
||||
1. validate and lock the repository assurance manifest;
|
||||
2. compile policy and create source and artifact execution requests;
|
||||
3. obtain deterministic plans;
|
||||
4. project `execute-one` work items to the required trust partition;
|
||||
5. preserve plan digest, work-item ID, attempt, exit code, and raw artifacts;
|
||||
6. call testctl normalization, bundling, and assessment;
|
||||
7. expose only canonical assessment and evidence digests downstream.
|
||||
|
||||
CI may choose runner parallelism but may not change selection, retry, timeout,
|
||||
expected artifacts, status, or obligation satisfaction. Missing work-item
|
||||
results are platform defects, never successful no-ops.
|
||||
|
||||
### Node and frontend capabilities
|
||||
|
||||
`ci-node-typescript` owns reproducible pnpm install, declared lint, and declared
|
||||
typecheck. It no longer executes or assesses unit/coverage suites when
|
||||
`ci-test-assurance` is selected.
|
||||
|
||||
`ci-frontend` owns one deterministic build of the selected source revision and
|
||||
verifies:
|
||||
|
||||
- a non-empty static output;
|
||||
- byte-identical rebuild evidence in an isolated verification workspace;
|
||||
- absence of undeclared environment and build-host values;
|
||||
- declared size budgets;
|
||||
- a canonical tree digest and archive manifest.
|
||||
|
||||
The candidate consumed after this point is the first verified candidate. The
|
||||
verification build proves determinism but is never promoted.
|
||||
|
||||
### Release capability family
|
||||
|
||||
Add the following capabilities:
|
||||
|
||||
- `ci-dependency-vulnerability`
|
||||
- `ci-artifact-signing`
|
||||
- `ci-static-artifact-supply-chain`
|
||||
- `ci-static-site-publish`
|
||||
|
||||
Generalize existing `ci-sbom` and `ci-provenance` subject contracts so that a
|
||||
static archive is a supported immutable subject without weakening their
|
||||
container behavior. `ci-static-artifact-supply-chain` is a composite that
|
||||
references, rather than copies, child evidence.
|
||||
|
||||
`delivery-release-control` promotes the exact candidate whose digest appears in
|
||||
the signed release identity. It may not build, modify, or repackage the
|
||||
candidate. Static-site publication returns provider and served-content digests;
|
||||
both must equal the approved subject before promotion succeeds.
|
||||
|
||||
### Release identity
|
||||
|
||||
The signed release identity is split into two immutable documents so that one
|
||||
candidate can be promoted to more than one environment without rewriting its
|
||||
candidate manifest:
|
||||
|
||||
- `ReleaseManifestV2` binds the candidate, test, supply-chain, policy,
|
||||
toolchain, and platform identities;
|
||||
- `PromotionSubject` binds the release-manifest digest to the target
|
||||
environment, approval identity/policy, confirmed publication, and expected
|
||||
Git CAS state.
|
||||
|
||||
Together they bind:
|
||||
|
||||
```text
|
||||
source revision
|
||||
candidate archive SHA-256 and canonical member manifest
|
||||
test-assurance manifest and policy digests
|
||||
source plan, evidence, and assessment digests
|
||||
artifact plan, evidence, and assessment digests
|
||||
dependency vulnerability, SBOM, and provenance digests
|
||||
signature key ID and signature digest
|
||||
CI policy, capability-registry, toolchain, and platform-catalog digests
|
||||
target environment and approval identity
|
||||
```
|
||||
|
||||
Promotion history is an append-only record and is not a mutable field inside
|
||||
`ReleaseManifestV2`.
|
||||
|
||||
A report for another candidate, a rebuilt candidate, a changed policy, an
|
||||
expired approval, or an unconfirmed provider mutation blocks promotion.
|
||||
|
||||
### Required workflow
|
||||
|
||||
The centrally installed required workflow remains a thin bootstrap. It contains
|
||||
no language, test, build, provider, or promotion logic. It materializes the
|
||||
exact source revision, verifies the signed platform catalog, invokes pinned
|
||||
`cicdctl`, and publishes one terminal sentinel.
|
||||
|
||||
Product repositories do not copy this workflow.
|
||||
|
||||
## Frontend consumer contract
|
||||
|
||||
The frontend repository adds:
|
||||
|
||||
- `delivery-platform.yaml`;
|
||||
- `test-assurance.yaml`;
|
||||
- a generated `test-assurance.lock.json`;
|
||||
- risk, obligation, suite, and change-surface documents under
|
||||
`config/test-assurance/`.
|
||||
|
||||
`delivery-platform.yaml` selects the core, Node, frontend, test-assurance,
|
||||
dependency vulnerability, SBOM, provenance, signing, static supply-chain,
|
||||
static publication, and release-control capabilities. It pins the signed
|
||||
platform version.
|
||||
|
||||
Suite definitions reference existing product-owned package scripts. One suite
|
||||
definition executes one bounded test purpose and declares its raw artifacts.
|
||||
The repository does not wrap several assurance decisions in one script.
|
||||
|
||||
The product retains:
|
||||
|
||||
- production runtime and adapter tests;
|
||||
- Vitest and Playwright configuration;
|
||||
- mocks, scenarios, fixtures, and browser assertions;
|
||||
- V8 instrumentation configuration;
|
||||
- product artifact codecs such as runtime/release manifest schemas.
|
||||
|
||||
It removes after cutover:
|
||||
|
||||
- the copied `.gitea/workflows/quality-gates.yml`;
|
||||
- `config/ci/gates.json` and its runner/generator/checker;
|
||||
- local risk selection, waiver, normalization, and assessment engines;
|
||||
- local provider, retention, promotion, and CI evidence orchestration;
|
||||
- package scripts whose only purpose is to reproduce platform policy.
|
||||
|
||||
## Treatment of completed and in-progress frontend work
|
||||
|
||||
Runtime production changes and their product tests remain unchanged.
|
||||
|
||||
Repository-wide V8 instrumentation, HTTP scenario execution, and browser
|
||||
read/write assertions remain as product test inputs. Their local selection,
|
||||
reconciliation, evidence assessment, and waiver logic moves to test assurance.
|
||||
|
||||
Product runtime/release artifact codecs remain local. Supply-chain provider
|
||||
policy, archive transfer, signing, retention, and promotion move to CI/CD.
|
||||
|
||||
The uncommitted Task 3 worktree is not reset or overwritten. Before migration,
|
||||
every changed path is classified as product-owned, test-assurance-owned,
|
||||
CI/CD-owned, or unrelated/user-owned. Reusable validators and adversarial tests
|
||||
move to their owning platform through explicit patches. Unrelated and
|
||||
origin-unknown changes are preserved.
|
||||
|
||||
## Migration sequence
|
||||
|
||||
### Phase 1: Test assurance capability readiness
|
||||
|
||||
Implement v3 contracts, frontend capabilities, adapters, normalizers, locks,
|
||||
positive fixtures, adversarial fixtures, and independent readiness cards. Each
|
||||
new capability reaches at least R1 before a consumer may select it in shadow.
|
||||
|
||||
### Phase 2: CI/CD integration and immutable release
|
||||
|
||||
Implement pinned testctl integration, source/artifact work-item projection,
|
||||
frontend candidate identity, provider evidence, signing, static publication,
|
||||
and release-control binding. Each CI capability receives independent P1
|
||||
evidence; activation remains shadow.
|
||||
|
||||
### Phase 3: Consumer declarations
|
||||
|
||||
Add both manifests and the assurance declarations to the frontend repository.
|
||||
Map existing product suites to capabilities without deleting the legacy path.
|
||||
Validate all manifests with the released platform binaries.
|
||||
|
||||
### Phase 4: Shadow parity
|
||||
|
||||
Run legacy and platform paths against the same source revision. Compare:
|
||||
|
||||
- selected suites and discovered/executed counts;
|
||||
- PASS, FAIL, FLAKY, INCOMPLETE, and platform-defect classification;
|
||||
- coverage production-module universe;
|
||||
- HTTP scenario declared/executed identities;
|
||||
- Chromium, Firefox, and WebKit results;
|
||||
- candidate archive and member digests;
|
||||
- provider evidence and promotion readiness.
|
||||
|
||||
The platform path is the only candidate producer in shadow. Legacy release and
|
||||
promotion commands become read-only comparison probes. No two writers may
|
||||
publish or promote.
|
||||
|
||||
Before shadow execution, the legacy workflow registration is disabled and its
|
||||
required status is detached. The centrally installed workflow runs in shadow,
|
||||
and its runner may invoke legacy test/evidence commands only as read-only
|
||||
comparison probes. Observed state records zero legacy candidate/provider/
|
||||
promotion invocations and exactly one platform candidate writer.
|
||||
|
||||
### Phase 5: Cutover
|
||||
|
||||
After parity and fault tests pass, activate the platform capabilities, attach
|
||||
the central required workflow/status, and remove copied workflow and local
|
||||
policy engines. Product tests and declared suite commands remain.
|
||||
|
||||
## Failure and rollback semantics
|
||||
|
||||
- Unsupported capability or unavailable toolchain is explicit UNSUPPORTED or
|
||||
INCOMPLETE, not a skipped pass.
|
||||
- Schema-major mismatch stops before execution and never invokes a local
|
||||
fallback.
|
||||
- Missing or corrupt raw results stop normalization.
|
||||
- Provider timeout, response loss, or digest mismatch blocks promotion and is
|
||||
reconciled by operation ID where mutation may have occurred.
|
||||
- A candidate, source assessment, or artifact assessment digest change
|
||||
invalidates approval.
|
||||
- A missing terminal sentinel blocks the required status.
|
||||
|
||||
Rollback changes only the repository's signed `platformVersion`/catalog pin to
|
||||
the previous proven release and restores the previous capability activation.
|
||||
It never restores a copied product workflow. Release rollback promotes the
|
||||
previous stable immutable subject through release-control.
|
||||
|
||||
## Verification strategy
|
||||
|
||||
### Test assurance platform
|
||||
|
||||
- schema positive and adversarial corpus;
|
||||
- compiler/selector/plan determinism;
|
||||
- each normalizer's valid, malformed, missing, empty, oversized, symlink, and
|
||||
cross-field cases;
|
||||
- zero-discovery, all-skipped, retry, browser-matrix, coverage-omission,
|
||||
architecture-empty/cycle, scenario-omission, and mutation-receipt fixtures;
|
||||
- v2/v3 isolation and digest-chain tests;
|
||||
- full conformance chain and Gradle verification.
|
||||
|
||||
### CI/CD platform
|
||||
|
||||
- manifest compiler and capability dependency/activation tests;
|
||||
- pinned testctl invocation and exit-code preservation;
|
||||
- fan-out completeness and missing-result fault tests;
|
||||
- deterministic static candidate and exact-member archive tests;
|
||||
- provider timeout, invalid signature, changed digest, response-loss, and stale
|
||||
approval tests;
|
||||
- required workflow thinness and exact required-status tests;
|
||||
- registry, boundary, fixture, fault, and readiness verification.
|
||||
|
||||
### Frontend consumer
|
||||
|
||||
- manifest validation with released platform binaries;
|
||||
- existing focused product tests;
|
||||
- platform shadow run against the actual repository;
|
||||
- semantic parity and digest reports;
|
||||
- one-writer and rollback drills;
|
||||
- removal tests proving the template works without copied workflow or local
|
||||
policy engines.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Test selection and assessment have one authority: test assurance.
|
||||
- CI orchestration and promotion have one authority: CI/CD.
|
||||
- The frontend repository contains product tests and declarations, not copied
|
||||
platform engines.
|
||||
- Every required frontend capability has independent readiness evidence.
|
||||
- Source and artifact test evidence bind the exact revision and candidate.
|
||||
- The promoted static site is byte-identical to the assessed, signed candidate.
|
||||
- Missing evidence, unsupported capability, platform failure, and provider
|
||||
uncertainty cannot produce a passing required status.
|
||||
- Cutover and rollback require no copied workflow logic in the product
|
||||
repository.
|
||||
@@ -0,0 +1,222 @@
|
||||
# Provider Evidence Guardian Transaction Design
|
||||
|
||||
## Goal
|
||||
|
||||
Make one guardian process own the provider evidence filesystem transaction from
|
||||
raw creation through sealed publication. A supervisor or provider hard death
|
||||
must leave neither canonical raw evidence nor a guardian-owned sealed temp/final
|
||||
inode, and the same workspace must be immediately retryable. Only the complete
|
||||
authenticated `publish -> PUBLISHED -> commit -> EOF` sequence preserves the
|
||||
canonical sealed artifact.
|
||||
|
||||
## Chosen Ownership Boundary
|
||||
|
||||
The guardian owns filesystem identity and publication. The supervisor retains
|
||||
archive, trust, schema, signature, and evidence validation. This avoids two
|
||||
unsafe alternatives:
|
||||
|
||||
- Keeping raw-only guardianship would leave the sealed rename-to-supervisor-death
|
||||
cleanup gap.
|
||||
- Moving evidence validation into the guardian would duplicate security policy
|
||||
and make the helper unnecessarily privileged and complex.
|
||||
|
||||
The client opens the canonical `provider-evidence/untrusted` and
|
||||
`provider-evidence` directories with `O_DIRECTORY|O_NOFOLLOW` before spawning
|
||||
the guardian. Those identity-pinned directory descriptors are inherited as fd 3
|
||||
and fd 4; they are never encoded in argv or the environment. The canonical raw
|
||||
and final leaves are fixed by provider kind. Before spawn, the client exclusively
|
||||
creates a nonce-private raw staging inode and the nonce-private sealed temp
|
||||
inode, records both identities, and inherits their open descriptors as fd 5 and
|
||||
fd 6. The guardian validates each inherited descriptor against its
|
||||
descriptor-relative pathname, then publishes raw staging to the fixed raw leaf
|
||||
with a no-replace hard link. Startup recovery authority is therefore an inode
|
||||
identity acquired before spawn, never an identity discovered later from an
|
||||
expected pathname. Both processes perform transaction I/O through
|
||||
`/proc/self/fd/<fd>/<leaf>` so pathname substitution cannot redirect creation or
|
||||
recovery into another directory. No raw path, sealed path, identity, provider
|
||||
command, or credential is an argv value. The initial request contains only the
|
||||
version, kind, random control nonce, and absolute deadline.
|
||||
|
||||
## Transaction Invariants
|
||||
|
||||
1. Before a complete valid guard frame, the guardian has not published a
|
||||
canonical filesystem object. The client may have allocated only zero-byte,
|
||||
mode `0600`, nonce-private raw staging and sealed temp inodes whose identities
|
||||
it already holds. EOF with no frame or a partial frame removes both allocations.
|
||||
2. Before spawning, the client validates that both pinned descriptors name the
|
||||
expected canonical directories; computes fixed raw/final leaves and
|
||||
nonce-private raw-staging/sealed-temp leaves; and creates the private leaves
|
||||
with `O_CREAT|O_EXCL|O_NOFOLLOW`, mode `0600`, size zero, and link count one.
|
||||
It retains both handles and inherits them as fd 5/fd 6 in addition to directory
|
||||
fd 3/fd 4.
|
||||
3. At bootstrap, the guardian fstats fd 5/fd 6, reads only their
|
||||
`/proc/self/fd/5|6` link targets, and accepts each basename only when it is a
|
||||
direct child of the canonical fd 3/fd 4 directory and matches the exact
|
||||
provider-kind/32-lowercase-hex private-leaf grammar. It then requires
|
||||
descriptor-relative lstat of that basename to match the already-fstat fd
|
||||
identity, type, mode, size, and link count. This binds a deletion alias to an
|
||||
inherited identity; it never promotes a pathname-discovered identity to
|
||||
ownership. The two basenames must encode the same kind and nonce prefix.
|
||||
4. After guard validation, the guardian verifies that the received kind/nonce
|
||||
derives those exact bootstrapped private leaves. It verifies fd 5/fd 6 remain
|
||||
regular zero-byte single-link `0600` files and exactly match the derived
|
||||
private pathnames. It
|
||||
uses `link(raw staging, canonical raw)` without replacement, verifies both
|
||||
names have the inherited raw identity and link count two, unlinks the private
|
||||
raw name, fsyncs the raw directory, and verifies the canonical raw link count
|
||||
is one. READY is emitted only after this authority transfer succeeds.
|
||||
5. READY is authenticated by the request nonce and returns raw dev/inode plus
|
||||
sealed temp leaf/dev/inode. The supervisor starts the provider only after it
|
||||
validates this exact bounded response with constant-time nonce equality.
|
||||
6. The supervisor writes only schema-validated sealed bytes to the temp inode.
|
||||
It opens with `O_NOFOLLOW`, checks dev/inode before and after writing, applies
|
||||
mode `0400`, writes the complete bounded bytes, fsyncs, and closes.
|
||||
7. Publish metadata contains the nonce, sealed dev/inode, byte length, and
|
||||
SHA-256. The guardian checks the held descriptor and temp pathname identity,
|
||||
regular-file type, link count, exact mode/size/hash, and canonical final-path
|
||||
absence.
|
||||
8. Publication uses atomic no-replace `link(temp, final)`, then unlinks temp and
|
||||
fsyncs the parent directory. If death occurs between link and unlink, both
|
||||
names refer to the same pinned inode and both are cleanup candidates.
|
||||
9. PUBLISHED is authenticated and is emitted only after final pathname identity
|
||||
and directory durability are verified.
|
||||
10. Commit is legal only after PUBLISHED. It removes the pinned raw inode and
|
||||
enters `commitPending`; it does not exit. EOF with no pending bytes is the
|
||||
sole success terminal and preserves only the sealed final inode.
|
||||
11. Any data after commit, including a separate later chunk, is a protocol error.
|
||||
EOF/abort/deadline/protocol failure before the success terminal cleans raw,
|
||||
temp, and final only when each path still names the guardian-owned identity.
|
||||
12. If the guardian dies before READY is accepted, the client attempts cleanup
|
||||
of raw staging, canonical raw, sealed temp, and sealed final aliases using
|
||||
only the two identities recorded before spawn. A current pathname is never
|
||||
opened and promoted to an owned identity. A competing canary or same-kind
|
||||
transaction therefore survives every startup failure.
|
||||
13. Cleanup attempts every owned target and reports cleanup failures together
|
||||
with the primary failure using `AggregateError` at the supervisor boundary.
|
||||
Client fd 3-fd 6 handles and guardian fd 5/fd 6 duplicates are closed on
|
||||
every success and failure branch; close errors join the same aggregate rather
|
||||
than skipping remaining cleanup.
|
||||
|
||||
Client-side exclusive private allocation is the startup ownership token. The
|
||||
guardian accepts that token only after inherited-fd, descriptor-relative
|
||||
pathname, type, mode, size, and link-count checks. Every cleanup identity is
|
||||
recorded at allocation or authenticated READY; pathname discovery never creates
|
||||
authority. Creation, validation, link, unlink, chmod, fstat, close, publish,
|
||||
sync, and cleanup failures all fail closed.
|
||||
|
||||
## Bounded Authenticated Protocol
|
||||
|
||||
Every control or acknowledgement message is a four-byte big-endian length plus
|
||||
canonical JSON with an exact ordered field set, strict UTF-8, no NUL, and a
|
||||
total payload bound. Unknown, duplicate, reordered, oversized, truncated, or
|
||||
trailing fields are rejected.
|
||||
|
||||
The state sequence is:
|
||||
|
||||
```text
|
||||
guard -> READY(raw identity, sealed temp identity)
|
||||
-> publish(size, sha256, sealed identity)
|
||||
-> PUBLISHED(sealed identity)
|
||||
-> commit
|
||||
-> EOF success
|
||||
```
|
||||
|
||||
All messages carry the same 32-byte random nonce. READY and PUBLISHED are
|
||||
validated with `timingSafeEqual`; publish and commit are authenticated the same
|
||||
way. Commit merely changes state, so a byte delivered in a later chunk before
|
||||
EOF remains observable and causes fail-closed cleanup.
|
||||
|
||||
The maximum initial lease is the provider wall timeout plus a fixed ten-minute
|
||||
post-processing allowance. The provider timeout remains bounded at 30 minutes,
|
||||
so the guardian maximum is 40 minutes. Near-provider-timeout tests must show
|
||||
that valid publication still has post-processing time, while an expired lease
|
||||
cleans all owned objects.
|
||||
|
||||
## Supervisor and Scope Exit Ownership
|
||||
|
||||
The lease exposes raw/temp/final identities, `publish(bytes)`, `commit()`,
|
||||
`abort()`, and a non-rejecting premature-exit promise. The client knows all
|
||||
possible leaves and both startup identities before spawn and retains its pinned
|
||||
directory and private-file handles until the lease terminates. Before READY it
|
||||
cleans only aliases that still match those recorded identities. After READY it
|
||||
checks the guardian response against the same identities and fallback-cleans
|
||||
raw, temp, and final by identity if the guardian dies.
|
||||
|
||||
Provider waiting owns an explicit `scopeActive` latch. A guardian exit starts
|
||||
whole-scope kill and collection only while that latch is true. Once the scope
|
||||
completion path has collected the unit, the callback records a lifecycle error
|
||||
but cannot start an unawaited kill. Publication and terminal commit observe the
|
||||
guardian exit through their normal awaited failure path and clean sealed state.
|
||||
|
||||
Provider stdout and stderr are untrusted secret-bearing byte streams. The
|
||||
supervisor counts and bounds them for resource enforcement but never forwards
|
||||
their raw bytes into supervisor/CI stdout or stderr, on either success or
|
||||
failure. Functional provider assertions use signed evidence or a non-log side
|
||||
channel. Sealing/output I/O is allowed to settle; the design does not claim
|
||||
OS-level cancellation. `GITHUB_OUTPUT` is a runner-owned regular file. After
|
||||
output append succeeds, commit makes the guardian remove raw and EOF completes
|
||||
the transaction.
|
||||
|
||||
Guardian diagnostics are best-effort only. A closed stderr or control descriptor
|
||||
must not turn a fail-closed branch into a resolved operation or exit zero:
|
||||
diagnostic and fd-close failures are absorbed after cleanup, and a nonzero exit
|
||||
or requested fatal signal is issued unconditionally.
|
||||
|
||||
## Failure and Recovery
|
||||
|
||||
- No/partial guard EOF: no canonical raw or sealed object is published. The
|
||||
guardian removes both nonce-private allocations through aliases that bootstrap
|
||||
already bound to inherited fd identities, without needing kind/nonce from a
|
||||
complete control frame.
|
||||
- A competing canonical raw canary or another same-kind attempt causes
|
||||
no-replace link failure. The loser removes only its private identities and
|
||||
never removes the winner or canary.
|
||||
- Guardian death after linking raw but before READY: the client uses its
|
||||
pre-recorded raw identity to clean both private and canonical aliases and its
|
||||
pre-recorded sealed identity for temp/final aliases, then retries the same
|
||||
workspace immediately.
|
||||
- Parent death after creation but before READY: stdout/control pipe failure or
|
||||
EOF makes the still-running guardian clean both owned objects.
|
||||
- Guardian death after READY: the supervisor knows raw and sealed identities and
|
||||
cleans raw, temp, and final fallbacks.
|
||||
- Supervisor death after PUBLISHED: guardian EOF cleans raw and the published
|
||||
final inode, including the link/unlink intermediate state.
|
||||
- Publish or commit race: serialized guardian state completes the current file
|
||||
operation, then applies EOF/protocol failure cleanup; success requires clean
|
||||
EOF after commitPending.
|
||||
- Cleanup failure: remaining targets are still attempted and every error is
|
||||
preserved; PASS is impossible.
|
||||
|
||||
There is one bounded crash window before spawn: if the client itself is killed
|
||||
after private allocation but before the guardian is created, zero-byte `0600`
|
||||
nonce-private leaves can remain. They contain no provider or credential bytes
|
||||
and cannot occupy the fixed canonical raw/final names, so they do not block an
|
||||
immediate same-kind retry. Automatic pathname sweeping is intentionally omitted
|
||||
because an unproven stale pathname is not deletion authority.
|
||||
|
||||
After each observable managed-process failure, tests require canonical raw,
|
||||
private staging/temp, canonical final, guardian, and provider cgroup residual
|
||||
counts to be zero before retrying the same workspace successfully. The
|
||||
documented pre-spawn client hard-death window is the sole residual exception.
|
||||
|
||||
## Verification
|
||||
|
||||
Real-process tests cover no/partial frames, a competing raw canary, same-kind
|
||||
concurrency, guardian `SIGKILL` after raw link but before READY followed by
|
||||
same-workspace retry, parent death around READY, valid
|
||||
READY identities, EOF/deadline cleanup, publish/PUBLISHED, post-scope guardian
|
||||
death, supervisor death after publication, commit trailing bytes in a later
|
||||
chunk, closed-stderr fail-closed termination, near-timeout publication, and no
|
||||
residual guardian/files. A provider that successfully prints a supplied
|
||||
credential is verified not to expose it through supervisor stdout/stderr. Live fixtures
|
||||
also specify active-scope guardian kill, detached-child external marker/raw
|
||||
append suppression, cgroup collection, and same-workspace retry. Live
|
||||
systemd/bwrap execution remains explicitly unverified when the approval limit
|
||||
prevents running it.
|
||||
|
||||
The external-canary regression waits for a test guardian spawn marker before
|
||||
creating the fixed raw file, proving that the initial absence check has already
|
||||
completed. The fixed raw bytes and dev/inode must remain unchanged after startup
|
||||
rejection. The pre-READY link regression watches only the fixed raw basename,
|
||||
kills the exact direct child on that link event, and requires identity-bound
|
||||
cleanup plus an immediate same-workspace retry.
|
||||
@@ -0,0 +1,47 @@
|
||||
# V8 Coverage Counter Contract Design
|
||||
|
||||
## Goal
|
||||
|
||||
Version the serialized risk-coverage artifact independently from its policy and lock the repository's counter-bearing/counterless classifier to the output of the installed Vitest/V8 producer.
|
||||
|
||||
## Artifact contract
|
||||
|
||||
`config/testing/risk-coverage.json` remains policy schema version 2. `scripts/check-risk-coverage.ts` changes only its serialized output envelope to schema version 3 because the artifact fields were renamed from executable/non-executable terminology to `counterBearingTotal`, `instrumentedCounterBearingTotal`, `counterlessTotal`, and `counterlessModules`.
|
||||
|
||||
The contract test runs the real CLI against an owned temporary repository. It reuses the current policy, materializes its 19 policy-sensitive source paths as counter-bearing modules, writes an exact consistent coverage summary, and reads the published JSON artifact. It requires output schema version 3, the exact counter-bearing fields, and absence of every legacy executable/non-executable field.
|
||||
|
||||
## Producer microfixture
|
||||
|
||||
Repository fixtures under `tests/fixtures/v8-coverage-counter-semantics/` contain only source and a child test template. The child test file uses a `.fixture.ts` suffix and the main Vitest discovery exclusion is verified behaviorally so it cannot recursively join the repository suite.
|
||||
|
||||
At runtime, `scripts/check-v8-coverage-counter-semantics.ts` creates one owned directory below the operating-system temporary directory. It copies the fixed fixture into that directory and writes the child Vitest config there. The child process uses that directory as its root and writes its JSON summary below that same directory; it never writes repository coverage or artifact paths.
|
||||
|
||||
The fixture contains these exact source rows:
|
||||
|
||||
- `runtime.ts`: a runtime declaration/initializer; at least one standard counter total must be positive.
|
||||
- `import-type-empty.ts`: `import type {}` only.
|
||||
- `import-empty.ts`: `import {}` only.
|
||||
- `import-side-effect.ts`: a bare side-effect import only.
|
||||
- `import-value.ts`: a value import only.
|
||||
- `reexport-named.ts`: a named value re-export only.
|
||||
- `reexport-star.ts`: a star value re-export only.
|
||||
- `type-only.ts`: type declarations only.
|
||||
|
||||
Every row except `runtime.ts` must contain exact `0/0/0/100` lines, statements, functions, and branches counters. The checker rejects a missing summary, missing or additional row, malformed counter, counterless nonzero drift, or runtime all-zero drift.
|
||||
|
||||
## Process and failure handling
|
||||
|
||||
The child Vitest process is launched without a shell or network. Exit failure is converted to a bounded diagnostic containing truncated stdout/stderr. File or JSON failures identify the missing or invalid summary without exposing unbounded child output. An outer `finally` removes only the exact owned temporary root for success and every failure path.
|
||||
|
||||
Pure summary validation is exported from a focused library and covered with literal mutation fixtures. Runner tests inject child exit or successful-without-summary behavior and assert cleanup. The real standalone checker executes in `test:coverage` before the repository coverage run, so FE-GATE-005 and sample removal both consume it through the existing package script contract.
|
||||
|
||||
## Documentation and evidence
|
||||
|
||||
The testing strategy is synchronized to the current 19 high-risk modules and 80 thresholds, documents output schema version 3, and retains policy schema version 2. Final evidence includes the focused unit/contract tests, node/test TypeScript, changed-file lint, standalone producer checker, root risk checker, sample removal, and diff validation.
|
||||
|
||||
## Self-review
|
||||
|
||||
- No placeholder or deferred choice remains.
|
||||
- Policy schema 2 and artifact schema 3 are explicitly separate.
|
||||
- All child-owned paths are below one temporary root and cleanup has one owner.
|
||||
- Main discovery, subprocess failure, missing summary, exact rows, zero/nonzero drift, and bounded diagnostics have explicit verification paths.
|
||||
@@ -0,0 +1,751 @@
|
||||
# 어댑터 배럴(`index.ts`)을 공개 경계로 승격 — 설계서
|
||||
|
||||
- 대상 레포: `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template`
|
||||
- 브랜치: `develop` (기준 커밋 `5434760`)
|
||||
- 전제(이미 확정): 배럴 폐지안은 기각. `src/adapters/<group>/index.ts`를 **진짜 공개 경계**로 만든다.
|
||||
- 게이트 기준선: `check:architecture` PASS, `check:types:app` PASS 유지.
|
||||
- 이 문서는 설계만 한다. 소스 수정·빌드·테스트 실행 없음.
|
||||
|
||||
---
|
||||
|
||||
## 0. 요약 (먼저 읽을 것)
|
||||
|
||||
| 항목 | 판정 |
|
||||
|---|---|
|
||||
| 배럴 표기 표준 | **명명 재수출(named re-export)**. `export *`는 "이미 명시적인 서브배럴을 합칠 때"만 허용 |
|
||||
| 새로 만들 배럴 | 8개, 합계 114줄 (auth 9 / diagnostics 7 / telemetry 7 / storage 4 / service-worker 17 / query-cache 21 / platform 22 / http 27) |
|
||||
| `platform/` | 배럴은 만든다. 단 **어댑터→커널 간선은 파일 직접 import를 유지**한다 (게이트가 그걸 요구함) |
|
||||
| `storage/` | 최상위 배럴은 **서브폴더를 재수출하지 않는다**. `indexeddb/index.ts`·`opfs/index.ts`가 곧 경계다 |
|
||||
| `http/` | V3(`createContractHttpExecutor`)가 권장 경로, V2(`createHttpClient`)는 legacy 보존. 둘 다 배럴에 넣고 주석으로 표시 |
|
||||
| 치환할 import | `src/` 15줄 (배럴 경유 2줄은 이미 합격) + `.storybook/` 2줄(선택) |
|
||||
| 게이트 | `.dependency-cruiser.json`에 규칙 1개 추가. `tests/`는 대상 아님(스캔 범위가 `src`뿐) |
|
||||
| 최대 위험 | 번들 예산. `package.json`에 `sideEffects` 선언이 없어 배럴이 초기 청크를 키울 수 있다 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 기존 배럴 8개의 지배적 관례
|
||||
|
||||
읽은 파일: `src/adapters/{browser-files,browser-file-storage,browser-rpc,browser-transfer,cache-storage,cross-context-invalidation,realtime,web-push}/index.ts`
|
||||
추가로 서브배럴 8개: `browser-transfer/{image-cdn,presigned,resumable-upload}/index.ts`, `realtime/{polling,sse,websocket}/index.ts`, `storage/{indexeddb,opfs}/index.ts`
|
||||
|
||||
### 1.1 관례 (문장으로)
|
||||
|
||||
1. **소스 파일 단위로 블록을 만들고, 블록마다 `export { ... } from "./파일.ts";` 로 이름을 전부 적는다.**
|
||||
블록이 전부 타입이면 `export type { ... } from "...";` 형태를 쓴다 (`browser-files/index.ts:1`, `:28`, `:52`, `:53`).
|
||||
2. **블록 안 순서는 「값 먼저, 타입 나중」이고 각각 대소문자 무시 알파벳순이다.**
|
||||
근거: `web-push/index.ts:42-51` — `createLinkedAbortController, failureCode, nativeFailure, observeWebPush, systemTimeoutScheduler, withAbortableDeadline, type LinkedAbortController, type TimeoutScheduler`.
|
||||
상수도 값이므로 같은 줄에 섞인다: `realtime/index.ts:13-22` — `parseRetryAfterDelay, REALTIME_RECONNECT_CEILINGS, reconnectBudgetRemaining` (대소문자 무시로 `parse < realtime_ < reconnect`).
|
||||
타입은 인라인 `type X` 접두어로 쓴다 (`browser-rpc/index.ts:9-18`).
|
||||
3. **블록(파일) 순서는 대체로 알파벳순이되 엄격하지 않다.** 하위 폴더 블록은 뒤로 몰아둔다 (`web-push/index.ts:59`, `:66`의 `./inbound/*`).
|
||||
엄격하지 않은 실례: `cross-context-invalidation/index.ts`는 `browser-cross-context-invalidation.ts`(:1) 다음에 `browser-cross-context-host.ts`(:20) — 역순.
|
||||
4. **배럴은 그룹의 전체 export 목록이 아니다.** 그룹 안에 배럴에 없는 파일이 실제로 존재한다.
|
||||
근거: `src/adapters/realtime/result.ts`는 export를 가지지만 `realtime/index.ts` 어디에도 없다. `browser-files/browser-file-vault.ts`도 `browser-files/index.ts`에 없다.
|
||||
→ 즉 이 레포는 이미 "배럴 = 선별된 공개 표면"을 실천하고 있다. 새 배럴도 같은 기준으로 고르면 된다.
|
||||
5. (참고, 따라하지 말 것) `browser-files/index.ts:1-16`은 어댑터가 아니라 **application 포트 타입**을 재수출한다. 배럴이 하위 레이어의 통로가 되는 형태라 새 배럴에서는 재현하지 않는다. 필요하면 소비자가 포트에서 직접 가져오면 된다.
|
||||
|
||||
### 1.2 `export *`는 표준인가 — 판정
|
||||
|
||||
**표준은 명명 재수출이다. `export *`는 예외가 아니라 "서브배럴 합성" 전용 관용구다.**
|
||||
|
||||
근거:
|
||||
|
||||
- `export *`가 쓰인 곳은 단 두 파일, 여섯 줄이다.
|
||||
- `src/adapters/browser-transfer/index.ts:1-3` — `./image-cdn/index.ts`, `./presigned/index.ts`, `./resumable-upload/index.ts`
|
||||
- `src/adapters/realtime/index.ts:56-58` — `./polling/index.ts`, `./sse/index.ts`, `./websocket/index.ts`
|
||||
- **여섯 줄 전부 대상이 `index.ts`(서브배럴)다.** 구현 파일(`.ts`)을 `export *`로 푼 사례는 0건이다.
|
||||
- 그리고 그 서브배럴들은 자기 자신이 전부 명명 재수출이다 (`browser-transfer/image-cdn/index.ts:1-6`, `realtime/sse/index.ts:1-9` 등).
|
||||
- `realtime/index.ts`는 한 파일 안에서 두 형태를 동시에 쓴다: 1-55줄은 구현 파일에 대한 명명 재수출, 56-58줄은 서브배럴에 대한 `export *`. 즉 `browser-transfer`만 특이한 게 아니라, **대상이 서브배럴이냐 구현 파일이냐**가 형태를 가른다.
|
||||
|
||||
왜 이 구분이 옳은가: 지시대로 `export *`는 "무엇이 공개되는지 파일을 열어야 안다"는 문제가 있다. 그런데 대상이 서브배럴이면 그 파일 자체가 이미 명시적 목록이므로, `export *` 한 줄을 따라가면 곧바로 명시적 목록에 도달한다. 목록이 사라지는 게 아니라 한 단계 아래에 있는 것뿐이다. 반대로 구현 파일을 `export *`하면 목록이 어디에도 없어진다.
|
||||
|
||||
**채택 규칙**
|
||||
|
||||
> 배럴은 구현 파일(`*.ts`)에 대해서는 반드시 이름을 하나씩 적는다.
|
||||
> `export *`는 대상이 같은 그룹의 서브배럴(`*/index.ts`)일 때만 쓴다.
|
||||
|
||||
새로 만드는 8개 중 `export *`를 쓸 자리는 **없다** (아래 §3.7에서 `storage`가 서브배럴을 재수출하지 않기로 판정하므로).
|
||||
|
||||
---
|
||||
|
||||
## 2. 공개/내부 판정 기준
|
||||
|
||||
판정 근거는 실제 사용처다. 측정 명령:
|
||||
|
||||
```
|
||||
grep -rnE 'from "[^"]*adapters/(auth|browser-files|browser-file-storage|browser-rpc|browser-transfer|cache-storage|cross-context-invalidation|diagnostics|http|platform|query-cache|realtime|service-worker|storage|telemetry|web-push)/' src/ --include='*.ts' --include='*.tsx' | grep -v '^src/adapters/'
|
||||
```
|
||||
|
||||
- **공개**: `src/bootstrap/**`, `src/features/**`, `src/presentation/**`이 import하는 심볼 + 그 심볼의 시그니처에 이름으로 등장하는 타입(의존성/옵션/반환 파사드).
|
||||
- **내부**: 그룹 안에서만 쓰이는 헬퍼.
|
||||
- **내부(테스트 전용)**: `tests/`만 import하는 심볼. 배럴에 넣지 않고, 테스트는 깊은 경로를 유지한다. 각 그룹에서 별도로 표시했다.
|
||||
|
||||
예외 처리 하나: 같은 파일의 동급 팩토리 형제는 오늘 테스트만 쓰더라도 공개로 올린다(예 `createAnonymousSessionAdapter`). 근거는 §3.1.
|
||||
|
||||
---
|
||||
|
||||
## 3. 새로 만들 배럴 8개 (전문)
|
||||
|
||||
아래 내용은 전부 **그대로 파일로 저장 가능**하다. 모든 심볼은 기계 대조로 존재를 확인했다(82개 전수, §7).
|
||||
|
||||
### 3.1 `src/adapters/auth/index.ts`
|
||||
|
||||
그룹 파일: `external-session-adapter.ts` 1개.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createExternalAuthSessionAdapter` | `external-session-adapter.ts:51` | 공개 | `src/bootstrap/runtime-adapters.ts:3` |
|
||||
| `createDemoSessionAdapter` | `:104` | 공개 | `src/bootstrap/runtime-adapters.ts:2`, `:279` |
|
||||
| `createUnavailableSessionAdapter` | `:140` | 공개 | `src/bootstrap/runtime-adapters.ts:4` |
|
||||
| `createAnonymousSessionAdapter` | `:77` | 공개(승격) | 위 셋과 같은 파일·같은 반환형(`AuthSessionPort`)의 형제 팩토리. 오늘 `src` 소비자는 `.storybook/preview.tsx:5`뿐이라 배럴이 없으면 깊은 경로가 남는다 |
|
||||
| `DEMO_AUTHORIZATION_MARKER` | `:98` | 공개 | 계약 문서가 이름으로 참조: `docs/architecture/decisions/VD-23-api-transport-selection-and-rest-execution.md:305` |
|
||||
| `ExternalSessionOwner` | `:10` | 공개 | `src/bootstrap/runtime-adapters.ts:5` |
|
||||
| `DemoSessionAdapter` | `:89` | 공개 | `createDemoSessionAdapter`의 반환 타입 |
|
||||
| `validateCredentialPatch` | `:26` | **내부** | `src/`·`tests/` 어디서도 import하지 않음 |
|
||||
|
||||
```ts
|
||||
export {
|
||||
createAnonymousSessionAdapter,
|
||||
createDemoSessionAdapter,
|
||||
createExternalAuthSessionAdapter,
|
||||
createUnavailableSessionAdapter,
|
||||
DEMO_AUTHORIZATION_MARKER,
|
||||
type DemoSessionAdapter,
|
||||
type ExternalSessionOwner,
|
||||
} from "./external-session-adapter.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 `src/adapters/diagnostics/index.ts`
|
||||
|
||||
그룹 파일: `bounded-diagnostics.ts` 1개. export 5개 전부 공개.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createDiagnosticsAdapter` | `bounded-diagnostics.ts:18` | 공개 | `src/bootstrap/runtime-adapters.ts:7` |
|
||||
| `recordBootFailure` | `:80` | 공개 | `src/bootstrap/main.tsx:3` |
|
||||
| `getLastBootEvidence` | `:108` | 공개 | `recordBootFailure`의 짝(부팅 증거 읽기). 오늘 사용처는 `tests/`만 |
|
||||
| `noOpDiagnostics` | `:11` | 공개 | `DiagnosticsPort`의 null object. 다른 그룹이 기본값으로 기대하는 형태 |
|
||||
| `MAX_DIAGNOSTIC_ENTRIES` | `:16` | 공개 | 선언된 상한값(계약 수치) |
|
||||
|
||||
```ts
|
||||
export {
|
||||
createDiagnosticsAdapter,
|
||||
getLastBootEvidence,
|
||||
MAX_DIAGNOSTIC_ENTRIES,
|
||||
noOpDiagnostics,
|
||||
recordBootFailure,
|
||||
} from "./bounded-diagnostics.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 `src/adapters/telemetry/index.ts`
|
||||
|
||||
그룹 파일: `best-effort-telemetry.ts` 1개.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createTelemetryAdapter` | `best-effort-telemetry.ts:52` | 공개 | `src/bootstrap/runtime-adapters.ts:29` |
|
||||
| `noOpTelemetry` | `:31` | 공개 | null object |
|
||||
| `MAX_TELEMETRY_QUEUE` | `:47` | 공개 | 선언된 상한값 |
|
||||
| `TelemetryAdapter` | `:10` | 공개 | 팩토리 반환 타입 |
|
||||
| `TelemetryAdapterOptions` | `:20` | 공개 | 팩토리 인자 타입 |
|
||||
| `safeTraceparent` | `:229` | **내부(테스트 전용)** | `tests/`만 import. traceparent 정규화는 어댑터 내부 동작 |
|
||||
| `assertBoundedCapacity` 재수출 | `:49` | **배럴에 넣지 않음** | 이건 telemetry의 export가 아니라 **커널(`../platform/bounded-capacity.ts`) 심볼의 재수출**이다. 배럴에 올리면 커널로 가는 두 번째 문이 생긴다 |
|
||||
|
||||
> **부수 발견 / 별도 처리 권고**
|
||||
> `src/adapters/telemetry/best-effort-telemetry.ts:49`
|
||||
> ```ts
|
||||
> export { assertBoundedCapacity } from "../platform/bounded-capacity.ts";
|
||||
> ```
|
||||
> 이 줄은 이 배럴 작업과 무관하게 지우는 게 맞다. 같은 파일 `:8`에서 이미 같은 심볼을 import해서 `:61`에서 쓰고 있으므로 재수출은 순수 잉여이고, 실제 소비자도 없다(`grep -rn 'assertBoundedCapacity' src/ tests/` → `platform/bounded-capacity.ts` 정의부, `telemetry:8,49,61`, `diagnostics:9,25`가 전부). 이 문서 범위에서는 **건드리지 않고** 배럴에서 제외만 한다.
|
||||
|
||||
```ts
|
||||
export {
|
||||
createTelemetryAdapter,
|
||||
MAX_TELEMETRY_QUEUE,
|
||||
noOpTelemetry,
|
||||
type TelemetryAdapter,
|
||||
type TelemetryAdapterOptions,
|
||||
} from "./best-effort-telemetry.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 `src/adapters/platform/index.ts` — 커널
|
||||
|
||||
#### 판정: 배럴은 만들되, **어댑터→커널 간선은 파일 직접 import를 유지한다.**
|
||||
|
||||
질문은 "`browser-transfer/presigned/presigned-transfer-executor.ts:15`의 `../../platform/abortable-operation.ts`가 `../../platform/index.ts`로 바뀌어야 하는가"였다. **아니다. 바꾸면 게이트가 깨진다.**
|
||||
|
||||
**근거 1 (결정적) — `check:adapter-inventory`가 파일 경로를 직접 검증한다.**
|
||||
`scripts/check-adapter-inventory.ts:63-83`:
|
||||
|
||||
```ts
|
||||
const PRIMITIVE_PATH = path.resolve("src/adapters/platform/abortable-operation.ts");
|
||||
for (const consumer of REQUIRED_ABORT_CONSUMERS) {
|
||||
...
|
||||
const specifiers = [...source.matchAll(/from\s+"([^"]*platform\/abortable-operation\.ts)"/gu)]...;
|
||||
const resolved = specifiers.some(
|
||||
(specifier) => path.resolve(path.dirname(consumer), specifier) === PRIMITIVE_PATH,
|
||||
);
|
||||
if (!resolved) { problems.push(`abortable-operation: ${consumer} does not resolve...`); }
|
||||
}
|
||||
```
|
||||
|
||||
`REQUIRED_ABORT_CONSUMERS`(`scripts/check-adapter-inventory.ts:134-137`)는 정확히 네 파일이다:
|
||||
`browser-transfer/presigned/presigned-capability-http-provider.ts`, `.../presigned-transfer-executor.ts`, `browser-transfer/image-cdn/browser-image-probe.ts`, `browser-transfer/resumable-upload/fetch-json-transport.ts`.
|
||||
이들이 `../../platform/index.ts`로 바뀌면 정규식이 매칭되지 않아 `check:adapter-inventory`가 실패한다.
|
||||
|
||||
**근거 2 — 규칙상으로는 둘 다 가능하지만, 커널의 정체성은 "파일"이다.**
|
||||
`.dependency-cruiser.json:191`의 carve-out은 `^src/adapters/($1/|platform/|browser-file-storage/result\.ts$|cross-context-invalidation/index\.ts$)` 로 `platform/` 폴더 전체를 허용한다. 즉 규칙은 중립이다.
|
||||
그런데 `docs/architecture/layers.md:34`는 커널을 "the system clock, the shared abort primitive and the bounded-capacity guard" — **세 개의 프리미티브**로 정의한다. 런타임 합성물이 아니라 원시 도구다. 원시 도구는 "어느 파일에서 왔는지"가 곧 정체성이고, 실제로 위 게이트가 그 정체성을 파일 경로로 확인한다.
|
||||
|
||||
**근거 3 — 그래서 새 게이트 규칙은 `src/adapters/**`를 출발점에서 제외한다.**
|
||||
어댑터끼리의 간선은 이미 `adapters-do-not-know-other-concrete-adapters`(`.dependency-cruiser.json:183-193`)가 담당한다. 새 규칙이 그 위에 겹칠 이유가 없다. §5의 `from.pathNot: "^src/adapters/"`가 이 판정의 실행형이다.
|
||||
|
||||
#### 그러면 배럴에는 뭘 넣나
|
||||
|
||||
커널 프리미티브도 **전부** 넣는다. 이유: 배럴을 쓰는 쪽은 `src/bootstrap`과 `tests`인데, 이들은 `systemClock`(tests), `createAbortableOperation`(tests), `createBrowserLifecycleRuntime`(`src/bootstrap/optional-runtime-host.ts:2`), `createBrowserMutationIntentFactory`(`src/bootstrap/runtime-adapters.ts:28`)를 쓴다. 일부만 넣으면 "바깥은 배럴만"이라는 규칙에 예외가 생긴다. 두 개의 문이 생기는 게 아니라 **문이 소비자별로 하나씩**이다: 어댑터는 파일, 그 외는 배럴.
|
||||
|
||||
| 파일 | 공개 심볼 | 소비자 근거 |
|
||||
|---|---|---|
|
||||
| `abortable-operation.ts` | `createAbortableOperation:90`, `compensateLateHandle:261`, `snapshotAbortTimers:70`, `AbortableOperation:24`, `AbortableOperationInput:51`, `AbortRace:19`, `AbortTerminalReason:11`, `AbortTimerSnapshot:59` | 어댑터 4곳(직접 경로 유지) + `tests/` |
|
||||
| `bounded-capacity.ts` | `assertBoundedCapacity:10` | `diagnostics:9`, `telemetry:8` (직접 경로 유지) |
|
||||
| `browser-lifecycle.ts` | `createBrowserLifecycleRuntime:57`, `BrowserLifecycleEvent:20`, `BrowserLifecycleRuntime:36`, `BrowserLifecycleSnapshot:13` | `src/bootstrap/optional-runtime-host.ts:1-4` |
|
||||
| `browser-mutation-intent-factory.ts` | `createBrowserMutationIntentFactory:9`, `BrowserMutationIntentFactoryDependencies:4` | `src/bootstrap/runtime-adapters.ts:28` |
|
||||
| `system-clock.ts` | `systemClock:3` | 어댑터 6곳(직접 경로 유지) + `tests/` |
|
||||
|
||||
내부: 없음. 이 그룹은 모든 export가 커널 표면이다.
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 어댑터 커널의 공개 경계.
|
||||
*
|
||||
* `src/adapters/**` 안에서는 이 배럴을 쓰지 않는다. 커널 프리미티브는 파일
|
||||
* 경로로 직접 import한다 — `scripts/check-adapter-inventory.ts`가
|
||||
* `platform/abortable-operation.ts`로 해석되는 specifier를 네 소비자에게
|
||||
* 요구하고, `.dependency-cruiser.json`의 kernel carve-out도 폴더 단위다.
|
||||
* 이 배럴은 bootstrap·features·tests 같은 그룹 바깥 소비자를 위한 문이다.
|
||||
*/
|
||||
export {
|
||||
compensateLateHandle,
|
||||
createAbortableOperation,
|
||||
snapshotAbortTimers,
|
||||
type AbortableOperation,
|
||||
type AbortableOperationInput,
|
||||
type AbortRace,
|
||||
type AbortTerminalReason,
|
||||
type AbortTimerSnapshot,
|
||||
} from "./abortable-operation.ts";
|
||||
export { assertBoundedCapacity } from "./bounded-capacity.ts";
|
||||
export {
|
||||
createBrowserLifecycleRuntime,
|
||||
type BrowserLifecycleEvent,
|
||||
type BrowserLifecycleRuntime,
|
||||
type BrowserLifecycleSnapshot,
|
||||
} from "./browser-lifecycle.ts";
|
||||
export {
|
||||
createBrowserMutationIntentFactory,
|
||||
type BrowserMutationIntentFactoryDependencies,
|
||||
} from "./browser-mutation-intent-factory.ts";
|
||||
export { systemClock } from "./system-clock.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 `src/adapters/query-cache/index.ts`
|
||||
|
||||
파일 5개, 파일마다 팩토리 1~3개. 전부 공개(4개는 bootstrap이 직접, `createCursorPaginationRuntime`·`createQueryCacheAdapter`는 동급 팩토리).
|
||||
|
||||
| 심볼 | 위치 | 근거 |
|
||||
|---|---|---|
|
||||
| `createConditionalValidatorStore` | `conditional-validator-store.ts:55` | `src/bootstrap/runtime-adapters.ts:26` |
|
||||
| `ConditionalValidatorBinding` / `ConditionalValidatorStore` | `:3` / `:10` | 위 팩토리의 반환·요소 타입 |
|
||||
| `createCursorPaginationRuntime` | `cursor-pagination-runtime.ts:49` | 동급 런타임 팩토리(오늘 소비자는 `tests/`만) |
|
||||
| `createServerStateScopeRuntime` | `server-state-scope-runtime.ts:34` | `src/bootstrap/runtime-adapters.ts:25` |
|
||||
| `ScopeResetParticipant` / `ServerStateScopeDependencies` | `:19` / `:26` | 위 팩토리의 인자 타입 |
|
||||
| `createTanStackCacheCoordinator` | `tanstack-cache-coordinator.ts:33` | `src/bootstrap/runtime-adapters.ts:22` |
|
||||
| `TanStackCacheCoordinatorDependencies` | `:21` | 인자 타입 |
|
||||
| `createQueryClient` | `tanstack-query-cache.ts:21` | `src/bootstrap/runtime-adapters.ts:24`, `.storybook/preview.tsx:6` |
|
||||
| `createQueryCacheAdapter` | `:59` | 동급 팩토리(`QueryCachePort` 구현) |
|
||||
| `QUERY_CACHE_DEFAULTS` | `:12` | 선언된 기본값 |
|
||||
| `QueryCacheDependencies` | `:8` | 인자 타입 |
|
||||
|
||||
내부: 없음.
|
||||
|
||||
> 주의: 이 그룹은 `.dependency-cruiser.json:184`가 명시적으로 이름 붙인 미해결 간선의 출발점이다 — `tanstack-cache-coordinator.ts:19`가 `../cross-context-invalidation/index.ts`에서 협력자 타입 2개를 읽는다. 배럴 작업은 이 간선을 건드리지 않는다(어댑터→어댑터 간선이므로 새 규칙 범위 밖).
|
||||
|
||||
```ts
|
||||
export {
|
||||
createConditionalValidatorStore,
|
||||
type ConditionalValidatorBinding,
|
||||
type ConditionalValidatorStore,
|
||||
} from "./conditional-validator-store.ts";
|
||||
export { createCursorPaginationRuntime } from "./cursor-pagination-runtime.ts";
|
||||
export {
|
||||
createServerStateScopeRuntime,
|
||||
type ScopeResetParticipant,
|
||||
type ServerStateScopeDependencies,
|
||||
} from "./server-state-scope-runtime.ts";
|
||||
export {
|
||||
createTanStackCacheCoordinator,
|
||||
type TanStackCacheCoordinatorDependencies,
|
||||
} from "./tanstack-cache-coordinator.ts";
|
||||
export {
|
||||
createQueryCacheAdapter,
|
||||
createQueryClient,
|
||||
QUERY_CACHE_DEFAULTS,
|
||||
type QueryCacheDependencies,
|
||||
} from "./tanstack-query-cache.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.6 `src/adapters/service-worker/index.ts`
|
||||
|
||||
#### 제약 하나 먼저
|
||||
|
||||
`tsconfig.app.json:14-19`의 `exclude`에 `src/adapters/service-worker/service-worker-entry.ts`가 들어 있다. **배럴은 이 파일을 절대 참조하면 안 된다** — 참조하면 app 타입체크가 제외된 파일을 끌어들인다. 다행히 `service-worker-entry.ts`는 export가 0개라(`grep -n '^export ' → 없음`) 넣을 것도 없다.
|
||||
|
||||
| 파일 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| `service-worker-page-controller.ts` | 공개 | `src/bootstrap/register-service-worker.ts:5`가 `createServiceWorkerPageController`를 씀 |
|
||||
| `service-worker-lifecycle.ts` | 공개 | 워커 realm 진입점(`service-worker-entry.ts:11`)이 합성하는 런타임. 그룹 바깥(워커 번들·tests)이 실제 소비자 |
|
||||
| `service-worker-protocol.ts` | 공개 | 페이지↔워커 메시지 코덱. 양쪽 realm이 공유하는 어휘 |
|
||||
| `service-worker-removal.ts` | **내부** | 소비자는 `service-worker-page-controller.ts:20`(그룹 내) + `tests/`뿐 |
|
||||
| `service-worker-static-assets.ts` | **내부** | 소비자는 `service-worker-lifecycle.ts:17`(그룹 내) + `tests/`뿐 |
|
||||
| `service-worker-entry.ts` | 대상 아님 | export 0개 + `tsconfig.app.json` 제외 |
|
||||
|
||||
내부(테스트 전용)로 남아 깊은 경로를 유지할 심볼: `removeOwnedRegistration`, `purgeOwnedResources`, `isOwnedRegistration`, `expectedServiceWorkerUrls`(`service-worker-removal.ts:33,65,89,138`), `installStaticAssets`, `classifyFetch`, `validateStaticAssetManifest`, `selectCachesToDelete`(`service-worker-static-assets.ts:35,85,119,377`).
|
||||
|
||||
```ts
|
||||
export {
|
||||
createServiceWorkerRuntime,
|
||||
type WorkerClientLike,
|
||||
type WorkerRuntimeConfig,
|
||||
type WorkerScopeLike,
|
||||
} from "./service-worker-lifecycle.ts";
|
||||
export {
|
||||
createServiceWorkerPageController,
|
||||
type ActivationBlocker,
|
||||
type PageControllerDependencies,
|
||||
} from "./service-worker-page-controller.ts";
|
||||
export {
|
||||
createNonceRegistry,
|
||||
createServiceWorkerMessage,
|
||||
parseServiceWorkerMessage,
|
||||
type ParsedMessage,
|
||||
} from "./service-worker-protocol.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 `src/adapters/storage/index.ts`
|
||||
|
||||
#### 판정: 최상위 배럴은 서브폴더를 재수출하지 **않는다**. 서브폴더 배럴이 곧 경계다.
|
||||
|
||||
`browser-transfer/index.ts:1-3`과 `realtime/index.ts:56-58`의 선례를 따라 `export * from "./indexeddb/index.ts"`를 넣고 싶어지지만, **storage에서는 그게 게이트를 깬다.**
|
||||
|
||||
**근거 (결정적) — 제거 드릴이 서브폴더만 삭제한다.**
|
||||
`scripts/test-browser-file-storage-runtime-removal.ts:23-34`:
|
||||
|
||||
```ts
|
||||
const runtimePaths = [
|
||||
...
|
||||
"src/adapters/storage/indexeddb",
|
||||
"src/adapters/storage/opfs",
|
||||
...
|
||||
] as const;
|
||||
```
|
||||
|
||||
이 스크립트는 fixture 트리에서 위 경로를 `rm -rf`한 뒤(`:56-61`) `assertNoRuntimeImports(fixtureRoot, runtimeSourceRoots, ...)`를 호출한다(`:129-133`). 그 함수는 `scripts/lib/removal-fixture.ts:251-260`:
|
||||
|
||||
```ts
|
||||
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
|
||||
if (graph.importingFiles.length > 0) {
|
||||
throw new Error(`Removed ${capability} runtime is still imported by: ...`);
|
||||
}
|
||||
```
|
||||
|
||||
즉 `src/adapters/storage/index.ts`가 `./indexeddb/index.ts`를 재수출하면, 삭제 후에도 그 파일이 살아남아 삭제된 런타임을 import하는 상태가 되어 드릴이 **즉시 예외로 실패**한다. 그 뒤에 이어지는 `check:types` / `lint` / `check:architecture` / `build`(`:135-143`)까지 전부 못 간다.
|
||||
|
||||
**대조 — realtime·browser-transfer는 왜 괜찮은가.**
|
||||
`scripts/test-realtime-runtime-removal.ts:21-31`은 `src/adapters/realtime` **폴더 전체**를 지운다. `browser-file-storage` 드릴도 `src/adapters/browser-transfer` 전체를 지운다(`test-browser-file-storage-runtime-removal.ts:28`). 배럴이 폴더와 함께 사라지므로 문제가 없다. **storage만 부분 삭제 대상**이다 — 최상위 `browser-storage-adapter.ts`(localStorage/sessionStorage KV)는 남고 IndexedDB·OPFS 런타임만 빠진다.
|
||||
|
||||
**따라서:**
|
||||
- `src/adapters/storage/index.ts` = 최상위 파일들만.
|
||||
- `src/adapters/storage/indexeddb/index.ts`, `src/adapters/storage/opfs/index.ts` = 각 제거 가능 런타임의 공개 경계. **이미 존재하고 이미 명명 재수출이다.** 새로 만들 필요 없다.
|
||||
- §5의 게이트 정규식은 그래서 1단계 중첩 `index.ts`까지 배럴로 인정해야 한다.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createBrowserStorageAdapter` | `browser-storage-adapter.ts:35` | 공개 | `src/bootstrap/runtime-adapters.ts:27` |
|
||||
| `BrowserStorageDependencies` | `:20` | 공개 | 팩토리 인자 타입. 필드가 전부 원시형/포트라 codec 타입을 노출하지 않는다(`:20-27` 확인) |
|
||||
| `browser-storage-codec.ts` 전체 (`DEFAULT_BROWSER_STORAGE_MAX_SERIALIZED_BYTES:1`, `encodeBrowserStorageEnvelope:31`, `decodeBrowserStorageEnvelope:57`, `assertValidBrowserStorageByteLimit:88`, `BrowserStorageEnvelope:11`, `BrowserStorageCodecFailure:17`, `BrowserStorageCodecResult:22`) | — | **내부** | 유일한 소비자가 `browser-storage-adapter.ts:14-18`(그룹 내). `src/`·`tests/` 어디서도 직접 import 없음 |
|
||||
|
||||
```ts
|
||||
export {
|
||||
createBrowserStorageAdapter,
|
||||
type BrowserStorageDependencies,
|
||||
} from "./browser-storage-adapter.ts";
|
||||
```
|
||||
|
||||
> **서브배럴 보완 권고 (별건, 이 문서 범위 밖)**
|
||||
> `tests/unit/opfs-byte-store.test.ts:25,28`가 `OPFS_WORKER_PROTOCOL_VERSION`(`opfs/opfs-worker-protocol.ts:23`), `PreparePhysicalObjectRequest`(`:179`), `writeWithSyncAccessHandle`(`opfs/opfs-worker-runtime.ts:1363`)를 쓰는데 이 셋은 `opfs/index.ts`에 없다. 테스트를 배럴로 옮길 때(§4.2) 함께 결정해야 한다 — 올리거나, 내부로 확정하고 테스트가 깊은 경로를 유지하거나.
|
||||
|
||||
---
|
||||
|
||||
### 3.8 `src/adapters/http/index.ts`
|
||||
|
||||
#### V2 / V3 판정
|
||||
|
||||
**V3(`createContractHttpExecutor`, `http-execution-v3.ts:377`)가 권장 경로.** V2(`createHttpClient`, `client.ts:138`)는 legacy 보존.
|
||||
|
||||
근거:
|
||||
1. `src/adapters/http/client.ts:121`에 `LegacyHttpInput`이라는 타입이 있고, 공개 시그니처 `HttpClient.execute`(`:131-137`)가 그걸 두 번째 인자로 받는다. 파일이 스스로 legacy라고 말한다.
|
||||
2. `src/adapters/http/http-execution-v3.ts:45-51` 헤더 주석: "§7–§8. Descriptor-driven HTTP execution. ... the runtime owns bounds, the total deadline, the single retry authority and the effect-certainty verdict." — 계약 기반 실행이 V3에 있다.
|
||||
3. 라이브 경로가 V3다. `src/bootstrap/runtime-adapters.ts:423`이 `createContractHttpExecutor`를 조립해 실제 런타임에 넣는다. 반면 V2 래퍼 `createRuntimeHttpClient`(`src/bootstrap/runtime-adapters.ts:163`)의 유일한 호출자는 `tests/unit/runtime-adapters.test.ts:371`이다 (`grep -rn 'createRuntimeHttpClient' src/ tests/` 결과 3줄: 정의 1 + 테스트 2).
|
||||
4. V3 타입은 이미 feature 경계를 넘는다: `src/features/reference-feature/adapters/create-reference-feature-input.ts:9`가 `HttpExecutionOutcome`를 import한다.
|
||||
|
||||
둘 다 배럴에 넣되 **블록 순서로 V3를 먼저 두고 주석으로 표시**한다.
|
||||
|
||||
#### 공개 표면
|
||||
|
||||
| 파일 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| `http-execution-v3.ts` | 공개 | `src/bootstrap/runtime-adapters.ts:10,11`, `src/features/reference-feature/adapters/create-reference-feature-input.ts:9` |
|
||||
| `client.ts` | 공개(legacy) | `src/bootstrap/runtime-adapters.ts:8` |
|
||||
| `http-contract-bridge.ts` | `CredentialPatchOutcome`만 공개 | `http-execution-v3.ts:292`의 공개 시그니처 `attachCredentials(...): Promise<CredentialPatchOutcome> \| CredentialPatchOutcome`에 이름으로 등장 → bootstrap이 그 콜백을 구현한다(`src/bootstrap/runtime-adapters.ts:434` 부근) |
|
||||
| `request-builder.ts` | `OperationRequestInput`만 공개 | `client.ts:133`의 공개 시그니처 `execute(request: string \| OperationRequestInput, ...)`에 이름으로 등장 |
|
||||
| `bounded-body-reader.ts` | **내부** | 소비자는 `bounded-json.ts:1`, `http-execution-v3.ts:18-20`(그룹 내) + `tests/` |
|
||||
| `bounded-json.ts` | **내부** | 소비자는 `client.ts:45`(그룹 내) + `tests/` |
|
||||
| `http-effect-certainty.ts` | **내부** | 소비자는 `http-execution-v3.ts:38-42`(그룹 내) + `tests/` |
|
||||
| `retry-policy.ts` | **내부** | 소비자는 `client.ts:9`, `http-execution-v3.ts:43`(그룹 내) + `tests/` |
|
||||
| `schema-registry.ts` | **내부** | 소비자는 `client.ts:14`(그룹 내) + `tests/` |
|
||||
| `resource-mapper.ts` | **내부** | 소비자는 `client.ts:8`(그룹 내) + `tests/` |
|
||||
| `http-contract-bridge.ts` 나머지 11개 | **내부** | 그룹 내 + `tests/` |
|
||||
| `request-builder.ts` 나머지 2개 (`buildRequestTarget:28`, `RequestTargetResult:14`) | **내부(테스트 전용)** | `tests/`만 |
|
||||
|
||||
```ts
|
||||
/**
|
||||
* §7–§8. 권장 경로는 V3 계약 실행기(`createContractHttpExecutor`)다. 설치된
|
||||
* 계약과 타입 입력을 받아 상한·전체 데드라인·재시도 권한·효과 확실성 판정을
|
||||
* 런타임이 소유한다.
|
||||
*/
|
||||
export {
|
||||
createContractHttpExecutor,
|
||||
type AuthIntegrationFailureReason,
|
||||
type AuthOperationContext,
|
||||
type CancellationOwner,
|
||||
type ContractHttpExecutor,
|
||||
type ContractHttpExecutorDependencies,
|
||||
type HttpContractViolation,
|
||||
type HttpContractViolationKind,
|
||||
type HttpEffectCertainty,
|
||||
type HttpExecutionContext,
|
||||
type HttpExecutionObservation,
|
||||
type HttpExecutionOutcome,
|
||||
type HttpTransportFailure,
|
||||
type SafeResponseMetadata,
|
||||
} from "./http-execution-v3.ts";
|
||||
/** V3 `attachCredentials` 콜백이 반환해야 하는 결과 타입. */
|
||||
export type { CredentialPatchOutcome } from "./http-contract-bridge.ts";
|
||||
/**
|
||||
* V2 legacy. operationId + `LegacyHttpInput`으로 호출하는 범용 클라이언트다.
|
||||
* 새 코드는 위의 V3 실행기를 쓴다. 남아 있는 이유는 계약이 아직 없는
|
||||
* 오퍼레이션을 위한 이행 경로이기 때문이다.
|
||||
*/
|
||||
export {
|
||||
createHttpClient,
|
||||
type HttpClient,
|
||||
type HttpClientDependencies,
|
||||
type HttpFailure,
|
||||
type HttpResult,
|
||||
type LegacyHttpInput,
|
||||
type Scheduler,
|
||||
} from "./client.ts";
|
||||
/** V2 `HttpClient.execute`의 첫 인자 타입. */
|
||||
export type { OperationRequestInput } from "./request-builder.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 호출처 치환 목록
|
||||
|
||||
### 4.1 `src/` — 전수 (17줄)
|
||||
|
||||
측정: §2의 grep. 17줄 중 2줄은 이미 배럴 경유라 변경 없음. 실제 치환 대상 **15줄**.
|
||||
|
||||
| # | 파일:줄 | 현재 specifier | 바뀔 specifier | 비고 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | `src/bootstrap/main.tsx:3` | `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` | `recordBootFailure` |
|
||||
| 2 | `src/bootstrap/optional-runtime-host.ts:4` | `../adapters/platform/browser-lifecycle.ts` | `../adapters/platform/index.ts` | 블록 시작은 `:1` |
|
||||
| 3 | `src/bootstrap/register-service-worker.ts:5` | `../adapters/service-worker/service-worker-page-controller.ts` | `../adapters/service-worker/index.ts` | `createServiceWorkerPageController` |
|
||||
| 4 | `src/bootstrap/runtime-adapters.ts:6` | `../adapters/auth/external-session-adapter.ts` | `../adapters/auth/index.ts` | 블록 시작 `:1` |
|
||||
| 5 | `src/bootstrap/runtime-adapters.ts:7` | `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` | `createDiagnosticsAdapter` |
|
||||
| 6 | `src/bootstrap/runtime-adapters.ts:8` | `../adapters/http/client.ts` | `../adapters/http/index.ts` | **7번과 한 블록으로 합칠 것** |
|
||||
| 7 | `src/bootstrap/runtime-adapters.ts:12` | `../adapters/http/http-execution-v3.ts` | `../adapters/http/index.ts` | 블록 시작 `:9` |
|
||||
| 8 | `src/bootstrap/runtime-adapters.ts:20` | `../adapters/cross-context-invalidation/index.ts` | (변경 없음) | 이미 배럴 |
|
||||
| 9 | `src/bootstrap/runtime-adapters.ts:23` | `../adapters/query-cache/tanstack-cache-coordinator.ts` | `../adapters/query-cache/index.ts` | 블록 시작 `:21`. **9~12를 한 블록으로 합칠 것** |
|
||||
| 10 | `src/bootstrap/runtime-adapters.ts:24` | `../adapters/query-cache/tanstack-query-cache.ts` | `../adapters/query-cache/index.ts` | `createQueryClient` |
|
||||
| 11 | `src/bootstrap/runtime-adapters.ts:25` | `../adapters/query-cache/server-state-scope-runtime.ts` | `../adapters/query-cache/index.ts` | `createServerStateScopeRuntime` |
|
||||
| 12 | `src/bootstrap/runtime-adapters.ts:26` | `../adapters/query-cache/conditional-validator-store.ts` | `../adapters/query-cache/index.ts` | `createConditionalValidatorStore` |
|
||||
| 13 | `src/bootstrap/runtime-adapters.ts:27` | `../adapters/storage/browser-storage-adapter.ts` | `../adapters/storage/index.ts` | `createBrowserStorageAdapter` |
|
||||
| 14 | `src/bootstrap/runtime-adapters.ts:28` | `../adapters/platform/browser-mutation-intent-factory.ts` | `../adapters/platform/index.ts` | `createBrowserMutationIntentFactory` |
|
||||
| 15 | `src/bootstrap/runtime-adapters.ts:29` | `../adapters/telemetry/best-effort-telemetry.ts` | `../adapters/telemetry/index.ts` | `createTelemetryAdapter` |
|
||||
| 16 | `src/bootstrap/server-state-generation-store.ts:3` | `../adapters/cross-context-invalidation/index.ts` | (변경 없음) | 이미 배럴 |
|
||||
| 17 | `src/features/reference-feature/adapters/create-reference-feature-input.ts:9` | `../../../adapters/http/http-execution-v3.ts` | `../../../adapters/http/index.ts` | `type HttpExecutionOutcome` |
|
||||
|
||||
**작업 순서 주의.** `runtime-adapters.ts`는 12줄이 한 덩어리(`:6`~`:29`)다. 6·7을 합치고 9~12를 합치면 그 아래 줄 번호가 전부 밀린다. **아래에서 위로 편집**하거나 `:1`~`:29` 블록을 한 번에 다시 쓴다.
|
||||
|
||||
**선택 사항 (게이트 범위 밖, 일관성 목적).** `check:architecture`는 `src`만 스캔하므로(`scripts/check-architecture.ts:69`, `:116-123`) 아래 두 줄은 강제되지 않는다. 같은 커밋에서 정리하는 걸 권한다.
|
||||
|
||||
| 파일:줄 | 현재 | 바뀔 것 |
|
||||
|---|---|---|
|
||||
| `.storybook/preview.tsx:5` | `../src/adapters/auth/external-session-adapter.ts` | `../src/adapters/auth/index.ts` |
|
||||
| `.storybook/preview.tsx:6` | `../src/adapters/query-cache/tanstack-query-cache.ts` | `../src/adapters/query-cache/index.ts` |
|
||||
|
||||
**곁다리 정리 기회 하나.** `src/bootstrap/runtime-adapters.ts:65`가 `type HttpClientDependencies = Parameters<typeof createHttpClient>[0];`로 타입을 역추출한다. 배럴이 `HttpClientDependencies`를 직접 내보내므로 이 줄은 지우고 import로 대체할 수 있다. 필수는 아니다.
|
||||
|
||||
### 4.2 `tests/` — 그룹별 집계와 권고
|
||||
|
||||
측정(2026-09-16, `develop` `5434760`):
|
||||
|
||||
```
|
||||
grep -rnE 'from "[^"]*adapters/<16개 그룹>/' tests/ --include='*.ts' --include='*.tsx' | grep -v '^tests/fixtures/'
|
||||
```
|
||||
|
||||
**166줄 / 83개 파일.** (`tests/fixtures` 포함 시 169줄. 지시문의 184와 다른데, `tests/fixtures`는 `.dependency-cruiser.json:208`에서 제외 대상이고 그 안의 `indexeddb-repository.ts` 같은 경로는 존재하지 않는 금지 recipe fixture다.)
|
||||
|
||||
| 그룹 | 줄 수 | 배럴 존재 | 배럴 경유 |
|
||||
|---|---:|---|---:|
|
||||
| browser-transfer | 26 | 기존 | 0 |
|
||||
| realtime | 24 | 기존 | 1 |
|
||||
| http | 22 | **신규** | 0 |
|
||||
| storage | 20 | **신규**(+서브배럴 2) | 0 |
|
||||
| web-push | 17 | 기존 | 0 |
|
||||
| browser-files | 17 | 기존 | 1 |
|
||||
| query-cache | 9 | **신규** | 0 |
|
||||
| auth | 9 | **신규** | 0 |
|
||||
| browser-file-storage | 5 | 기존 | 0 |
|
||||
| service-worker | 4 | **신규** | 0 |
|
||||
| platform | 3 | **신규** | 0 |
|
||||
| browser-rpc | 3 | 기존 | 2 |
|
||||
| diagnostics | 2 | **신규** | 0 |
|
||||
| cross-context-invalidation | 2 | 기존 | 2 |
|
||||
| cache-storage | 2 | 기존 | 0 |
|
||||
| telemetry | 1 | **신규** | 0 |
|
||||
| **합계** | **166** | | **6** |
|
||||
|
||||
배럴 경유 6줄의 정확한 위치:
|
||||
`tests/unit/browser-file-runtime.test.ts:13`, `tests/unit/browser-rpc/browser-rpc-remediation.test.ts:9`, `tests/unit/browser-rpc/browser-rpc-runtime.test.ts:11`, `tests/unit/cross-tab-invalidation.test.ts:21`, `tests/unit/realtime/realtime-reconnect-coordinator.test.ts:20`, `tests/unit/tanstack-cache-coordinator.test.ts:7`.
|
||||
|
||||
#### 권고: 한꺼번에 옮기지 않는다. 파일을 건드릴 때 그 파일 것만 옮긴다.
|
||||
|
||||
이유 셋:
|
||||
|
||||
1. **게이트가 강제하지 않는다.** `check:architecture`는 `src`만 본다(`scripts/check-architecture.ts:69`, `:116-123`). 테스트를 지금 옮겨도 검증되는 게 없고, 안 옮겨도 깨지는 게 없다. 강제되지 않는 대량 변경은 리뷰 비용만 남는다.
|
||||
2. **테스트의 절반 이상이 내부 심볼을 쓴다.** 예: `tests/unit/opfs-byte-store.test.ts:25,28`의 `PreparePhysicalObjectRequest`·`writeWithSyncAccessHandle`은 `opfs/index.ts`에 없다. http의 `retry-policy`·`schema-registry`·`bounded-body-reader` 테스트도 마찬가지로 §3.8에서 내부로 판정한 심볼을 직접 겨눈다. 일괄 치환은 곧 "배럴에 내부 심볼을 밀어넣자"는 압력이 되고, 그러면 배럴이 경계가 아니라 재수출 덤프가 된다.
|
||||
3. **내부 심볼을 직접 겨누는 단위 테스트는 그래도 된다.** 배럴 규칙은 "그룹 바깥 **프로덕션 코드**는 배럴만"이지 "아무도 내부를 못 본다"가 아니다. 테스트는 구현 계약을 검증하는 게 일이다.
|
||||
|
||||
**실행 규칙 (문서에 남길 문장)**
|
||||
|
||||
> `tests/` 아래 어댑터 import는 배럴로 일괄 이관하지 않는다.
|
||||
> 어떤 테스트 파일을 **다른 이유로** 수정하거나 분할할 때, 그 파일이 쓰는 심볼이 해당 그룹 배럴에 있으면 그 파일 안에서만 배럴 경로로 바꾼다.
|
||||
> 배럴에 없는 심볼이면 깊은 경로를 유지한다. 배럴에 추가하고 싶으면 §2 기준으로 "공개"임을 논증하는 게 먼저다.
|
||||
|
||||
우선순위를 굳이 매긴다면: `auth`(9줄, 전부 공개 심볼), `diagnostics`(2줄), `telemetry`(1줄), `query-cache`(9줄) — 이 넷은 배럴이 그룹 표면을 100% 덮으므로 기계적 치환이 가능하다. 21줄. `http`·`storage`·`service-worker`는 내부 심볼 비중이 커서 파일 단위로만 접근한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 게이트로 강제하기
|
||||
|
||||
### 5.1 추가할 규칙 (그대로 붙여넣기)
|
||||
|
||||
`.dependency-cruiser.json`의 `forbidden` 배열에서 **`adapters-do-not-know-other-concrete-adapters` 바로 다음, `no-circular-dependencies` 앞**에 넣는다 (현재 `:193`과 `:194` 사이).
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "adapter-groups-are-reached-through-their-barrel",
|
||||
"comment": "docs/architecture/layers.md §4: 어댑터 그룹의 공개 표면은 그 그룹의 `index.ts`다. 그룹 바깥(bootstrap, features, presentation)은 배럴만 import한다. 배럴이 없던 시절 bootstrap은 어댑터 내부 파일 15곳을 직접 겨눴고, 그래서 어떤 파일이 공개이고 어떤 파일이 내부 헬퍼인지 아무 데도 적혀 있지 않았다. 출발점에서 `src/adapters`를 뺀 이유는 어댑터끼리의 간선은 바로 위 `adapters-do-not-know-other-concrete-adapters`가 이미 담당하고, 커널(`platform/**`)은 파일 단위로 공유되기 때문이다 — `scripts/check-adapter-inventory.ts`가 네 소비자에게 `platform/abortable-operation.ts`로 해석되는 specifier를 직접 요구한다. 도착점에서 1단계 중첩 `index.ts`를 허용한 이유는 `storage/indexeddb`와 `storage/opfs`가 각자 독립적으로 제거 가능한 런타임이고(scripts/test-browser-file-storage-runtime-removal.ts), 그래서 각자의 배럴이 곧 경계이기 때문이다.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/",
|
||||
"pathNot": "^src/adapters/"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/[^/]+/",
|
||||
"pathNot": "^src/adapters/[^/]+/(?:[^/]+/)?index\\.ts$"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
규칙 형태 적합성: `scripts/check-architecture.ts:799-815`의 `validateArchitectureRules`는 `from`에 `path`/`pathNot`, `to`에 `path`/`pathNot`/`circular`만 허용한다. 이 규칙은 그 안에 있다. 정규식은 `new RegExp(pattern, "u")`로 평가되므로(`:739`, `:747`) 비캡처 그룹 `(?:...)`도 문제없다. `$1` 역참조는 쓰지 않았다.
|
||||
|
||||
### 5.2 이 규칙이 무엇을 잡고 무엇을 안 잡는가 — 실측
|
||||
|
||||
레포의 실제 import 그래프(`src` 전체, 상대 specifier 해석)에 규칙을 그대로 돌려본 결과:
|
||||
|
||||
**잡는 것 — 오늘 기준 위반 15건** (= §4.1의 치환 대상 15줄과 정확히 일치)
|
||||
|
||||
```
|
||||
src/bootstrap/main.tsx:3 -> src/adapters/diagnostics/bounded-diagnostics.ts
|
||||
src/bootstrap/optional-runtime-host.ts:4 -> src/adapters/platform/browser-lifecycle.ts
|
||||
src/bootstrap/register-service-worker.ts:5 -> src/adapters/service-worker/service-worker-page-controller.ts
|
||||
src/bootstrap/runtime-adapters.ts:6 -> src/adapters/auth/external-session-adapter.ts
|
||||
src/bootstrap/runtime-adapters.ts:7 -> src/adapters/diagnostics/bounded-diagnostics.ts
|
||||
src/bootstrap/runtime-adapters.ts:8 -> src/adapters/http/client.ts
|
||||
src/bootstrap/runtime-adapters.ts:12 -> src/adapters/http/http-execution-v3.ts
|
||||
src/bootstrap/runtime-adapters.ts:23 -> src/adapters/query-cache/tanstack-cache-coordinator.ts
|
||||
src/bootstrap/runtime-adapters.ts:24 -> src/adapters/query-cache/tanstack-query-cache.ts
|
||||
src/bootstrap/runtime-adapters.ts:25 -> src/adapters/query-cache/server-state-scope-runtime.ts
|
||||
src/bootstrap/runtime-adapters.ts:26 -> src/adapters/query-cache/conditional-validator-store.ts
|
||||
src/bootstrap/runtime-adapters.ts:27 -> src/adapters/storage/browser-storage-adapter.ts
|
||||
src/bootstrap/runtime-adapters.ts:28 -> src/adapters/platform/browser-mutation-intent-factory.ts
|
||||
src/bootstrap/runtime-adapters.ts:29 -> src/adapters/telemetry/best-effort-telemetry.ts
|
||||
src/features/reference-feature/adapters/create-reference-feature-input.ts:9 -> src/adapters/http/http-execution-v3.ts
|
||||
```
|
||||
|
||||
§4.1을 적용하면 이 15건이 0이 된다. **즉 배럴 8개 생성 + import 15줄 치환 + 규칙 추가를 한 커밋에 넣어야 `check:architecture`가 계속 PASS한다.** 순서를 나누면 중간 커밋이 빨간불이 된다.
|
||||
|
||||
**안 잡는 것 (의도대로)**
|
||||
|
||||
| 경우 | 건수 | 왜 안 잡히나 |
|
||||
|---|---:|---|
|
||||
| 같은 그룹 내부 파일끼리 | 229건 중 189건 | `from.pathNot: "^src/adapters/"`가 출발점을 제외 |
|
||||
| 어댑터 → 커널 (`platform/**`, `browser-file-storage/result.ts`, `cross-context-invalidation/index.ts`) | 40건 | 같은 이유. 이 간선들은 `adapters-do-not-know-other-concrete-adapters`(`:183-193`)의 `pathNot` carve-out이 계속 담당 |
|
||||
| 바깥 → 배럴 (이미 합격) | 2건 | `to.pathNot`이 `index.ts`를 면제. `src/bootstrap/runtime-adapters.ts:20`, `src/bootstrap/server-state-generation-store.ts:3` |
|
||||
| `tests/**` | 166줄 전부 | 스캔 범위 밖 (아래 §5.3) |
|
||||
| `src/presentation/adapters/query/**` | — | 경로가 `src/presentation/...`이라 `to.path` `^src/adapters/`에 매칭 자체가 안 됨 |
|
||||
|
||||
세 가지 질문에 대한 명시적 답:
|
||||
|
||||
1. **같은 그룹 내부 import — 허용된다.** 출발점이 `^src/adapters/`이면 규칙이 아예 평가되지 않는다.
|
||||
2. **커널 접근 — 허용된다.** `platform/`은 위와 같은 이유로 통과. `browser-file-storage/result.ts`와 `cross-context-invalidation/index.ts`도 출발점이 어댑터이므로 통과. (참고: `cross-context-invalidation/index.ts`는 마침 `index.ts`라 도착점 면제에도 걸린다 — 이중으로 안전.)
|
||||
3. **`tests/`는 대상이 아니다.** `scripts/check-architecture.ts:69`가 `sourceRoot = resolve(projectRoot, "src")`이고 `:116-123`이 `depcruise src --config ...`를 돌린다. 두 그래프 모두 `src`만 본다.
|
||||
|
||||
### 5.3 기존 규칙과의 충돌 검토
|
||||
|
||||
`.dependency-cruiser.json` 전체(18개 규칙)를 읽고 대조했다.
|
||||
|
||||
| 기존 규칙 | 줄 | 충돌 |
|
||||
|---|---:|---|
|
||||
| `domain-is-framework-neutral` | 4 | 없음. domain→adapters는 어차피 전면 금지 |
|
||||
| `application-does-not-know-concrete-runtime` | 13 | 없음. 동일 |
|
||||
| `presentation-does-not-know-adapters` | 24 | 없음. presentation→adapters 전면 금지가 상위 |
|
||||
| `page-templates-own-layout-only` | 34 | 없음 |
|
||||
| `icon-vendor-is-facade-only` | 44 | 없음 (외부 패키지 대상) |
|
||||
| `adapters-do-not-know-presentation` | 54 | 없음 (방향 반대) |
|
||||
| `feature-*` 4개 | 64–103 | 없음. feature adapters→`src/adapters`는 이 규칙들이 막지 않으므로 새 규칙이 유효하게 작동 (실측 15번째 위반이 그 경우) |
|
||||
| `concrete-adapters-compose-only-in-bootstrap` | 104 | 없음. `src/(domain\|application\|presentation\|contracts)` → `^src/adapters` 전면 금지. 새 규칙은 그 나머지(`bootstrap`, `features`)에서만 실효 |
|
||||
| `external-contract-package-single-import-path` | 114 | 없음 |
|
||||
| `presentation-does-not-fetch-directly` | 126 | 없음. `^src/adapters/(http\|realtime\|service-worker\|web-worker\|storage)` 를 겨누는데 presentation은 이미 전면 금지 |
|
||||
| `generic-worker-has-no-network-or-credentials` | 137 | 없음. `^src/adapters/web-worker` 출발이라 새 규칙 출발점 제외와 겹칠 뿐. (`src/adapters/web-worker`는 현재 존재하지 않는 예방 규칙) |
|
||||
| `service-worker-entry-is-not-page-code` | 148 | 없음 |
|
||||
| `contracts-do-not-know-application` | 159 | 없음 |
|
||||
| `generic-presentation-does-not-compose-the-product` | 170 | 없음 |
|
||||
| **`adapters-do-not-know-other-concrete-adapters`** | **183** | **없음 — 상보적.** 저쪽은 `from: ^src/adapters/([^/]+)/`, 이쪽은 `from.pathNot: ^src/adapters/`. 정확히 반대 집합이라 이중 판정이 생기지 않는다 |
|
||||
| `no-circular-dependencies` | 194 | 주의 필요 → 아래 |
|
||||
|
||||
**순환 검토.** `no-circular-dependencies`(`:194-201`)가 error다. 배럴 도입이 순환을 만들려면 그룹 안 파일이 자기 그룹 `index.ts`를 import해야 한다.
|
||||
|
||||
```
|
||||
grep -rn 'from "\./index\.ts"\|from "\.\./index\.ts"' src/adapters/ --include='*.ts' → 0건
|
||||
```
|
||||
|
||||
또 `src/bootstrap/**`는 어댑터에서 import되지 않는다(`adapters-do-not-know-presentation`이 `^src/(presentation|bootstrap)`을 막음, `:54-62`). 따라서 **새 순환 없음.** 단 §3.7의 판정을 뒤집어 `storage/index.ts`가 서브배럴을 재수출하면, 순환은 아니지만 제거 드릴이 깨진다(§6 위험 2).
|
||||
|
||||
### 5.4 ESLint 쪽은 손대지 않는다
|
||||
|
||||
`eslint.config.ts:9-34`의 `layerPatterns`는 레이어 단위(`**/adapters/**`)만 다루고 그룹 내부 경로를 구분하지 않는다. 배럴 규칙을 여기에도 복제하면 두 곳에서 같은 사실을 관리하게 된다. dependency-cruiser 쪽 한 곳만 유지한다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 위험과 부수 작업
|
||||
|
||||
### 위험 1 (최대) — 번들 예산
|
||||
|
||||
`package.json`에 `"sideEffects"` 필드가 없다. 번들러는 모든 모듈을 부작용 있을 수 있는 것으로 보고, 배럴 재수출을 통해 들어온 모듈을 트리셰이킹에서 살려둘 수 있다.
|
||||
|
||||
구체적으로 위험한 세 곳:
|
||||
- `src/bootstrap/register-service-worker.ts`가 `service-worker/index.ts`를 import하면 `service-worker-lifecycle.ts`(666줄) → `service-worker-static-assets.ts`(394줄)가 **페이지 번들 그래프**에 들어온다. 지금은 `service-worker-page-controller.ts`(494줄) → `service-worker-protocol.ts` + `service-worker-removal.ts`만 들어온다.
|
||||
- `src/bootstrap/runtime-adapters.ts`가 `http/index.ts`를 import하면 V2·V3가 항상 함께 들어온다 (`client.ts` 1107줄 + `http-execution-v3.ts` 1602줄). 지금도 둘 다 import하긴 한다.
|
||||
- `query-cache/index.ts`는 `cursor-pagination-runtime.ts`(234줄)를 추가로 끌어온다.
|
||||
|
||||
예산: `config/performance/budgets.json`의 `bundle.initialJsGzipBytes = 204800`. `check:bundle`(`scripts/check-bundle.ts:47`)이 초과 시 실패한다.
|
||||
|
||||
**대응 (권장 순서)**
|
||||
1. 배럴 커밋에서 `corepack pnpm check:bundle`을 반드시 돌린다. 이 문서에서는 실행하지 않았다.
|
||||
2. 넘치면 `package.json`에 `"sideEffects": false`를 추가한다. `src/adapters` 아래에 최상위 부작용이 있는지 먼저 확인해야 한다(`presentation/styles/theme.css` 같은 CSS import는 `"sideEffects": ["*.css"]` 형태로 보존).
|
||||
3. 그래도 넘치면 `service-worker/index.ts`에서 `service-worker-lifecycle.ts` 블록을 빼고, 워커 realm은 파일 직접 import를 유지한다(`platform`과 같은 논리 — realm이 다르면 문도 다르다).
|
||||
|
||||
### 위험 2 — `storage/index.ts`에 서브배럴을 넣고 싶은 충동
|
||||
|
||||
`browser-transfer`/`realtime` 선례만 보고 `export * from "./indexeddb/index.ts"`를 넣으면 `test:browser-file-storage-removal`이 `assertNoRuntimeImports` 단계에서 예외로 죽는다(§3.7). 배럴 파일에 그 이유를 주석으로 못 박아 두는 걸 권한다.
|
||||
|
||||
### 위험 3 — `check:adapter-inventory`가 새 파일 8개를 거부한다
|
||||
|
||||
`scripts/check-adapter-inventory.ts:58-74`가 `git ls-files src/adapters` 결과와 `docs/reviews/adapters/INVENTORY.md`의 행 목록을 **정확히 일치**시키고, `합계: **N/N**` 숫자도 파일 수와 같아야 한다.
|
||||
|
||||
**같은 커밋에서 해야 할 일:**
|
||||
1. 새 `index.ts` 8개를 `git add` 한다 (추적되지 않으면 `git ls-files`에 안 잡혀서 오히려 통과하지만, 커밋하는 순간 깨진다).
|
||||
2. `docs/reviews/adapters/INVENTORY.md`에 행 8개 추가:
|
||||
|
||||
| 추가할 경로 | 상세 리뷰 링크 (같은 그룹 기존 행과 동일하게) |
|
||||
|---|---|
|
||||
| `src/adapters/auth/index.ts` | `[Network/state](./01-network-and-state.md)` — 기존 행 `:11` |
|
||||
| `src/adapters/diagnostics/index.ts` | `[Network/state](./01-network-and-state.md)` — `:58` |
|
||||
| `src/adapters/http/index.ts` | `[Network/state](./01-network-and-state.md)` — `:59-68` |
|
||||
| `src/adapters/platform/index.ts` | `[Network/state](./01-network-and-state.md)` — `:69-73` |
|
||||
| `src/adapters/query-cache/index.ts` | `[Network/state](./01-network-and-state.md)` — `:74-78` |
|
||||
| `src/adapters/telemetry/index.ts` | `[Network/state](./01-network-and-state.md)` — `:119` |
|
||||
| `src/adapters/service-worker/index.ts` | `[Worker/push](./05-service-worker-and-web-push.md)` — `:96-101` |
|
||||
| `src/adapters/storage/index.ts` | `[Storage/files](./03-storage-and-browser-files.md)` — `:102-103` |
|
||||
|
||||
3. `docs/reviews/adapters/INVENTORY.md:132`의 `합계: **120/120**` → `합계: **128/128**`.
|
||||
|
||||
행 번호(`| N |`)는 `inventoryRows`(`scripts/check-adapter-inventory.ts:33-40`)가 아래 정규식으로 **경로만** 뽑고 순서·연속성은 검사하지 않는다.
|
||||
|
||||
```
|
||||
^\|\s*\d+\s*\|\s*`([^`]+)`\s*\|
|
||||
```
|
||||
|
||||
그래도 읽는 사람을 위해 정렬 위치에 끼워 넣고 번호를 다시 매기는 걸 권한다.
|
||||
|
||||
### 위험 4 — `tsconfig.app.json` 제외 파일
|
||||
|
||||
`tsconfig.app.json:18`이 `src/adapters/service-worker/service-worker-entry.ts`를 제외한다. `service-worker/index.ts`가 이 파일을 참조하면 app 타입체크가 제외 대상을 끌어들인다. §3.6의 배럴은 참조하지 않는다(그 파일은 export가 0개다). 나중에 누가 "완전성"을 이유로 추가하지 않도록 배럴에 주석을 남기는 것도 방법이다.
|
||||
|
||||
### 부수 발견 (이 문서 범위 밖, 별도 티켓)
|
||||
|
||||
1. `src/adapters/telemetry/best-effort-telemetry.ts:49` — 커널 심볼 재수출. 소비자 0, 같은 파일 `:8`에 import가 이미 있음. 삭제 후보.
|
||||
2. `src/adapters/browser-files/index.ts:1-16` — 배럴이 application 포트 타입을 재수출. 어댑터 배럴이 하위 레이어의 통로가 되는 형태.
|
||||
3. `src/adapters/storage/opfs/index.ts` — `tests/unit/opfs-byte-store.test.ts:25,28`이 쓰는 `OPFS_WORKER_PROTOCOL_VERSION`·`PreparePhysicalObjectRequest`·`writeWithSyncAccessHandle` 3개가 빠져 있다.
|
||||
4. `.dependency-cruiser.json:141` — `^src/adapters/web-worker` 를 겨누는 규칙이 있으나 해당 디렉터리는 존재하지 않는다(어댑터 그룹은 16개). 예방 규칙인지 잔재인지 확인 필요.
|
||||
|
||||
---
|
||||
|
||||
## 7. 심볼 존재 검증 (기계 대조)
|
||||
|
||||
제시한 8개 배럴의 **모든 재수출 심볼 82개**를, 각 대상 파일의 실제 `export` 선언과 이름 단위로 대조했다.
|
||||
|
||||
- 대조 방법: 각 `export { ... } from "./X.ts"` 블록의 이름을 뽑아, `src/adapters/<group>/X.ts`에서 `^export (declare )?(async function|function|const|let|var|class|type|interface|enum) <name>` 으로 선언된 이름 집합에 들어 있는지 확인.
|
||||
- 결과: **82/82 존재. 누락 0, 오타 0.**
|
||||
- 대상 파일 존재 여부도 함께 확인(8개 배럴이 참조하는 소스 파일 16개 전부 존재).
|
||||
|
||||
그룹별 심볼 수: auth 7, diagnostics 5, telemetry 5, platform 16, query-cache 13, storage 2, service-worker 11, http 23.
|
||||
|
||||
---
|
||||
|
||||
## 8. 실행 체크리스트 (한 커밋)
|
||||
|
||||
1. `src/adapters/{auth,diagnostics,http,platform,query-cache,service-worker,storage,telemetry}/index.ts` 8개 생성 (§3 내용 그대로).
|
||||
2. §4.1 표의 15줄 치환. `src/bootstrap/runtime-adapters.ts`는 아래에서 위로 편집하거나 `:1-29` 블록 전체를 다시 쓴다.
|
||||
3. (선택) `.storybook/preview.tsx:5,6` 2줄 치환.
|
||||
4. `.dependency-cruiser.json`에 §5.1 규칙 추가 (현재 `:193`과 `:194` 사이).
|
||||
5. `docs/reviews/adapters/INVENTORY.md`에 행 8개 추가 + `:132`의 합계를 `128/128`로.
|
||||
6. `docs/architecture/layers.md`에 "어댑터 그룹의 공개 표면은 그 그룹의 `index.ts`다. 커널은 예외로 파일 단위로 공유된다"를 한 문단 추가 (`:29-45`의 adapter kernel 절 뒤). 이 문서 규칙들은 실행 규칙과 짝을 이루게 되어 있다(`layers.md:39-45`).
|
||||
7. 게이트 실행: `check:architecture` → `check:types:app` → `check:adapter-inventory` → `lint` → `check:bundle` → `test:browser-file-storage-removal`.
|
||||
마지막 두 개가 이번 변경의 실제 리스크 지점이다(§6 위험 1, 2).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,13 +90,39 @@ production Playwright profile은 source fixture가 아니라 `build` + `preview`
|
||||
|
||||
#### TanStack Query의 React integration test 기반
|
||||
|
||||
`tests/component/application-query.test.tsx`는 production query inbound
|
||||
adapter의 query/mutation lifecycle을 검증한다. cancellation, initial terminal
|
||||
failure, background stale-failure latch와 retry 복구, duplicate submit,
|
||||
optimistic commit/rollback, conflict 해제와 namespace invalidation이 실제
|
||||
QueryClient 위에서 실행된다. HTTP 자동 retry가 소유자이므로 이 adapter의
|
||||
production query inbound adapter의 React integration은 behavior owner별
|
||||
component suite로 분리되어 있다.
|
||||
|
||||
- `application-query-bridge.test.tsx`: initial/background state, cancellation
|
||||
- `application-query-scope-fence.test.tsx`: scope commit fence와 result budget
|
||||
- `application-mutation-scope-fence.test.tsx`: mutation scope fence
|
||||
- `application-query.test.tsx`: unknown-effect reconciliation
|
||||
- `application-mutation-admission.test.tsx`: intent/duplicate admission
|
||||
- `application-mutation-optimistic-cache.test.tsx`: optimistic cache commit/rollback
|
||||
- `application-query-fixture.tsx`: 공통 QueryClient/scope fixture
|
||||
|
||||
각 실패 파일명이 깨진 behavior contract를 직접 드러내며, 동일한 52개 계약을
|
||||
실제 QueryClient 위에서 검증한다. HTTP 자동 retry가 소유자이므로 이 adapter의
|
||||
query/mutation vendor retry는 꺼져 있다.
|
||||
|
||||
같은 기준을 대형 unit suite에도 적용한다. 줄 수를 기준으로 자르지 않고 실패가
|
||||
가리켜야 하는 behavior owner를 기준으로 분리한다.
|
||||
|
||||
- `public-response-cache.test.ts`: 일반 stage/activate/lookup/cache contract
|
||||
- `public-response-cache-repair.test.ts`: active release repair의 failure atomicity
|
||||
- `resumable-upload-runtime.test.ts`: upload/reconcile/control-plane 흐름
|
||||
- `resumable-upload-runtime-teardown.test.ts`: bounded drain/raw provider teardown
|
||||
- `security-followup.test.ts`: archived local evidence
|
||||
- `security-provider-evidence.test.ts`: provider signature/supervision/process lifecycle
|
||||
- `security-promotion-staging.test.ts`: private staging seal/replay/CLI identity
|
||||
- `tests/integration/provider-guardian-transaction.test.ts`: child process,
|
||||
filesystem, IPC frame, READY/PUBLISHED handshake와 process lifecycle. pure unit
|
||||
pool에 두지 않으며 handshake budget은 protocol timeout이 아닌 test watchdog이다.
|
||||
- 각 `*-fixture.ts`: 해당 owner들 사이에서만 공유하는 deterministic test fixture
|
||||
|
||||
이 분리는 production owner와 test failure surface를 맞추기 위한 것이며, 단순
|
||||
LOC 감축 목적이 아니다.
|
||||
|
||||
#### Form과 route 위험
|
||||
|
||||
form component/reference feature test가 error summary, 첫 오류 focus, Zod
|
||||
@@ -129,10 +155,26 @@ Chromium, Firefox, WebKit과 compact project로 실행한다. 빠른 Vite 개발
|
||||
|
||||
V8 text/JSON/LCOV를 생성하고 전체 기준과 retry/storage/telemetry/application
|
||||
composition/compatibility/performance/promotion/chunk/diagnostics/reference HTTP
|
||||
operation/query-mutation/registry compatibility 12개 high-risk module에 52개
|
||||
operation/query-mutation/registry compatibility 19개 high-risk module에 80개
|
||||
scoped threshold를 적용한다. critical module 누락 또는 threshold 미달 fixture는
|
||||
merge gate를 실패시킨다.
|
||||
|
||||
Repository inventory의 정적 provenance는 runtime 실행 가능 여부가 아니라 현재
|
||||
Vitest/V8 producer가 file counter를 생성하는 문장의 존재 여부를 나타낸다. 따라서
|
||||
artifact는 `counterBearingTotal`, `instrumentedCounterBearingTotal`,
|
||||
`counterlessTotal`, `counterlessModules`를 기록한다. 실제 microfixture에서
|
||||
`import type {}`, `import {}`, bare side-effect import, value import, named/star value
|
||||
re-export만 있는 모듈은 모두 exact all-zero row였고, 선언/초기화 또는 직접 실행문이
|
||||
있는 모듈은 counter-bearing이었다. Counterless import/re-export도 module evaluation
|
||||
과 side effect를 유발할 수 있으므로 이를 non-executable로 해석하지 않는다. Gate는
|
||||
static counterless 집합과 exact all-zero row 집합의 일치를 요구하고 critical/high-risk
|
||||
policy module이 counterless이면 실패시킨다.
|
||||
|
||||
Coverage policy 입력은 schema version 2를 유지한다. 반면 위 counter-bearing 필드를
|
||||
직렬화하는 risk-coverage 결과 artifact는 schema version 3이다. 두 version은 서로
|
||||
독립적인 계약이며, 실제 CLI contract test가 artifact의 새 필드와 legacy 필드 부재를
|
||||
검증한다.
|
||||
|
||||
남은 범위는 실제 device/browser farm, cloud visual approval, 외부 인증·telemetry
|
||||
provider와 production field data다. 이 증거가 없을 때 저장소 내부 test를
|
||||
`PRODUCTION_READY`의 대체물로 사용하지 않는다.
|
||||
|
||||
+69
-14
@@ -3,14 +3,72 @@
|
||||
Each gate is blocking in its declared scope. Failures are not downgraded with
|
||||
`continue-on-error` or warning-only scripts.
|
||||
|
||||
| Level | Command | Evidence |
|
||||
| --- | --- | --- |
|
||||
| runtime schema | `pnpm test:runtime-schema` | `artifacts/tests/runtime-schema.xml` |
|
||||
| unit | `pnpm test:unit` | `artifacts/tests/unit.xml` |
|
||||
| component | `pnpm test:component` | `artifacts/tests/component.xml` |
|
||||
| integration | `pnpm test:integration` | `artifacts/tests/integration.xml` |
|
||||
| end-to-end | `pnpm test:e2e` | `artifacts/tests/e2e/` |
|
||||
| accessibility | `pnpm test:a11y` | `artifacts/tests/a11y.json` |
|
||||
## Executable levels
|
||||
|
||||
| Level | Command | Ownership / prerequisite | Evidence |
|
||||
| --- | --- | --- | --- |
|
||||
| runtime schema | `pnpm test:runtime-schema` | pure runtime schema contracts | `artifacts/tests/runtime-schema.xml` |
|
||||
| unit | `pnpm test:unit` | Node-only domain/application/pure policy/runtime units; no systemd/bwrap/cgroup prerequisite | `artifacts/tests/unit.xml` |
|
||||
| capability contract | `pnpm test:contract` | reusable capability consumer contracts | `artifacts/tests/contract.xml` |
|
||||
| component | `pnpm test:component` | React/hook/UI behavior | `artifacts/tests/component.xml` |
|
||||
| integration | `pnpm test:integration` | HTTP/MSW, IndexedDB, composed browser-runtime boundaries, child-process/filesystem/IPC integration | `artifacts/tests/integration.xml` |
|
||||
| system / CI runner | `pnpm test:system` | compatible Linux host with systemd, bubblewrap, cgroup v2 and CI-provider process controls | `artifacts/tests/system.xml` |
|
||||
| end-to-end | `pnpm test:e2e` | pinned browser engines | `artifacts/tests/e2e/` |
|
||||
| accessibility | `pnpm test:a11y` | pinned browser engines | `artifacts/tests/a11y.json` |
|
||||
|
||||
`test:all` is the normal product-development loop. It intentionally includes
|
||||
runtime-schema, unit, capability-contract, component, integration, reference
|
||||
feature and recipe suites, but does not include `test:system`. CI-runner and
|
||||
supply-chain assurance has different host prerequisites and is invoked
|
||||
explicitly in the assurance path.
|
||||
|
||||
## Deterministic test process
|
||||
|
||||
All Vitest package scripts launch through `scripts/run-vitest.ts`.
|
||||
|
||||
That runner:
|
||||
|
||||
1. rejects Node versions outside the repository-supported
|
||||
`>=24.11.0 <25.0.0` range before the suite starts,
|
||||
2. owns `NODE_ENV=test` rather than trusting the parent shell,
|
||||
3. removes host-specific `npm_config_userconfig`, `npm_config_prefix` and
|
||||
`npm_config_globalconfig` values before Vitest starts.
|
||||
|
||||
`vitest.config.ts` also fixes `NODE_ENV=test` so a direct Vitest invocation
|
||||
cannot accidentally select React's production behavior.
|
||||
|
||||
The system suite additionally runs
|
||||
`scripts/check-system-test-prerequisites.ts` and fails with one prerequisite
|
||||
report when the CI-runner host does not provide its required Linux facilities.
|
||||
|
||||
## Development paths
|
||||
|
||||
Product feature:
|
||||
|
||||
- focused feature/unit/component test
|
||||
- capability contract test when a reusable boundary changes
|
||||
- type/lint/architecture
|
||||
- `test:all`
|
||||
|
||||
Reusable capability:
|
||||
|
||||
- focused unit tests
|
||||
- capability contract tests
|
||||
- integration tests
|
||||
- type/lint/architecture
|
||||
|
||||
CI / release assurance:
|
||||
|
||||
- `test:system`
|
||||
- supply-chain / promotion / release gates
|
||||
|
||||
A host-level process/sandbox test must not be placed in `tests/unit` merely
|
||||
because it uses Vitest. The classification follows the system boundary and
|
||||
prerequisites, not the test framework. Process-heavy tests that spawn child
|
||||
processes but do not require privileged host facilities belong in
|
||||
`tests/integration`; for example the provider guardian transaction protocol
|
||||
lives at `tests/integration/provider-guardian-transaction.test.ts`. Its
|
||||
handshake timeout is a test watchdog, not a production protocol deadline.
|
||||
|
||||
End-to-end and automated accessibility scenarios run on the pinned Chromium,
|
||||
Firefox, and WebKit engines. The responsive contract explicitly exercises
|
||||
@@ -30,9 +88,6 @@ Promotion is an AND graph:
|
||||
3. release gates plus rollback/runbook drills
|
||||
4. production promotion plus eligible field Web Vitals evidence
|
||||
|
||||
This file describes the currently registered taxonomy. The
|
||||
[frontend platform testing strategy](./frontend-platform-testing-strategy.md)
|
||||
documents the target additions: test TypeScript projects, real bootstrap
|
||||
composition tests, shared MSW scenarios, query/mutation/form/router coverage,
|
||||
Storybook interaction and accessibility checks, visual regression, and a
|
||||
built-output Playwright profile.
|
||||
The [frontend platform testing strategy](./frontend-platform-testing-strategy.md)
|
||||
contains the broader testing design. This file is the executable taxonomy for
|
||||
where a test belongs and which environment is allowed to run it.
|
||||
|
||||
+54
-29
@@ -11,7 +11,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node scripts/build-frontend.ts",
|
||||
"build:release": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security",
|
||||
"build:profile": "node scripts/generate-runtime-config.ts",
|
||||
"build:release-candidate": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security && corepack pnpm verify:release && node scripts/verify-supply-chain-artifacts.ts && node scripts/create-release-candidate.ts",
|
||||
"preview": "vite preview",
|
||||
"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",
|
||||
@@ -19,6 +20,9 @@
|
||||
"check:design-system:fixture": "node scripts/check-design-system.ts --fixture",
|
||||
"check:i18n": "node scripts/check-i18n.ts",
|
||||
"check:i18n:fixture": "node scripts/check-i18n.ts --fixture",
|
||||
"check:adapter-inventory": "node scripts/check-adapter-inventory.ts",
|
||||
"check:remediation-ledger": "node scripts/check-remediation-ledger.ts",
|
||||
"check:release-admission": "node scripts/check-release-admission.ts",
|
||||
"check:diagnostics": "node scripts/check-diagnostics.ts",
|
||||
"check:diagnostics:fixture": "node scripts/check-diagnostics.ts --fixture",
|
||||
"check:types": "corepack pnpm check:types:app && corepack pnpm check:types:node && corepack pnpm check:types:test && corepack pnpm check:types:recipes && corepack pnpm check:types:web-worker && corepack pnpm check:types:service-worker",
|
||||
@@ -26,26 +30,31 @@
|
||||
"check:types:node": "tsc --project tsconfig.node.json",
|
||||
"check:types:test": "tsc --project tsconfig.test.json",
|
||||
"check:types:recipes": "node scripts/check-optional-recipe-types.ts",
|
||||
"check:types:fixture": "tsc --ignoreConfig --strict --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext tests/fixtures/typecheck/invalid-port-call.ts",
|
||||
"check:types:fixture:ts-port": "tsc --ignoreConfig --strict --noEmit --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-port-implementation.ts",
|
||||
"check:types:fixture:ts-result": "tsc --ignoreConfig --strict --noEmit --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-result-narrowing.ts",
|
||||
"check:types:fixture:application-output": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-application-output.ts",
|
||||
"check:types:fixture:application-input": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-application-input.ts",
|
||||
"check:types:fixture:feature-input": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-feature-input.ts",
|
||||
"check:types:fixture:failure-kind": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-failure-kind.ts",
|
||||
"check:types:fixture:reference-operation": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-reference-operation.ts",
|
||||
"check:types:fixture:async-overlay": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-async-overlay.ts",
|
||||
"check:types:fixture:route-runtime": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-route-runtime.ts",
|
||||
"check:types:fixture:page-action": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler --jsx react-jsx tests/fixtures/typecheck/invalid-page-action.tsx",
|
||||
"check:types:fixture:icon-button": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler --jsx react-jsx tests/fixtures/typecheck/invalid-icon-button.tsx",
|
||||
"check:types:fixture:i18n-key": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-message-key.ts",
|
||||
"check:types:fixture:i18n-params": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-message-params.ts",
|
||||
"check:types:fixture:diagnostics": "tsc --ignoreConfig --strict --noEmit --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler tests/fixtures/typecheck/invalid-diagnostics-port.ts",
|
||||
"test:runtime-schema": "vitest run tests/runtime-schema --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/runtime-schema.xml --passWithNoTests",
|
||||
"test:unit": "vitest run tests/unit --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/unit.xml",
|
||||
"test:component": "vitest run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml",
|
||||
"test:integration": "vitest run tests/integration --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/integration.xml",
|
||||
"test:recipes": "vitest run tests/recipes --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/optional-recipes.xml --passWithNoTests",
|
||||
"check:types:fixture": "tsc -p tests/fixtures/typecheck/tsconfig.port-call.json",
|
||||
"check:types:fixture:ts-port": "tsc -p tests/fixtures/typecheck/tsconfig.port-implementation.json",
|
||||
"check:types:fixture:ts-result": "tsc -p tests/fixtures/typecheck/tsconfig.result-narrowing.json",
|
||||
"check:types:fixture:application-output": "tsc -p tests/fixtures/typecheck/tsconfig.application-output.json",
|
||||
"check:types:fixture:application-input": "tsc -p tests/fixtures/typecheck/tsconfig.application-input.json",
|
||||
"check:types:fixture:feature-input": "tsc -p tests/fixtures/typecheck/tsconfig.feature-input.json",
|
||||
"check:types:fixture:failure-kind": "tsc -p tests/fixtures/typecheck/tsconfig.failure-kind.json",
|
||||
"check:types:fixture:reference-operation": "tsc -p tests/fixtures/typecheck/tsconfig.reference-operation.json",
|
||||
"check:types:fixture:async-overlay": "tsc -p tests/fixtures/typecheck/tsconfig.async-overlay.json",
|
||||
"check:types:fixture:route-runtime": "tsc -p tests/fixtures/typecheck/tsconfig.route-runtime.json",
|
||||
"check:types:fixture:page-action": "tsc -p tests/fixtures/typecheck/tsconfig.page-action.json",
|
||||
"check:types:fixture:icon-button": "tsc -p tests/fixtures/typecheck/tsconfig.icon-button.json",
|
||||
"check:types:fixture:i18n-key": "tsc -p tests/fixtures/typecheck/tsconfig.message-key.json",
|
||||
"check:types:fixture:i18n-params": "tsc -p tests/fixtures/typecheck/tsconfig.message-params.json",
|
||||
"check:types:fixture:diagnostics": "tsc -p tests/fixtures/typecheck/tsconfig.diagnostics-port.json",
|
||||
"check:types:fixture:image-resolve-signal": "tsc -p tests/fixtures/typecheck/tsconfig.image-cdn-resolve-signal.json",
|
||||
"test:runtime-schema": "node scripts/run-vitest.ts run tests/runtime-schema --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/runtime-schema.xml --passWithNoTests",
|
||||
"test:unit": "node scripts/run-vitest.ts run tests/unit --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/unit.xml",
|
||||
"test:contract": "node scripts/run-vitest.ts run tests/contract --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/contract.xml",
|
||||
"test:system": "node scripts/check-system-test-prerequisites.ts && node scripts/run-vitest.ts run tests/system --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/system.xml",
|
||||
"test:component": "node scripts/run-vitest.ts run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml",
|
||||
"test:integration": "node scripts/run-vitest.ts run tests/integration --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/integration.xml",
|
||||
"test:http-scenario-catalog": "node scripts/run-vitest.ts run tests/integration/http-scenario-catalog.test.ts --reporter=default --maxWorkers=1",
|
||||
"test:http-scenario-evidence": "node scripts/run-http-scenario-evidence.ts",
|
||||
"test:recipes": "node scripts/run-vitest.ts run tests/recipes --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/optional-recipes.xml --passWithNoTests",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:dev": "playwright test --config playwright.dev.config.ts",
|
||||
"test:browser-capabilities": "playwright test --config playwright.capabilities.config.ts",
|
||||
@@ -56,6 +65,9 @@
|
||||
"test:visual": "playwright test --config playwright.visual.config.ts",
|
||||
"test:visual:update": "playwright test --config playwright.visual.config.ts --update-snapshots",
|
||||
"check:test-evidence": "node scripts/check-test-evidence.ts",
|
||||
"check:test-evidence:browser": "node scripts/check-test-evidence.ts --skip-scenario-executions",
|
||||
"check:http-scenario-evidence": "node scripts/check-test-evidence.ts --scenario-only --artifact artifacts/quality/http-scenario-evidence.json",
|
||||
"check:http-scenario-evidence:fixture": "node scripts/check-test-evidence.ts --scenario-only --source-root tests/fixtures/test-evidence/scenarios/source --policy tests/fixtures/test-evidence/scenarios/policy.json --catalog tests/fixtures/test-evidence/scenarios/catalog.json --receipt tests/fixtures/test-evidence/scenarios/receipt.json --artifact artifacts/quality/http-scenario-evidence-fixture.json",
|
||||
"check:test-evidence:source": "node scripts/check-test-evidence.ts --source-only --artifact artifacts/quality/test-evidence-source.json",
|
||||
"check:test-evidence:fixture": "node scripts/check-test-evidence.ts --source-root tests/fixtures/test-evidence/forbidden --artifact artifacts/quality/test-evidence-fixture.json",
|
||||
"test:a11y": "playwright test --grep @a11y && node scripts/write-a11y-report.ts",
|
||||
@@ -64,21 +76,25 @@
|
||||
"test:optional-recipe-removal": "node scripts/test-optional-recipe-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:reference-feature": "vitest run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests",
|
||||
"test:coverage": "vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts",
|
||||
"test:reference-feature": "node scripts/run-vitest.ts run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests",
|
||||
"check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts",
|
||||
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && node scripts/run-vitest.ts run tests/runtime-schema tests/unit tests/contract 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",
|
||||
"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",
|
||||
"verify:lockfile": "corepack pnpm install --frozen-lockfile",
|
||||
"test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:contract && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:recipes",
|
||||
"verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts",
|
||||
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
|
||||
"generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts",
|
||||
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
||||
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
|
||||
"verify:supply-chain": "node scripts/verify-supply-chain-artifacts.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",
|
||||
"update:dependency-baseline": "node scripts/update-dependency-baseline.ts",
|
||||
"check:supply-chain:fixtures": "node scripts/check-supply-chain-fixtures.ts",
|
||||
"check:supply-chain:provider-fixtures": "node scripts/check-supply-chain-provider-fixtures.ts",
|
||||
"verify:supply-chain:promotion": "node scripts/verify-supply-chain-promotion.ts",
|
||||
"verify:reproducible-build": "node scripts/verify-reproducible-build.ts",
|
||||
"scan:security": "node scripts/security-scan.ts",
|
||||
"scan:security:fixture": "node scripts/security-scan.ts --policy tests/fixtures/security/secret-detection/forbidden-policy.json --artifact artifacts/security/scan-fixture.sarif",
|
||||
"check:security:fixtures": "node scripts/check-security-fixtures.ts",
|
||||
"check:browser-security": "node scripts/check-browser-security.ts",
|
||||
"check:browser-file-storage-boundaries": "node scripts/check-browser-file-storage-boundaries.ts",
|
||||
"check:realtime-boundaries": "node scripts/check-realtime-boundaries.ts",
|
||||
@@ -101,12 +117,21 @@
|
||||
"drill:runbook": "node scripts/drill-runbook.ts",
|
||||
"drill:runbooks": "corepack pnpm drill:runbook -- FE-RB-001 && corepack pnpm drill:runbook -- FE-RB-002 && corepack pnpm drill:runbook -- FE-RB-003 && corepack pnpm drill:runbook -- FE-RB-004 && corepack pnpm drill:runbook -- FE-RB-005",
|
||||
"ci:gate": "node scripts/run-ci-gate.ts",
|
||||
"check:ci": "node scripts/check-ci-contract.ts",
|
||||
"generate:ci-workflow": "node scripts/generate-ci-workflow.ts",
|
||||
"check:ci-workflow": "node scripts/generate-ci-workflow.ts --check",
|
||||
"check:ci": "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts && corepack pnpm check:ci-workflow",
|
||||
"verify:documentation": "node scripts/verify-documentation-readiness.ts",
|
||||
"build:app-only": "vite build && node scripts/generate-build-manifest.ts",
|
||||
"generate:contract-set": "node scripts/generate-contract-set.ts",
|
||||
"check:types:web-worker": "tsc --project tsconfig.web-worker.json",
|
||||
"check:types:service-worker": "tsc --project tsconfig.service-worker.json"
|
||||
"check:types:service-worker": "tsc --project tsconfig.service-worker.json",
|
||||
"check:types:fixture:http-operation-input": "tsc -p tests/fixtures/typecheck/tsconfig.http-operation-input.json",
|
||||
"check:types:fixture:http-wire-mapper": "tsc -p tests/fixtures/typecheck/tsconfig.http-wire-mapper.json",
|
||||
"check:types:fixture:http-operation-id": "tsc -p tests/fixtures/typecheck/tsconfig.http-operation-id.json",
|
||||
"check:types:fixture:http-route-id": "tsc -p tests/fixtures/typecheck/tsconfig.http-route-id.json",
|
||||
"check:types:fixture:feature-contribution-input": "tsc -p tests/fixtures/typecheck/tsconfig.feature-contribution-input.json",
|
||||
"check:types:fixture:feature-capability-selection": "tsc -p tests/fixtures/typecheck/tsconfig.feature-capability-selection.json",
|
||||
"check:types:fixture:direct-feature-composition": "tsc -p tests/fixtures/typecheck/tsconfig.direct-feature-composition.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "5.101.4",
|
||||
|
||||
@@ -14,5 +14,8 @@
|
||||
"WEB_WORKER": "DEFAULT",
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "number",
|
||||
"const": 1
|
||||
},
|
||||
"buildId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"commitSha": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"releaseId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"moduleInventoryHash": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"generatedAt": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"buildContext": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nodeVersion": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"packageManagerVersion": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"runnerImage": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"sourceDateEpoch": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -59,37 +35,53 @@
|
||||
"runnerImage",
|
||||
"sourceDateEpoch"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"type": "object"
|
||||
},
|
||||
"buildId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"commitSha": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"generatedAt": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"moduleInventoryHash": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"viteManifest": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"moduleInventory": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"routeChunks": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"propertyNames": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"runtimeConfigSchema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"viteManifest": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -99,7 +91,15 @@
|
||||
"routeChunks",
|
||||
"runtimeConfigSchema"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"type": "object"
|
||||
},
|
||||
"releaseId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -112,5 +112,5 @@
|
||||
"buildContext",
|
||||
"outputs"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://clean-architecture-frontend.local/schemas/dependency-inventory.schema.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"packageManager",
|
||||
"lockfileSha256",
|
||||
"dependencyCount",
|
||||
"directDependencyCount",
|
||||
"dependencies"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 2 },
|
||||
"packageManager": { "type": "string", "minLength": 1 },
|
||||
"lockfileSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"dependencyCount": { "type": "integer", "minimum": 1 },
|
||||
"directDependencyCount": { "type": "integer", "minimum": 1 },
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dependencies": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"direct": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"integrity": {
|
||||
"pattern": "^sha512-",
|
||||
"type": "string"
|
||||
},
|
||||
"license": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"optional": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"scope": {
|
||||
"enum": [
|
||||
"production",
|
||||
"development"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
@@ -36,25 +54,41 @@
|
||||
"integrity",
|
||||
"dependencies"
|
||||
],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"version": { "type": "string", "minLength": 1 },
|
||||
"direct": { "type": "boolean" },
|
||||
"scope": {
|
||||
"enum": ["production", "development"]
|
||||
},
|
||||
"optional": { "type": "boolean" },
|
||||
"license": { "type": "string", "minLength": 1 },
|
||||
"integrity": {
|
||||
"type": "string",
|
||||
"pattern": "^sha512-"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
},
|
||||
"dependencyCount": {
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"type": "integer"
|
||||
},
|
||||
"directDependencyCount": {
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"type": "integer"
|
||||
},
|
||||
"lockfileSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"packageManager": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"packageManager",
|
||||
"lockfileSha256",
|
||||
"dependencyCount",
|
||||
"directDependencyCount",
|
||||
"dependencies"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -1,54 +1,442 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"generatedAt",
|
||||
"baselineDigest",
|
||||
"currentDigest",
|
||||
"compatibility",
|
||||
"failures",
|
||||
"registries"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 2 },
|
||||
"generatedAt": { "type": "string", "format": "date-time" },
|
||||
"baselineDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"currentDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"compatibility": {
|
||||
"type": "object",
|
||||
"required": ["impact", "changes"],
|
||||
"properties": {
|
||||
"impact": {
|
||||
"enum": ["none", "additive", "behavior-change", "breaking"]
|
||||
"$defs": {
|
||||
"__schema0": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
"changes": { "type": "array" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/$defs/__schema0"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema0"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"failures": { "type": "array", "maxItems": 0 },
|
||||
"registries": {
|
||||
"type": "array",
|
||||
"minItems": 10,
|
||||
"maxItems": 10,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"registryId",
|
||||
"owner",
|
||||
"source",
|
||||
"rowCount",
|
||||
"contract",
|
||||
"rows"
|
||||
]
|
||||
}
|
||||
"__schema1": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/$defs/__schema1"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema1"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"__schema2": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"baselineDigest": {
|
||||
"anyOf": [
|
||||
{
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"changes": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"after": {
|
||||
"$ref": "#/$defs/__schema1"
|
||||
},
|
||||
"before": {
|
||||
"$ref": "#/$defs/__schema0"
|
||||
},
|
||||
"changeId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"impact": {
|
||||
"enum": [
|
||||
"none",
|
||||
"additive",
|
||||
"behavior-change",
|
||||
"breaking"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"registryId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rowName": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"changeId",
|
||||
"registryId",
|
||||
"rowName",
|
||||
"field",
|
||||
"kind",
|
||||
"impact"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"impact": {
|
||||
"enum": [
|
||||
"not-evaluated",
|
||||
"none",
|
||||
"additive",
|
||||
"behavior-change",
|
||||
"breaking"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"impact",
|
||||
"changes"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"currentDigest": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"failures": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxItems": 0,
|
||||
"type": "array"
|
||||
},
|
||||
"generatedAt": {
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"registries": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"contract": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"owner": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"registryId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rowCount": {
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"rows": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"source": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"registryId",
|
||||
"owner",
|
||||
"source",
|
||||
"rowCount",
|
||||
"contract",
|
||||
"rows"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"maxItems": 11,
|
||||
"minItems": 11,
|
||||
"type": "array"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"generatedAt",
|
||||
"baselineDigest",
|
||||
"currentDigest",
|
||||
"compatibility",
|
||||
"failures",
|
||||
"registries"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"baselineDigest": {
|
||||
"anyOf": [
|
||||
{
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"changes": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"after": {
|
||||
"$ref": "#/$defs/__schema1"
|
||||
},
|
||||
"before": {
|
||||
"$ref": "#/$defs/__schema0"
|
||||
},
|
||||
"changeId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"impact": {
|
||||
"enum": [
|
||||
"none",
|
||||
"additive",
|
||||
"behavior-change",
|
||||
"breaking"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"registryId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rowName": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"changeId",
|
||||
"registryId",
|
||||
"rowName",
|
||||
"field",
|
||||
"kind",
|
||||
"impact"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"impact": {
|
||||
"enum": [
|
||||
"not-evaluated",
|
||||
"none",
|
||||
"additive",
|
||||
"behavior-change",
|
||||
"breaking"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"impact",
|
||||
"changes"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"currentDigest": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"failures": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
},
|
||||
"generatedAt": {
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"registries": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"contract": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"owner": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"registryId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rowCount": {
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"rows": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"source": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"registryId",
|
||||
"owner",
|
||||
"source",
|
||||
"rowCount",
|
||||
"contract",
|
||||
"rows"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"generatedAt",
|
||||
"baselineDigest",
|
||||
"currentDigest",
|
||||
"compatibility",
|
||||
"failures",
|
||||
"registries"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,130 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://clean-architecture-frontend.local/schemas/supply-chain-verification.schema.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dependencyDiff": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"added": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"changed": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"removed": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"upgrades": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"from": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"from",
|
||||
"to"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"added",
|
||||
"removed",
|
||||
"changed",
|
||||
"upgrades"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"distSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"failures": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"highRiskReview": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"localStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"lockfileSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"promotionStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL_UNVERIFIED"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"provenanceAttestationStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL_UNVERIFIED"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"sbomSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"sourceSetSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"vulnerabilityStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL",
|
||||
"FAIL_UNVERIFIED"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"localStatus",
|
||||
@@ -17,39 +139,5 @@
|
||||
"provenanceAttestationStatus",
|
||||
"failures"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 1 },
|
||||
"localStatus": { "enum": ["PASS", "FAIL"] },
|
||||
"promotionStatus": {
|
||||
"enum": ["PASS", "FAIL_UNVERIFIED"]
|
||||
},
|
||||
"lockfileSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"sourceSetSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"distSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"sbomSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"dependencyDiff": { "type": "object" },
|
||||
"highRiskReview": { "type": "array" },
|
||||
"vulnerabilityStatus": {
|
||||
"enum": ["PASS", "FAIL", "FAIL_UNVERIFIED"]
|
||||
},
|
||||
"provenanceAttestationStatus": {
|
||||
"enum": ["PASS", "FAIL_UNVERIFIED"]
|
||||
},
|
||||
"failures": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -13,12 +13,19 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim
|
||||
* 1. clean dist and .generated/frontend-runtime
|
||||
* 2. generate contractSet and build-info source
|
||||
* 3. Vite app build (emptyOutDir = true)
|
||||
* 4. scan app dist and generate the static asset source
|
||||
* 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
|
||||
* 6. generate Release Manifest V2 and the build manifest
|
||||
* 4. materialize dist/config.json from the declared APP_PROFILE
|
||||
* 5. scan app dist and generate the static asset source
|
||||
* 6. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
|
||||
* 7. generate Release Manifest V2 and the build manifest
|
||||
*
|
||||
* Steps 4 and 5 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
|
||||
* Steps 5 and 6 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
|
||||
* and `null`: those modes never run an active worker build.
|
||||
*
|
||||
* Step 4 has to follow the Vite build and precede the asset scan. Vite copies
|
||||
* `public/` verbatim, so without it every build — including a production one —
|
||||
* ships the local runtime document; and the Service Worker hashes the emitted
|
||||
* `config.json`, so the profile must be in place before that inventory is
|
||||
* taken.
|
||||
*/
|
||||
|
||||
const selection = INSTALLED_RUNTIME_CAPABILITIES.serviceWorker;
|
||||
@@ -45,10 +52,13 @@ run("node", ["scripts/generate-contract-set.ts"]);
|
||||
// 3. app build
|
||||
run("npx", ["vite", "build"]);
|
||||
|
||||
// 4. runtime config for the declared profile
|
||||
run("node", ["scripts/generate-runtime-config.ts"]);
|
||||
|
||||
if (buildsActiveWorker) {
|
||||
// 4. hashed asset inventory
|
||||
// 5. hashed asset inventory
|
||||
run("node", ["scripts/generate-service-worker-assets.ts", "dist"]);
|
||||
// 5. service worker build
|
||||
// 6. service worker build
|
||||
run("npx", ["vite", "build", "--config", "vite.service-worker.config.ts"]);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
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.
|
||||
//
|
||||
// 이 목록은 재검토가 이름으로 지목한 네 파일만 덮는다. 같은 두 그룹의
|
||||
// `image-cdn-runtime.ts`와 `resumable-upload-runtime.ts`는 여기 없고 지금도
|
||||
// 자기 abort 사본을 들고 있다. 어댑터 전체로는 23개 파일이 그렇다. 그 전수
|
||||
// 이행은 abort 의미론을 바꾸는 별도 작업이라 이 목록으로 강제하지 않고,
|
||||
// 아래 래칫이 개수가 늘어나는 것만 막는다.
|
||||
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",
|
||||
);
|
||||
const PRIMITIVE_SPECIFIER = "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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 손수 짠 abort 배선은 줄어들기만 해야 한다.
|
||||
//
|
||||
// 커널 `platform/abortable-operation.ts`가 있는데도 어댑터 23개 파일이
|
||||
// `addEventListener("abort")`로 같은 race/cleanup을 각자 짠다. 그 전수 이행은
|
||||
// 동작이 바뀌는 큰 작업이라 한 번에 하지 않는다. 대신 개수를 여기 고정해
|
||||
// 되돌아가지 못하게 한다. 이행으로 숫자가 내려가면 이 상수도 같이 내린다.
|
||||
// `platform/`은 커널 자신이므로 세지 않는다.
|
||||
//
|
||||
// 24 → 23: `browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts`가
|
||||
// IndexedDB 커널로 옮겨가면서 자기 abort 리스너를 지웠다.
|
||||
// 23 → 22: `storage/indexeddb/indexeddb-maintenance.ts`가 같은 이유로 지웠다.
|
||||
// 22 → 21: `storage/indexeddb/indexeddb-runtime.ts`가 같은 이유로 지웠다.
|
||||
// 이것으로 IndexedDB 어댑터 4벌이 모두 커널을 쓴다.
|
||||
const HAND_ROLLED_ABORT_CEILING = 21;
|
||||
const handRolledScan = spawnSync(
|
||||
"git",
|
||||
["grep", "-l", 'addEventListener("abort"', "--", "src/adapters"],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const handRolledFiles = (
|
||||
handRolledScan.status === 0 ? handRolledScan.stdout : ""
|
||||
)
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.filter((file) => !file.startsWith("src/adapters/platform/"))
|
||||
.filter((file) => !readFileSync(file, "utf8").includes(PRIMITIVE_SPECIFIER))
|
||||
.sort();
|
||||
if (handRolledFiles.length > HAND_ROLLED_ABORT_CEILING) {
|
||||
problems.push(
|
||||
`abortable-operation: hand-rolled abort wiring grew to ` +
|
||||
`${handRolledFiles.length} files (ceiling ${HAND_ROLLED_ABORT_CEILING}). ` +
|
||||
`Use platform/abortable-operation.ts instead of a new listener pair. ` +
|
||||
`Current offenders (the new one is whichever this change added): ` +
|
||||
`${handRolledFiles.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
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; ` +
|
||||
`hand-rolled abort wiring: ${handRolledFiles.length}/` +
|
||||
`${HAND_ROLLED_ABORT_CEILING} files (ratchet) PASS`,
|
||||
);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -1,10 +1,13 @@
|
||||
import { parseAsync } from "@babel/core";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, readdir } from "node:fs/promises";
|
||||
import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import { architectureDependencyReportArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type PathRule = Readonly<{ path?: string; pathNot?: string }>;
|
||||
type ArchitectureRule = Readonly<{
|
||||
name: string;
|
||||
@@ -156,10 +159,11 @@ dependencyReport.staticImportGraph = {
|
||||
typeScriptOnlySourcePolicy: typeScriptOnlyPolicy,
|
||||
};
|
||||
|
||||
await writeFile(
|
||||
qualityArtifact,
|
||||
`${JSON.stringify(dependencyReport, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: qualityArtifact,
|
||||
schema: architectureDependencyReportArtifactSchema,
|
||||
value: dependencyReport,
|
||||
});
|
||||
|
||||
let architectureFailed = false;
|
||||
|
||||
@@ -719,9 +723,13 @@ function findArchitectureViolations(
|
||||
continue;
|
||||
}
|
||||
for (const dependency of dependencies) {
|
||||
const sourceGroups = rule.from?.path
|
||||
? (new RegExp(rule.from.path, "u").exec(dependency.source)?.slice(1) ??
|
||||
[])
|
||||
: [];
|
||||
if (
|
||||
matchesPath(dependency.source, rule.from) &&
|
||||
matchesPath(dependency.target, rule.to)
|
||||
matchesPath(dependency.target, rule.to, sourceGroups)
|
||||
) {
|
||||
violations.push({
|
||||
rule: rule.name,
|
||||
@@ -742,16 +750,52 @@ function findArchitectureViolations(
|
||||
function matchesPath(
|
||||
modulePath: string,
|
||||
criterion: PathRule | undefined,
|
||||
sourceGroups: readonly string[] = [],
|
||||
): boolean {
|
||||
if (!criterion) return true;
|
||||
if (criterion.path && !new RegExp(criterion.path, "u").test(modulePath)) {
|
||||
if (
|
||||
criterion.path &&
|
||||
!new RegExp(expandSourceGroups(criterion.path, sourceGroups), "u").test(
|
||||
modulePath,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !(
|
||||
criterion.pathNot && new RegExp(criterion.pathNot, "u").test(modulePath)
|
||||
criterion.pathNot &&
|
||||
new RegExp(expandSourceGroups(criterion.pathNot, sourceGroups), "u").test(
|
||||
modulePath,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes `$1`..`$9` in a `to` pattern with the capture groups the `from`
|
||||
* pattern matched on the importing module.
|
||||
*
|
||||
* Without it, "an adapter may not import a *different* adapter" cannot be
|
||||
* written as one rule: the target pattern has to name the importer's own
|
||||
* directory to exempt it. The alternative is one rule per adapter group, which
|
||||
* silently stops covering a group the moment somebody adds one — exactly the
|
||||
* gap that let `diagnostics` import `telemetry` while the documented rule said
|
||||
* it could not.
|
||||
*/
|
||||
function expandSourceGroups(
|
||||
pattern: string,
|
||||
sourceGroups: readonly string[],
|
||||
): string {
|
||||
return pattern.replaceAll(/\$([1-9])/gu, (whole, index: string) => {
|
||||
const captured = sourceGroups[Number(index) - 1];
|
||||
// A `from` pattern that did not capture leaves the token literal rather
|
||||
// than quietly matching everything.
|
||||
return captured === undefined ? whole : escapeRegExp(captured);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
||||
}
|
||||
|
||||
function validateArchitectureRules(rules: readonly ArchitectureRule[]): void {
|
||||
if (!rules.some((rule) => rule.to?.circular === true)) {
|
||||
throw new Error("Architecture configuration must contain a circular rule");
|
||||
@@ -780,12 +824,13 @@ async function runGraphFixtureChecks(): Promise<GraphFixtureResult> {
|
||||
"tests/fixtures/architecture/dependency-graph",
|
||||
);
|
||||
const allowedRoot = resolve(fixtureRoot, "allowed");
|
||||
const [allowedGraph, unresolvedGraph, layerGraph, cycleGraph] =
|
||||
const [allowedGraph, unresolvedGraph, layerGraph, cycleGraph, barrelGraph] =
|
||||
await Promise.all([
|
||||
analyzeSourceGraph(allowedRoot, "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "unresolved"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "layer"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "cycle"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "barrel"), "src"),
|
||||
]);
|
||||
const allowedFiles = new Set(await listFiles(allowedRoot));
|
||||
const allowedSourceFile = resolve(
|
||||
@@ -806,6 +851,21 @@ async function runGraphFixtureChecks(): Promise<GraphFixtureResult> {
|
||||
),
|
||||
);
|
||||
const assertions = [
|
||||
{
|
||||
name: "deep adapter import from outside the group is rejected",
|
||||
passed: blockingViolations(barrelGraph).some(
|
||||
({ rule, source, target }) =>
|
||||
rule === "adapter-groups-are-reached-through-their-barrel" &&
|
||||
source === "src/bootstrap/compose-deep.ts" &&
|
||||
target === "src/adapters/http/client.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "barrel import from outside the group is accepted",
|
||||
passed: !blockingViolations(barrelGraph).some(
|
||||
({ source }) => source === "src/bootstrap/compose-barrel.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "explicit TS specifier resolves to a TS module",
|
||||
passed: allowedGraph.dependencies.some(
|
||||
|
||||
+14
-10
@@ -1,10 +1,13 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.ts";
|
||||
import {
|
||||
bundleOutputInventoryArtifactSchema,
|
||||
bundlePerformanceArtifactSchema,
|
||||
} from "./contracts/release-artifacts.ts";
|
||||
import { classifyViteJavascript } from "./lib/classify-vite-bundle.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type BundleOutput = { path: string; gzipBytes: number };
|
||||
type BundleReport = { outputs: BundleOutput[]; [key: string]: unknown };
|
||||
type ViteManifest = Record<
|
||||
string,
|
||||
{ file: string; isEntry?: boolean; imports?: string[] }
|
||||
@@ -14,9 +17,9 @@ type BundleBudgets = {
|
||||
lazyChunkGzipBytes: number;
|
||||
};
|
||||
|
||||
const report = JSON.parse(
|
||||
await readFile("artifacts/performance/bundle.json", "utf8"),
|
||||
) as BundleReport;
|
||||
const report = bundleOutputInventoryArtifactSchema.parse(
|
||||
JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8")) as unknown,
|
||||
);
|
||||
const viteManifest = JSON.parse(
|
||||
await readFile("dist/.vite/manifest.json", "utf8"),
|
||||
) as ViteManifest;
|
||||
@@ -87,10 +90,11 @@ const completedReport = {
|
||||
passed,
|
||||
};
|
||||
|
||||
await writeFile(
|
||||
"artifacts/performance/bundle.json",
|
||||
`${JSON.stringify(completedReport, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/performance/bundle.json",
|
||||
schema: bundlePerformanceArtifactSchema,
|
||||
value: completedReport,
|
||||
});
|
||||
if (!passed) {
|
||||
process.stderr.write(
|
||||
`Bundle budget or manifest integrity failed: ${[
|
||||
|
||||
+106
-277
@@ -1,313 +1,142 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import {
|
||||
evaluatePromotionReadiness,
|
||||
PROMOTION_FORMULA,
|
||||
type GateResult,
|
||||
} from "../src/application/policies/promotion-readiness.ts";
|
||||
import {
|
||||
loadCiGateContract,
|
||||
indexCiGateContract,
|
||||
} from "./contracts/ci-gates.ts";
|
||||
import { generateCiWorkflow, renderCiWorkflow } from "./generate-ci-workflow.ts";
|
||||
import {
|
||||
ciContractReportSchema,
|
||||
} from "./lib/ci-contract-report.ts";
|
||||
import {
|
||||
validateInstallScriptPolicy,
|
||||
validatePackageScriptGraph,
|
||||
} from "./lib/package-script-graph.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type GateDefinition = Readonly<{
|
||||
steps?: readonly unknown[];
|
||||
evidence?: readonly string[];
|
||||
retentionClass?: string;
|
||||
}>;
|
||||
type CiContractDocument = Readonly<{
|
||||
providerAdapter: string;
|
||||
stages: Readonly<Record<string, Readonly<{ gates?: readonly string[] }>>>;
|
||||
gates: Readonly<Record<string, GateDefinition>>;
|
||||
retention: Readonly<{ durationStatus: unknown }>;
|
||||
}>;
|
||||
|
||||
const document = parseCiContractDocument(
|
||||
JSON.parse(await readFile("config/ci/gates.json", "utf8")),
|
||||
);
|
||||
const workflow = await readFile(document.providerAdapter, "utf8");
|
||||
const nodeVersion = (await readFile(".nvmrc", "utf8")).trim();
|
||||
const gateRunner = await readFile("scripts/run-ci-gate.ts", "utf8");
|
||||
const drillRunner = await readFile("scripts/drill-runbook.ts", "utf8");
|
||||
const buildManifestGenerator = await readFile(
|
||||
"scripts/generate-build-manifest.ts",
|
||||
"utf8",
|
||||
);
|
||||
const removalFixtureMode = process.argv.includes("--reduced-removal-fixture");
|
||||
const contract = await loadCiGateContract(process.cwd(), {
|
||||
mode: removalFixtureMode ? "removal-fixture" : "canonical",
|
||||
});
|
||||
const index = indexCiGateContract(contract);
|
||||
const [packageDocument, nodeVersion] = await Promise.all([
|
||||
readFile("package.json", "utf8").then((value) => JSON.parse(value) as { scripts?: Record<string, string> }),
|
||||
readFile(".nvmrc", "utf8").then((value) => value.trim()),
|
||||
]);
|
||||
const packageScripts = packageDocument.scripts ?? {};
|
||||
const failures: string[] = [];
|
||||
if (!/^\d+\.\d+\.\d+$/.test(nodeVersion)) {
|
||||
|
||||
if (!/^\d+\.\d+\.\d+$/u.test(nodeVersion)) {
|
||||
failures.push(".nvmrc must contain one exact Node.js semantic version");
|
||||
}
|
||||
const setupNodeCount =
|
||||
workflow.match(/uses:\s*actions\/setup-node@v4/g)?.length ?? 0;
|
||||
const nodeVersionFileCount =
|
||||
workflow.match(/node-version-file:\s*\.nvmrc/g)?.length ?? 0;
|
||||
if (setupNodeCount === 0 || nodeVersionFileCount !== setupNodeCount) {
|
||||
failures.push("every setup-node step must use node-version-file: .nvmrc");
|
||||
for (const script of [
|
||||
"build:release-candidate",
|
||||
"verify:local-evidence",
|
||||
"verify:promotion",
|
||||
"generate:ci-workflow",
|
||||
"check:ci-workflow",
|
||||
"check:ci",
|
||||
]) {
|
||||
if (!packageScripts[script]) failures.push(`package script missing ${script}`);
|
||||
}
|
||||
if (/node-version\s*:/.test(workflow) || /NODE_VERSION\s*:/.test(workflow)) {
|
||||
failures.push("workflow must not override the exact .nvmrc Node.js pin");
|
||||
for (const removedScript of [
|
||||
"build:release",
|
||||
"verify:supply-chain",
|
||||
"verify:supply-chain:promotion",
|
||||
]) {
|
||||
if (packageScripts[removedScript]) failures.push(`legacy package script remains ${removedScript}`);
|
||||
}
|
||||
const stageFormula: Readonly<Record<string, readonly string[]>> = {
|
||||
merge: PROMOTION_FORMULA.MERGE_READY,
|
||||
release: PROMOTION_FORMULA.RELEASE_READY,
|
||||
production: PROMOTION_FORMULA.PROD_PROMOTION_READY,
|
||||
field: PROMOTION_FORMULA.FIELD_SLO_READY,
|
||||
documentation: PROMOTION_FORMULA.DOCUMENTATION_READY,
|
||||
};
|
||||
if (/\b(?:build|rebuild)(?::[\w-]+)?\b/u.test(packageScripts["verify:promotion"] ?? "")) {
|
||||
failures.push("verify:promotion must not build or rebuild candidate bytes");
|
||||
}
|
||||
failures.push(...validatePackageScriptGraph(packageScripts, "check:ci"));
|
||||
failures.push(
|
||||
...validateInstallScriptPolicy(
|
||||
packageScripts,
|
||||
[...new Set(contract.commands.map(({ script }) => script))],
|
||||
),
|
||||
);
|
||||
|
||||
for (const [stage, expectedGates] of Object.entries(stageFormula)) {
|
||||
const actual = document.stages[stage]?.gates;
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expectedGates)) {
|
||||
failures.push(`${stage} gate formula drift`);
|
||||
}
|
||||
const immutable = index.gates.get("FE-GATE-015");
|
||||
const immutableCommands = immutable?.commandIds.map((id) => index.commands.get(id)?.script);
|
||||
if (JSON.stringify(immutableCommands) !== JSON.stringify(["build:release-candidate", "verify:local-evidence"])) {
|
||||
failures.push("FE-GATE-015 must build candidate bytes once and verify local evidence only");
|
||||
}
|
||||
const architecture = index.gates.get("FE-GATE-010");
|
||||
if (!architecture?.commandIds.some((id) => index.commands.get(id)?.script === "check:ci")) {
|
||||
failures.push("a blocking gate must execute check:ci");
|
||||
}
|
||||
if (!architecture?.evidenceArtifactIds.some((id) => index.artifacts.get(id)?.path === "artifacts/quality/ci-contract.json")) {
|
||||
failures.push("FE-GATE-010 must publish the typed CI contract report");
|
||||
}
|
||||
|
||||
const configuredGateIds = Object.keys(document.gates).sort();
|
||||
const expectedGateIds = Array.from(
|
||||
{ length: 26 },
|
||||
{ length: 27 },
|
||||
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
||||
);
|
||||
if (JSON.stringify(configuredGateIds) !== JSON.stringify(expectedGateIds)) {
|
||||
failures.push("gate registry must contain FE-GATE-001..026 exactly once");
|
||||
}
|
||||
|
||||
for (const [gateId, gate] of Object.entries(document.gates)) {
|
||||
if (!gate.steps?.length || !gate.evidence?.length || !gate.retentionClass) {
|
||||
failures.push(`${gateId} lacks command, evidence, or retention wiring`);
|
||||
}
|
||||
for (const [index, step] of (gate.steps ?? []).entries()) {
|
||||
if (!isRecord(step) || (step.expect !== "pass" && step.expect !== "fail")) {
|
||||
failures.push(`${gateId}[${index}] has an invalid step expectation`);
|
||||
continue;
|
||||
}
|
||||
if (step.expect === "pass") {
|
||||
if (
|
||||
step.expectedExitCode !== undefined ||
|
||||
step.expectedDiagnosticId !== undefined
|
||||
) {
|
||||
failures.push(
|
||||
`${gateId}[${index}] passing step declares a negative fixture identity`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
typeof step.expectedExitCode !== "number" ||
|
||||
!Number.isSafeInteger(step.expectedExitCode) ||
|
||||
step.expectedExitCode < 1 ||
|
||||
step.expectedExitCode > 255
|
||||
) {
|
||||
failures.push(`${gateId}[${index}] lacks an exact expected exit code`);
|
||||
}
|
||||
const diagnosticId = step.expectedDiagnosticId;
|
||||
if (
|
||||
typeof diagnosticId !== "string" ||
|
||||
diagnosticId.trim().length === 0 ||
|
||||
diagnosticId.length > 256 ||
|
||||
["\r", "\n", "\0"].some(
|
||||
(character) =>
|
||||
typeof diagnosticId === "string" && diagnosticId.includes(character),
|
||||
)
|
||||
) {
|
||||
failures.push(
|
||||
`${gateId}[${index}] lacks a bounded expected diagnostic identity`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const runbookGateEvidence = Object.freeze({
|
||||
"FE-GATE-016": "artifacts/runbooks/FE-RB-005/record.json",
|
||||
"FE-GATE-021": "artifacts/runbooks/FE-RB-001/record.json",
|
||||
"FE-GATE-022": "artifacts/runbooks/FE-RB-002/record.json",
|
||||
"FE-GATE-023": "artifacts/runbooks/FE-RB-003/record.json",
|
||||
"FE-GATE-024": "artifacts/runbooks/FE-RB-004/record.json",
|
||||
"FE-GATE-025": "artifacts/runbooks/FE-RB-005/record.json",
|
||||
});
|
||||
for (const [gateId, evidencePath] of Object.entries(runbookGateEvidence)) {
|
||||
const evidence = document.gates[gateId]?.evidence;
|
||||
if (
|
||||
!Array.isArray(evidence) ||
|
||||
evidence.length !== 1 ||
|
||||
evidence[0] !== evidencePath
|
||||
) {
|
||||
failures.push(`${gateId} runbook evidence path drift`);
|
||||
}
|
||||
}
|
||||
if (
|
||||
!drillRunner.includes(
|
||||
"const artifactDirectory = `artifacts/runbooks/${runbookId}`",
|
||||
) ||
|
||||
drillRunner.includes(
|
||||
"artifacts/runbooks/${runbookId}/${release.releaseId}",
|
||||
)
|
||||
) {
|
||||
failures.push(
|
||||
"runbook evidence path must be stable while releaseId stays in the record",
|
||||
);
|
||||
}
|
||||
|
||||
const forbiddenWorkflowPatterns = [
|
||||
/continue-on-error\s*:/,
|
||||
/retention-days\s*:/,
|
||||
/allow_failure\s*:/,
|
||||
];
|
||||
for (const pattern of forbiddenWorkflowPatterns) {
|
||||
if (pattern.test(workflow)) {
|
||||
failures.push(`workflow contains forbidden downgrade/unsupported setting ${pattern}`);
|
||||
}
|
||||
}
|
||||
const jobTimeoutCount = workflow.match(/timeout-minutes:\s*45/g)?.length ?? 0;
|
||||
if (jobTimeoutCount !== 5) {
|
||||
failures.push("every CI gate job must declare timeout-minutes: 45");
|
||||
}
|
||||
if (/if-no-files-found:\s*warn/.test(workflow)) {
|
||||
failures.push("CI evidence upload must fail when artifacts are absent");
|
||||
}
|
||||
for (const requiredToken of [
|
||||
"merge_gate:",
|
||||
"release_gate:",
|
||||
"production_gate:",
|
||||
"field_gate:",
|
||||
"documentation_gate:",
|
||||
"needs: merge_gate",
|
||||
"needs: release_gate",
|
||||
"needs: production_gate",
|
||||
"actions/upload-artifact@v4",
|
||||
"if: always()",
|
||||
"permissions:",
|
||||
"contents: read",
|
||||
'CI: "true"',
|
||||
'VITE_BUILD_ID: "gitea-${{ gitea.run_id }}-${{ gitea.run_attempt }}"',
|
||||
'VITE_COMMIT_SHA: "${{ gitea.sha }}"',
|
||||
'RELEASE_ID: "${{ gitea.ref }}-${{ gitea.run_id }}-${{ gitea.run_attempt }}"',
|
||||
'CI_RUNNER_IMAGE: "${{ vars.RUNNER_IMAGE_DIGEST }}"',
|
||||
]) {
|
||||
if (!workflow.includes(requiredToken)) {
|
||||
failures.push(`workflow missing ${requiredToken}`);
|
||||
}
|
||||
}
|
||||
for (const requiredToken of [
|
||||
"ciCheckoutIdentityFailures",
|
||||
"ciBuildEnvironmentFailures",
|
||||
"SOURCE_DATE_EPOCH",
|
||||
'"--format=%H%n%ct"',
|
||||
"env: gateEnvironment",
|
||||
"classifyGateStepResult",
|
||||
"timeout: step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS",
|
||||
]) {
|
||||
if (!gateRunner.includes(requiredToken)) {
|
||||
failures.push(`CI gate runner missing ${requiredToken}`);
|
||||
}
|
||||
}
|
||||
for (const requiredToken of [
|
||||
"assertCiBuildEnvironment(process.env)",
|
||||
"releaseId",
|
||||
"sourceDateEpoch",
|
||||
]) {
|
||||
if (!buildManifestGenerator.includes(requiredToken)) {
|
||||
failures.push(`build manifest generator missing ${requiredToken}`);
|
||||
}
|
||||
}
|
||||
|
||||
const passingResults: Record<string, GateResult> = {};
|
||||
for (const gateId of expectedGateIds) passingResults[gateId] = "PASS";
|
||||
const passingResults: Record<string, GateResult> = Object.fromEntries(
|
||||
expectedGateIds.map((gateId) => [gateId, "PASS"]),
|
||||
);
|
||||
const allPass = evaluatePromotionReadiness(passingResults);
|
||||
const negativeFixtures: Array<{
|
||||
readiness: keyof typeof PROMOTION_FORMULA;
|
||||
failedGate: string;
|
||||
passed: boolean;
|
||||
}> = [];
|
||||
for (const readiness of Object.keys(PROMOTION_FORMULA) as Array<
|
||||
keyof typeof PROMOTION_FORMULA
|
||||
>) {
|
||||
const gateIds = PROMOTION_FORMULA[readiness];
|
||||
const failedGate = gateIds[0];
|
||||
if (!failedGate) throw new Error(`${readiness} has no configured gates`);
|
||||
const result = evaluatePromotionReadiness({
|
||||
...passingResults,
|
||||
[failedGate]: "FAIL",
|
||||
});
|
||||
const passed = result[readiness] === false;
|
||||
for (const readiness of Object.keys(PROMOTION_FORMULA) as Array<keyof typeof PROMOTION_FORMULA>) {
|
||||
const failedGate = PROMOTION_FORMULA[readiness][0];
|
||||
if (!failedGate) throw new TypeError(`${readiness} has no configured gates`);
|
||||
const evaluated = evaluatePromotionReadiness({ ...passingResults, [failedGate]: "FAIL" });
|
||||
const passed = evaluated[readiness] === false;
|
||||
negativeFixtures.push({ readiness, failedGate, passed });
|
||||
if (!passed) failures.push(`${readiness} did not fail closed`);
|
||||
}
|
||||
if (!Object.values(allPass).every(Boolean)) {
|
||||
failures.push("all-PASS formula did not produce every readiness state");
|
||||
}
|
||||
if (!Object.values(allPass).every(Boolean)) failures.push("all-PASS formula did not produce every readiness state");
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
providerAdapter: document.providerAdapter,
|
||||
const renderedWorkflow = renderCiWorkflow(contract);
|
||||
const workflowCheck = await generateCiWorkflow({
|
||||
root: process.cwd(),
|
||||
contract,
|
||||
check: true,
|
||||
});
|
||||
if (!workflowCheck.matches) {
|
||||
failures.push(
|
||||
`generated workflow drift at byte ${workflowCheck.firstDifferenceByte ?? "missing"}, line ${workflowCheck.firstDifferenceLine ?? "missing"}`,
|
||||
);
|
||||
}
|
||||
let checkedWorkflowBytes = Buffer.from(renderedWorkflow, "utf8");
|
||||
try {
|
||||
checkedWorkflowBytes = await readFile(workflowCheck.target);
|
||||
} catch (error) {
|
||||
failures.push(`generated workflow is unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
const report = ciContractReportSchema.parse({
|
||||
schemaVersion: 2,
|
||||
nodeVersion,
|
||||
gateCount: configuredGateIds.length,
|
||||
noDowngrade: failures.every(
|
||||
(failure) => !failure.includes("downgrade"),
|
||||
),
|
||||
durationStatus: document.retention.durationStatus,
|
||||
gateCount: contract.gates.length,
|
||||
commandDefinitionCount: contract.commands.length,
|
||||
commandReferenceCount: contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0),
|
||||
artifactCount: contract.artifacts.length,
|
||||
jobCount: contract.jobs.length,
|
||||
workflowSha256: createHash("sha256").update(checkedWorkflowBytes).digest("hex"),
|
||||
durationStatus: contract.retention.durationStatus,
|
||||
negativeFixtures,
|
||||
failures,
|
||||
passed: failures.length === 0,
|
||||
};
|
||||
await mkdir("artifacts/quality", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/quality/ci-contract.json",
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/quality/ci-contract.json",
|
||||
schema: ciContractReportSchema,
|
||||
value: report,
|
||||
});
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("CI contract: 26 blocking gates and 4-tier graph PASS\n");
|
||||
|
||||
function parseCiContractDocument(value: unknown): CiContractDocument {
|
||||
if (!isRecord(value)) throw new TypeError("CI gate config must be an object");
|
||||
if (
|
||||
typeof value.providerAdapter !== "string" ||
|
||||
!isRecord(value.stages) ||
|
||||
!isRecord(value.gates) ||
|
||||
!isRecord(value.retention)
|
||||
) {
|
||||
throw new TypeError("CI gate config is missing required registries");
|
||||
}
|
||||
const stages: Record<string, { gates?: readonly string[] }> = {};
|
||||
for (const [stage, candidate] of Object.entries(value.stages)) {
|
||||
if (!isRecord(candidate)) throw new TypeError(`Invalid CI stage: ${stage}`);
|
||||
if (
|
||||
candidate.gates !== undefined &&
|
||||
(!Array.isArray(candidate.gates) ||
|
||||
!candidate.gates.every((gate) => typeof gate === "string"))
|
||||
) {
|
||||
throw new TypeError(`Invalid gate list for CI stage: ${stage}`);
|
||||
}
|
||||
stages[stage] = {
|
||||
gates: candidate.gates as readonly string[] | undefined,
|
||||
};
|
||||
}
|
||||
const gates: Record<string, GateDefinition> = {};
|
||||
for (const [gateId, candidate] of Object.entries(value.gates)) {
|
||||
if (!isRecord(candidate)) throw new TypeError(`Invalid CI gate: ${gateId}`);
|
||||
if (
|
||||
candidate.evidence !== undefined &&
|
||||
(!Array.isArray(candidate.evidence) ||
|
||||
!candidate.evidence.every((path) => typeof path === "string"))
|
||||
) {
|
||||
throw new TypeError(`Invalid evidence list for CI gate: ${gateId}`);
|
||||
}
|
||||
gates[gateId] = {
|
||||
steps: Array.isArray(candidate.steps) ? candidate.steps : undefined,
|
||||
evidence: candidate.evidence as readonly string[] | undefined,
|
||||
retentionClass:
|
||||
typeof candidate.retentionClass === "string"
|
||||
? candidate.retentionClass
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
providerAdapter: value.providerAdapter,
|
||||
stages,
|
||||
gates,
|
||||
retention: { durationStatus: value.retention.durationStatus },
|
||||
};
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
process.stdout.write("CI contract: 27 gates, strict v2 graph and generated workflow model PASS\n");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
|
||||
import { classifyObjectSchemaChange } from "../src/application/policies/compatibility.ts";
|
||||
import { classifyObjectSchemaChange } from "../src/contracts/compatibility.ts";
|
||||
import { compatibilityFixturesArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type CompatibilitySchema = Readonly<{
|
||||
required?: readonly string[];
|
||||
@@ -32,25 +34,22 @@ for (const [family, cases] of Object.entries(fixtures.families)) {
|
||||
}
|
||||
|
||||
await mkdir("artifacts/release", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/release/compatibility.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
rules: [
|
||||
"additive changes preserve required fields",
|
||||
"breaking changes require version bump and migration, discard, fallback, or rollback",
|
||||
"config and API major versions must match",
|
||||
"incompatible persisted cache is discarded by default",
|
||||
"rollback uses a coherent compatibility tuple",
|
||||
],
|
||||
results,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/compatibility.json",
|
||||
schema: compatibilityFixturesArtifactSchema,
|
||||
value: {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
rules: [
|
||||
"additive changes preserve required fields",
|
||||
"breaking changes require version bump and migration, discard, fallback, or rollback",
|
||||
"config and API major versions must match",
|
||||
"incompatible persisted cache is discarded by default",
|
||||
"rollback uses a coherent compatibility tuple",
|
||||
],
|
||||
results,
|
||||
},
|
||||
});
|
||||
|
||||
if (results.some((result) => !result.passed)) {
|
||||
process.stderr.write("Compatibility fixture classification failed.\n");
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, readdir } from "node:fs/promises";
|
||||
|
||||
import { designSystemReportArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
import path from "node:path";
|
||||
|
||||
import { REQUIRED_COMPONENT_TOKENS, REQUIRED_PRIMITIVE_TOKENS, REQUIRED_SEMANTIC_TOKENS } from "../src/presentation/design-system/tokens/token-contract.ts";
|
||||
@@ -118,7 +121,7 @@ for (const file of sources) {
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
const report = designSystemReportArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
mode: fixtureMode ? "negative-fixture" : "source",
|
||||
checkedTokenCount:
|
||||
@@ -127,14 +130,15 @@ const report = {
|
||||
REQUIRED_COMPONENT_TOKENS.length,
|
||||
failures,
|
||||
passed: failures.length === 0,
|
||||
};
|
||||
});
|
||||
await mkdir("artifacts/quality", { recursive: true });
|
||||
await writeFile(
|
||||
fixtureMode
|
||||
await writeValidatedJsonArtifact({
|
||||
path: fixtureMode
|
||||
? "artifacts/quality/design-system-fixture.json"
|
||||
: "artifacts/quality/design-system.json",
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
schema: designSystemReportArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(`Design system contract failed:\n${failures.join("\n")}\n`);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, readdir } from "node:fs/promises";
|
||||
|
||||
import { diagnosticsReportArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
import path from "node:path";
|
||||
|
||||
import { DIAGNOSTIC_EVENT_REGISTRY } from "../src/contracts/diagnostics.ts";
|
||||
@@ -89,7 +92,7 @@ for (const file of sources) {
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
const report = diagnosticsReportArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
mode: fixtureMode ? "negative-fixture" : "source",
|
||||
telemetryEventCount: Object.keys(TELEMETRY_REGISTRY).length,
|
||||
@@ -97,14 +100,15 @@ const report = {
|
||||
checkedFiles: sources.length,
|
||||
failures,
|
||||
passed: failures.length === 0,
|
||||
};
|
||||
});
|
||||
await mkdir("artifacts/quality", { recursive: true });
|
||||
await writeFile(
|
||||
fixtureMode
|
||||
await writeValidatedJsonArtifact({
|
||||
path: fixtureMode
|
||||
? "artifacts/quality/diagnostics-fixture.json"
|
||||
: "artifacts/quality/diagnostics.json",
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
schema: diagnosticsReportArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(
|
||||
`Diagnostics contract failed:\n${failures.join("\n")}\n`,
|
||||
|
||||
+11
-7
@@ -1,4 +1,7 @@
|
||||
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, readdir } from "node:fs/promises";
|
||||
|
||||
import { i18nReportArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
import path from "node:path";
|
||||
|
||||
import { EN_MESSAGES, KO_MESSAGES, MESSAGE_CATALOGS } from "../src/presentation/i18n/catalog.ts";
|
||||
@@ -77,7 +80,7 @@ for (const file of sources) {
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
const report = i18nReportArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
mode: fixtureMode ? "negative-fixture" : "source",
|
||||
localeCount: Object.keys(MESSAGE_CATALOGS).length + 1,
|
||||
@@ -85,14 +88,15 @@ const report = {
|
||||
checkedFiles: sources.length,
|
||||
failures,
|
||||
passed: failures.length === 0,
|
||||
};
|
||||
});
|
||||
await mkdir("artifacts/quality", { recursive: true });
|
||||
await writeFile(
|
||||
fixtureMode
|
||||
await writeValidatedJsonArtifact({
|
||||
path: fixtureMode
|
||||
? "artifacts/quality/i18n-fixture.json"
|
||||
: "artifacts/quality/i18n.json",
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
schema: i18nReportArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(`I18n contract failed:\n${failures.join("\n")}\n`);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
|
||||
import { measureOptionalRecipeBundle } from "./lib/optional-recipe-bundle.ts";
|
||||
import { optionalRecipeFixturesArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import {
|
||||
scanOptionalRecipeSources,
|
||||
scanProductionBundle,
|
||||
validateRecipeCatalog,
|
||||
} from "./lib/optional-recipes.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type ReferenceRuntimeRecipe = Readonly<{
|
||||
id: string;
|
||||
@@ -176,17 +178,18 @@ const results = [
|
||||
),
|
||||
},
|
||||
];
|
||||
const report = {
|
||||
const report = optionalRecipeFixturesArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
results,
|
||||
bundleBudgetFixtures,
|
||||
passed: results.every(({ passed }) => passed),
|
||||
};
|
||||
});
|
||||
await mkdir("artifacts/quality", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/quality/optional-recipe-fixtures.json",
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/quality/optional-recipe-fixtures.json",
|
||||
schema: optionalRecipeFixturesArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
if (!report.passed) {
|
||||
process.stderr.write(
|
||||
`Optional recipe negative fixtures failed: ${results
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, stat } from "node:fs/promises";
|
||||
|
||||
import { optionalRecipesArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import {
|
||||
measureOptionalRecipeBundle,
|
||||
type OptionalRecipeBundleMeasurement,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
validateRecipeCatalog,
|
||||
} from "./lib/optional-recipes.ts";
|
||||
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type GateViolation = Readonly<{
|
||||
ruleId: string;
|
||||
@@ -141,7 +143,7 @@ const violations: GateViolation[] = [
|
||||
...referenceRuntimeBundleConfigurationViolations,
|
||||
...referenceRuntimeBundleMeasurementViolations,
|
||||
];
|
||||
const report = {
|
||||
const report = optionalRecipesArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
decisionId: "VD-10",
|
||||
selectedCapabilities: [],
|
||||
@@ -169,9 +171,13 @@ const report = {
|
||||
: "NOT_BUILT",
|
||||
violations,
|
||||
passed: violations.length === 0,
|
||||
};
|
||||
});
|
||||
await mkdir("artifacts/quality", { recursive: true });
|
||||
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: artifactPath,
|
||||
schema: optionalRecipesArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
|
||||
if (violations.length > 0) {
|
||||
process.stderr.write(
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { realtimeBoundariesArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { scanRealtimeBoundaries } from "./lib/realtime-boundaries.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
const sourceRoot = argument("--source-root") ?? "src";
|
||||
const artifact =
|
||||
argument("--artifact") ??
|
||||
"artifacts/quality/realtime-boundaries.json";
|
||||
const violations = await scanRealtimeBoundaries(sourceRoot);
|
||||
const report = Object.freeze({
|
||||
const report = realtimeBoundariesArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
sourceRoot,
|
||||
violations,
|
||||
@@ -16,10 +18,11 @@ const report = Object.freeze({
|
||||
});
|
||||
|
||||
await mkdir(path.dirname(artifact), { recursive: true });
|
||||
await writeFile(
|
||||
artifact,
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: artifact,
|
||||
schema: realtimeBoundariesArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
|
||||
if (violations.length > 0) {
|
||||
for (const violation of violations) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user