Compare commits
1
Commits
main
..
72f361969f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f361969f |
@@ -1,225 +0,0 @@
|
||||
{
|
||||
"forbidden": [
|
||||
{
|
||||
"name": "domain-is-framework-neutral",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/domain"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(application|presentation|adapters|bootstrap)|react|react-dom|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application-does-not-know-concrete-runtime",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/application"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(presentation|adapters|bootstrap)|react|react-dom|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "presentation-does-not-know-adapters",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/presentation/(?!adapters/query)"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(adapters|bootstrap)|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page-templates-own-layout-only",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/presentation/templates"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(application|adapters|bootstrap)|src/presentation/adapters|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "icon-vendor-is-facade-only",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src",
|
||||
"pathNot": "^src/presentation/design-system/icons/vendors/lucide\\.tsx$"
|
||||
},
|
||||
"to": {
|
||||
"path": "^lucide-react$"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adapters-do-not-know-presentation",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/adapters"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/(presentation|bootstrap)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature-domain-is-framework-neutral",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/features/[^/]+/domain"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(application|presentation|adapters|bootstrap)|src/features/[^/]+/(application|adapters|presentation)|react|react-dom|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature-application-does-not-know-runtime",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/features/[^/]+/application"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(presentation|adapters|bootstrap)|src/features/[^/]+/(adapters|presentation)|react|react-dom|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature-presentation-does-not-know-outbound-adapters",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/features/[^/]+/presentation"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(adapters|bootstrap)|src/features/[^/]+/adapters|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature-adapters-do-not-know-presentation",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/features/[^/]+/adapters"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(presentation|bootstrap)|src/features/[^/]+/presentation)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "concrete-adapters-compose-only-in-bootstrap",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/(domain|application|presentation|contracts)"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "external-contract-package-single-import-path",
|
||||
"comment": "§4.1: a generated service package may only be imported from src/features/<feature>/contracts/*-contract-contribution.ts",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src",
|
||||
"pathNot": "^src/features/[^/]+/contracts/[^/]+-contract-contribution\\.ts$"
|
||||
},
|
||||
"to": {
|
||||
"path": "^@org-contracts/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "presentation-does-not-fetch-directly",
|
||||
"comment": "§9.2 / appendix B: a page or hook never opens a socket, worker or HTTP adapter itself",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/(presentation|features/[^/]+/presentation)"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/(http|realtime|service-worker|web-worker|storage)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "generic-worker-has-no-network-or-credentials",
|
||||
"comment": "§16.13 / §21.10: a CPU worker never imports HTTP, realtime or auth",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/adapters/web-worker"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/(http|realtime|auth|web-push)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "service-worker-entry-is-not-page-code",
|
||||
"comment": "§17.2.1: the worker realm never imports React, presentation or bootstrap page code",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/adapters/service-worker"
|
||||
},
|
||||
"to": {
|
||||
"path": "^(src/(presentation|bootstrap)|react|react-dom|@tanstack)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "contracts-do-not-know-application",
|
||||
"comment": "§4. `src/contracts` is the lower of the two packages: application reads contracts, never the other way round. Before this rule the shared Result carrier and the compatibility predicate lived in application and were imported back down by contracts, so neither package owned the shared vocabulary and the coupling was invisible to every gate.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/contracts"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/(application|features)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "generic-presentation-does-not-compose-the-product",
|
||||
"comment": "§4 / §9. Which features are installed is a product decision that belongs to bootstrap. Generic presentation reads the installed registries directly today; the paths below are the exact set that does so, frozen so the coupling cannot spread while the assembly is lifted into bootstrap.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/presentation/",
|
||||
"pathNot": "^src/presentation/(layouts/app-shell\\.tsx|pages/(not-found-page|home-page)\\.tsx|routes/(route-contract|route-codecs|app-router|navigation-policy)\\.(ts|tsx)|i18n/catalog\\.ts|examples/platform-overview-page\\.tsx)$"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/features/installed-"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adapters-do-not-know-other-concrete-adapters",
|
||||
"comment": "docs/architecture/layers.md §4: a concrete adapter never depends on another concrete adapter. Only the adapter kernel is shared — `src/adapters/platform` (clock, abort primitive, capacity guard) and the browser-data result helpers. `query-cache` still reads two collaborator types from `cross-context-invalidation`; that edge is named here rather than left silent, and closes when those types are lifted to a port.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/adapters/([^/]+)/"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/([^/]+)/",
|
||||
"pathNot": "^src/adapters/($1/|platform/|browser-file-storage/result\\.ts$|cross-context-invalidation/index\\.ts$)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no-circular-dependencies",
|
||||
"severity": "error",
|
||||
"from": {},
|
||||
"to": {
|
||||
"circular": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"doNotFollow": {
|
||||
"path": "node_modules"
|
||||
},
|
||||
"exclude": {
|
||||
"path": "^(dist|artifacts|tests/fixtures)"
|
||||
},
|
||||
"enhancedResolveOptions": {
|
||||
"exportsFields": [
|
||||
"exports"
|
||||
],
|
||||
"conditionNames": [
|
||||
"import",
|
||||
"require",
|
||||
"node",
|
||||
"default"
|
||||
]
|
||||
},
|
||||
"tsConfig": {
|
||||
"fileName": "tsconfig.app.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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,461 +0,0 @@
|
||||
# GENERATED FILE — edit config/ci/gates.json and run `corepack pnpm generate:ci-workflow`.
|
||||
name: frontend-quality-gates
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stage:
|
||||
description: Highest promotion tier to evaluate
|
||||
required: true
|
||||
default: merge
|
||||
type: choice
|
||||
options:
|
||||
- merge
|
||||
- release
|
||||
- production
|
||||
- field
|
||||
- documentation
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
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 }}"
|
||||
|
||||
jobs:
|
||||
merge_gate:
|
||||
name: "${{ matrix.gate }} / ${{ matrix.name }}"
|
||||
if: ${{ gitea.event_name != 'workflow_dispatch' || inputs.stage != 'documentation' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { gate: FE-GATE-001, name: manifest-lockfile, browser: false }
|
||||
- { gate: FE-GATE-002, name: lint, browser: false }
|
||||
- { gate: FE-GATE-003, name: typecheck, browser: false }
|
||||
- { gate: FE-GATE-004, name: runtime-schema, browser: false }
|
||||
- { gate: FE-GATE-005, name: unit, browser: false }
|
||||
- { gate: FE-GATE-006, name: component, browser: false }
|
||||
- { gate: FE-GATE-007, name: integration, browser: false }
|
||||
- { gate: FE-GATE-008, name: e2e, browser: true }
|
||||
- { gate: FE-GATE-009, name: accessibility, browser: true }
|
||||
- { 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: removability, browser: false }
|
||||
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: 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 merge gate evidence
|
||||
if: always()
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "${{ matrix.gate }}-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
release_gate:
|
||||
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 }}"
|
||||
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-019, name: hosting-header, browser: false }
|
||||
- { gate: FE-GATE-026, name: lab-performance, browser: true }
|
||||
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: 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 release gate evidence
|
||||
if: always()
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "${{ matrix.gate }}-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
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 \
|
||||
scripts/lib/vite-route-chunks.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
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { gate: FE-GATE-016, name: rollback-drill }
|
||||
- { gate: FE-GATE-021, name: runbook-boot-config }
|
||||
- { gate: FE-GATE-022, name: runbook-chunk-mismatch }
|
||||
- { gate: FE-GATE-023, name: runbook-api-degradation }
|
||||
- { gate: FE-GATE-024, name: runbook-telemetry }
|
||||
- { gate: FE-GATE-025, name: runbook-release-rollback }
|
||||
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: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||
- name: Upload production gate evidence
|
||||
if: always()
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "${{ matrix.gate }}-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
field_gate:
|
||||
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 }}"
|
||||
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: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-018
|
||||
- name: Upload field gate evidence
|
||||
if: always()
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "FE-GATE-018-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
|
||||
documentation_gate:
|
||||
name: "FE-GATE-017 / diagram-review"
|
||||
if: ${{ gitea.event_name == 'workflow_dispatch' && inputs.stage == 'documentation' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
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: Run documentation gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-017
|
||||
- name: Upload documentation gate evidence
|
||||
if: always()
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
|
||||
with:
|
||||
name: "FE-GATE-017-${{ gitea.run_id }}"
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
.generated/
|
||||
.tmp/
|
||||
playwright-report/
|
||||
test-results/
|
||||
coverage/
|
||||
.worktrees/
|
||||
!tests/fixtures/coverage/
|
||||
!tests/fixtures/coverage/below-threshold.json
|
||||
artifacts/**/*.json
|
||||
artifacts/**/*.xml
|
||||
artifacts/**/*.txt
|
||||
artifacts/**/*.sarif
|
||||
artifacts/tests/e2e/
|
||||
artifacts/tests/browser-capabilities/
|
||||
artifacts/storybook/
|
||||
artifacts/tests/storybook/
|
||||
artifacts/tests/visual/
|
||||
!artifacts/**/.gitkeep
|
||||
|
||||
# Local environment overrides. `.env.example` is the tracked template; every
|
||||
# other `.env*` file is a developer's own machine and never enters the repo.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Git worktrees created inside the repository. A worktree is a checkout, not
|
||||
# source: committing one would nest a second working copy inside this one.
|
||||
.worktrees/
|
||||
.playwright-mcp/
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
||||
addons: ["@storybook/addon-a11y"],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,123 +0,0 @@
|
||||
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 { 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({
|
||||
session: createAnonymousSessionAdapter(),
|
||||
preferences: {
|
||||
read: (name) => ({ ok: true, value: preferences.get(name) }),
|
||||
write: (name, value) => {
|
||||
preferences.set(name, structuredClone(value));
|
||||
return { ok: true };
|
||||
},
|
||||
remove: (name) => {
|
||||
preferences.delete(name);
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
diagnostics: { record() {} },
|
||||
telemetry: { emit() {} },
|
||||
releaseInfo: {
|
||||
getCurrent: async () => ({
|
||||
buildId: "storybook-build",
|
||||
releaseId: "storybook-release",
|
||||
configSchemaVersion: "1",
|
||||
apiContractVersion: "1",
|
||||
assetManifestHash: "storybook-assets",
|
||||
routeChunks: {},
|
||||
}),
|
||||
refresh: async () => ({
|
||||
buildId: "storybook-build",
|
||||
releaseId: "storybook-release",
|
||||
configSchemaVersion: "1",
|
||||
apiContractVersion: "1",
|
||||
assetManifestHash: "storybook-assets",
|
||||
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(
|
||||
(
|
||||
[
|
||||
"REALTIME",
|
||||
"WEB_WORKER",
|
||||
"SERVICE_WORKER",
|
||||
"OFFLINE_COMMANDS",
|
||||
] as const
|
||||
).map((capabilityId) =>
|
||||
Object.freeze({
|
||||
capabilityId,
|
||||
selected: 0,
|
||||
active: 0,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
navigation: { reload() {} },
|
||||
});
|
||||
const queryClient = createQueryClient();
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ApplicationProvider application={application}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<LocaleProvider>
|
||||
<ThemeProvider>
|
||||
<SessionProvider>
|
||||
<div id="portal-root" />
|
||||
<main className="ui-page" style={{ padding: "1rem" }}>
|
||||
<Story />
|
||||
</main>
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
</LocaleProvider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</ApplicationProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
a11y: {
|
||||
test: "error",
|
||||
},
|
||||
controls: {
|
||||
expanded: true,
|
||||
},
|
||||
options: {
|
||||
storySort: {
|
||||
order: ["Platform"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -1,110 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Task 10 report
|
||||
|
||||
## Mapping
|
||||
|
||||
- Source Studio provider/runtime shell and header → application-input-created, provider-scoped gateway; React Router navigation; native Public link; persisted `pageshow` generation reset.
|
||||
- Source dashboard → exact workspace heading, totals, workflow sections, row labels, links, loading and error copy.
|
||||
- Source document list → exact search/filter/list/empty surfaces plus cursor pagination, retry, and abort of obsolete requests.
|
||||
- Source new-document form → exact type cards/copy, session gateway creation, announcement, and editor redirect.
|
||||
- Source Studio not-found → in-shell 404 surface; Studio routes remain public with no auth UI.
|
||||
|
||||
## TDD evidence
|
||||
|
||||
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx` failed both suites at missing Studio presentation imports (exit 1).
|
||||
- GREEN: the same command passed 2 files / 8 tests.
|
||||
- Focused regression: both Studio suites plus `tests/features/tech-log/runtime-composition.test.ts` passed 3 files / 10 tests.
|
||||
|
||||
## Files
|
||||
|
||||
- Added the 12 Task 10 Studio provider/runtime/shell/component/page files under `src/features/tech-log/presentation/studio/`.
|
||||
- Added `studio-shell-smoke.test.tsx` and `studio-screens-smoke.test.tsx`.
|
||||
|
||||
## SHA
|
||||
|
||||
- Base: `2b6fa42620136c3edb1506f907ce79c2251d1316`.
|
||||
- Implementation: the commit containing this report, titled `feat: port TechLog Studio shell and indexes` (final SHA recorded in the Task 10 handoff).
|
||||
|
||||
## Deferred
|
||||
|
||||
- Task 11 editor screens and Task 12 dirty-leave/save/validation dialogs remain intentionally deferred.
|
||||
- Broad architecture, type, lint, build, security, and visual gates remain deferred to Task 14 by user direction.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Task 11 report
|
||||
|
||||
## Mapping
|
||||
|
||||
- Source common, Case, Reference, and Question fields → exact target labels, controls, order, loaded values, conditional resolution fields, and CSS classes.
|
||||
- Source ordered text/rule/option/relation editors → presentation-owned add, remove, reorder, local IDs, limits, and accessibility names.
|
||||
- Source document editor/status rail → source tabs, keyboard focus, working-copy status, dirty indicator, version/kind rail, and deferred workflow controls.
|
||||
- Source instant preview → Content Format v1 `projectWorkingCopy` plus the shared `PublicRecordRenderer`; no gateway preview mutation or parser/renderer duplication.
|
||||
- Source edit page → registered route input adaptation for the document ID.
|
||||
- Task 10 provider seam → smallest generic provider-owned editor session (`saved`, `draft`, `status`) retained across editor tabs.
|
||||
|
||||
## TDD evidence
|
||||
|
||||
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-editor-smoke.test.tsx` failed at the exact missing `document-editor-screen.tsx` import before test collection.
|
||||
- First GREEN: the same focused command passed 1 file / 5 tests.
|
||||
- Focused regression: editor smoke plus `content-format.test.ts` and `public-render.test.tsx` passed 3 files / 41 tests.
|
||||
- Scope check: `git diff --check` passed.
|
||||
|
||||
## Files
|
||||
|
||||
- Added the 12 Task 11 component/page files under `src/features/tech-log/presentation/studio/`.
|
||||
- Extended `studio-provider.tsx` and `use-studio.ts` only with presentation-owned editor session state.
|
||||
- Added `tests/features/tech-log/studio-editor-smoke.test.tsx`.
|
||||
|
||||
## SHA
|
||||
|
||||
- Base: `887f5e6eb1a5ccdf4feab0b27fae1c5823233190`.
|
||||
- Implementation: the commit containing this report, titled `feat: port TechLog Studio editors` (final SHA recorded in the Task 11 handoff).
|
||||
|
||||
## Deferred
|
||||
|
||||
- Save/conflict resolution, guarded navigation, validation, server preview, publish, and unpublish workflows remain deferred to Tasks 12/13. The source Save control is present but disabled until Task 12 supplies the workflow.
|
||||
- Broad app/test types, lint, build, architecture, security, visual, and full-suite gates remain deferred to integrated Task 14 by user direction.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Task 12 report
|
||||
|
||||
## Mapping
|
||||
|
||||
- Source editor save workflow → pending/success announcements, fresh per-command idempotency keys, retry after request failure, and revision-conflict state without replacing the local draft.
|
||||
- Source guarded Studio links/provider/dialog → all internal Studio anchors use the guarded `<a>` DOM; dirty navigation offers stay, discard, and save-then-navigate with modal focus/trigger restoration and native `beforeunload` protection.
|
||||
- Source validation report/screen → saved-version validation gates, current/stale freshness copy, error-before-warning issue order, exact JSON-pointer editor anchors, retry/not-found surfaces, and abortable route reads.
|
||||
- Source Public Preview screen → missing/current/stale/expired states, exact next-action labels, idempotent preview creation, retry/not-found surfaces, and the shared typed `PublicRecordRenderer`.
|
||||
- Route pages → existing route-input codecs supply the validation/preview document ID; no publish/history/snapshot behavior was pulled forward.
|
||||
- Task 10/11 seam → provider gained only dirty-navigation/time state, editor gained the save callback, and existing Studio links were switched to the newly available source guarded-link component.
|
||||
|
||||
## TDD evidence
|
||||
|
||||
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx` exited 1 before collection at the intentionally missing `guarded-studio-link.tsx` and `public-preview-screen.tsx` imports (2 failed files, 0 tests).
|
||||
- GREEN: the two workflow suites plus `tests/features/tech-log/mock-studio-gateway.test.ts` passed 3 files / 22 tests.
|
||||
- Focused seam regression: those three files plus the existing Studio shell, screen, and editor suites passed 6 files / 35 tests.
|
||||
- Scope check: `git diff --check` passed.
|
||||
|
||||
## Files
|
||||
|
||||
- Added guarded link, unsaved dialog, beforeunload hook, validation report/screen, Public Preview screen, and validation/preview route pages under `src/features/tech-log/presentation/studio/`.
|
||||
- Extended the Task 10 provider/context and Task 11 editor/status rail; updated existing Studio internal link consumers to use the source guard.
|
||||
- Added `studio-save-navigation.test.tsx` and `studio-validation-preview.test.tsx`.
|
||||
|
||||
## SHA
|
||||
|
||||
- Base: `59332659752ee17c095471d06e7f0fc8b00c89b4`.
|
||||
- Implementation: the commit containing this report, titled `feat: port TechLog Studio validation workflow` (final SHA recorded in the Task 12 handoff).
|
||||
|
||||
## Deferred
|
||||
|
||||
- Publish, republish, unpublish, publication history, warning acknowledgement, and immutable publication snapshots remain deferred to Task 13.
|
||||
- Broad browser-security, app/test types, lint, build, architecture, visual, and full-suite gates remain deferred to integrated Task 14 by user direction.
|
||||
@@ -1,47 +0,0 @@
|
||||
# Task 13 report: publication flow and atomic install
|
||||
|
||||
## Status
|
||||
|
||||
`DONE_WITH_CONCERNS`
|
||||
|
||||
Base SHA: `9c6906fc6f76115346360d050dd8c211fee1b5a9`
|
||||
|
||||
Delivery commit: the commit containing this report, with subject `feat: complete TechLog Studio publication flow`.
|
||||
|
||||
## Publication mapping
|
||||
|
||||
- Added the source-faithful publish screen, warning acknowledgements, publication history/filter, unpublish dialog, immutable event snapshot preview, and their three route pages.
|
||||
- Publish blocks invalid or stale validation, requires every warning acknowledgement, creates a fresh idempotency key per command/retry, preserves gateway command ordering, and exposes pending/error/retry states.
|
||||
- Unpublish preserves the reason/confirmation contract. Historical preview reads the event-owned immutable snapshot and renders it through the shared `PublicRecordRenderer`; a missing/unknown event stays inside Studio.
|
||||
- Added `tests/features/tech-log/studio-publication-flow.test.tsx` first. The initial red was the exact missing `publication-list.tsx` module/screen; the implemented suite is green at 7 tests.
|
||||
|
||||
## Atomic install and removals
|
||||
|
||||
- Installed exactly the 27 governed TechLog route definitions, codecs, runtime imports, module identities, message catalogs, schemas, and release-manifest chunk IDs. `PublicShell` and `StudioShell` are the grouped layout elements.
|
||||
- Added governed Vite chunk naming for the 27 route module identities and changed the performance probe to `TECH_LOG_HOME`.
|
||||
- Kept the reference contract/adapters and API/schema/invalidation/platform fixture tests, while removing its presentation runtime/pages and page-level tests.
|
||||
- Removed the four sample presentation pages, starter home/not-found pages, their page-level component/E2E screens, and all four `/examples/*` E2E specs authorized by the brief.
|
||||
- Added `tests/e2e/tech-log-studio-workflow.spec.ts`; it was deliberately not run and is deferred to Task 14.
|
||||
- Updated the removal fixture to retain TechLog after reference removal and to exclude tests whose only contract is the removed reference runtime or the canonical (non-reduced) CI authority.
|
||||
- Split `DocumentEditorController` into a type-only module to remove the editor/status-rail cycle exposed by the installed route graph.
|
||||
|
||||
## Verification evidence
|
||||
|
||||
- Publication + validation-preview + mock gateway: PASS, 3 files / 23 tests.
|
||||
- Router + runtime application + retained reference contract: PASS, 3 files / 16 tests.
|
||||
- Final route contract + navigation policy: PASS, 2 files / 10 tests.
|
||||
- Registry structure: PASS, 11 registries.
|
||||
- Release manifest inventory: PASS, exactly 27 derived chunk IDs.
|
||||
- `git diff --check`: PASS.
|
||||
- `test:sample-removal` was run once. Its isolated home smoke passed 9/9 and registry/CI reduced-contract checks passed, but its internally broad type/architecture/unit/coverage/build loop failed. Task-owned findings were fixed afterward: ES-target-incompatible `toSorted`, stale `APP_HOME`, direct adapter import, editor/status-rail cycle, reference-dependent fixture residue, canonical-CI-only tests in a reduced fixture, and missing governed build chunk names. Per fast-mode direction, that several-minute broad loop was not rerun; Task 14 must confirm the fixes through its integrated gates.
|
||||
|
||||
## Files
|
||||
|
||||
- Publication/UI/runtime: `src/features/tech-log/presentation/**`, including the five publication components, three pages, route runtime, and controller boundary.
|
||||
- Contracts/install: `src/contracts/{routes,route-runtime-contract}.ts`, `src/features/installed-feature-*.{ts,tsx}`, TechLog route contract, platform codecs/runtime, router, layout reference, registry governance, Vite config, release manifest, performance/removal scripts.
|
||||
- Tests: new publication flow and Studio workflow specs; updated route/router/runtime/reference/navigation expectations; authorized sample/reference presentation test deletions.
|
||||
|
||||
## Task 14 deferred concerns
|
||||
|
||||
- Run the complete integrated review/gates, including the sample-removal loop with the post-fix code, production build/manifest verification, types, lint, architecture, security, and Playwright workflow.
|
||||
- Confirm the governed Vite chunk names in the generated production manifest and assess any unrelated environment/timing failures from the broad isolated fixture.
|
||||
@@ -1,310 +0,0 @@
|
||||
# Task 14 report: integrated TechLog parity and release verification
|
||||
|
||||
## Status
|
||||
|
||||
`DONE_WITH_CONCERNS`. The production serving correction, 130-case
|
||||
source-to-target comparison, full recursive product-tree evidence, direct HTTP
|
||||
contract, target visuals, focused browser suites, and static gates pass. The
|
||||
automated Chromium accessibility suite passes 29/29, but the 27 signed human
|
||||
keyboard/focus/screen-reader records remain `PENDING`; `FE-GATE-009` is not
|
||||
claimed as passing. The other concern is the repository's pre-existing
|
||||
restricted-runner `test:all` baseline: 19 provider-environment cases remain
|
||||
red. The isolation and exact counts below prove that no TechLog test is among
|
||||
those failures.
|
||||
|
||||
The work remains on `feature/techlog-ui-migration`. It was not merged, pushed,
|
||||
finished with GitFlow, or deleted. The immutable code candidate is
|
||||
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`
|
||||
(`fix: complete TechLog migration evidence`). This report and the durable
|
||||
parity JSON are deliberately recorded afterward in
|
||||
`docs: record TechLog migration evidence`, so the evidence can name the exact
|
||||
candidate it verifies.
|
||||
|
||||
## Evidence files
|
||||
|
||||
Added or replaced product evidence:
|
||||
|
||||
- `tests/visual/tech-log.visual.spec.ts` and 129 target-only PNG snapshots. The
|
||||
suite has 130 cases because canonical and state coverage for the 1440-pixel
|
||||
`/studio/publications` screen deliberately share the same reviewed image.
|
||||
- `scripts/lib/tech-log-production-server.ts`, the generated self-contained
|
||||
`dist/server.mjs` build artifact, its serving contract/generator, and 39-case
|
||||
direct HTTP regression coverage.
|
||||
- `tests/e2e/tech-log-public-discovery.spec.ts`,
|
||||
`tests/e2e/tech-log-accessibility.spec.ts`, and
|
||||
`tests/e2e/tech-log-responsive.spec.ts`.
|
||||
- `tests/support/browser/tech-log-fixtures.ts`, the checked Node 24 parity
|
||||
runner, and durable
|
||||
`docs/operations/evidence/tech-log-source-parity.json` evidence.
|
||||
- Focused regressions in `tests/unit/vite-route-chunks.test.ts`,
|
||||
`tests/unit/design-system-source.test.ts`, the bounded-body reader tests, the
|
||||
router component suite, and TechLog feature suites.
|
||||
- Governed registry, dependency, release, CI, and operations evidence in
|
||||
`config/contracts`, `config/security`, `config/ci`, the generated Gitea
|
||||
workflow, `README.md`, and
|
||||
`docs/operations/techlog-ui-migration-baseline.md`.
|
||||
|
||||
Removed starter-only evidence:
|
||||
|
||||
- `tests/e2e/compact-smoke.spec.ts`,
|
||||
`tests/e2e/design-system-interactions.spec.ts`, `tests/e2e/i18n.spec.ts`, and
|
||||
`tests/e2e/theme.spec.ts`.
|
||||
- `tests/visual/platform.visual.spec.ts` and all five platform visual PNGs.
|
||||
|
||||
The retained `app-shell`, registry-wide accessibility, and responsive suites
|
||||
were rewritten around TechLog. No stale starter browser or visual snapshot is
|
||||
left referenced.
|
||||
|
||||
## Source-to-target visual method and result
|
||||
|
||||
The supplied source at `/home/donghyeon/workspace/techlog-studio-frontend` was
|
||||
never written. It was copied to `/tmp/techlog-source-parity.I0CBK7`; build and
|
||||
Vinext runtime caches were created only in that temporary copy. The source
|
||||
production server on `4375` and target `dist/server.mjs` production artifact on
|
||||
`4174` were opened by one Playwright Chromium instance with two fresh contexts
|
||||
and the following identical controls:
|
||||
|
||||
- device scale factor 1, light color scheme, `ko-KR`, `Asia/Seoul`, reduced
|
||||
motion, service workers blocked, 1000-pixel viewport height, and full-page
|
||||
screenshots;
|
||||
- fixed clock `2026-08-14T01:00:00.000Z`, deterministic in-memory data,
|
||||
`document.fonts.ready`, matching Pretendard/IBM Plex Mono font-face state,
|
||||
and zero-duration animation, transition, and caret styles;
|
||||
- no masks and no tolerance: exact RGBA pixel comparison, normalized recursive
|
||||
product-subtree tags, ordered child nodes, complete classes, attributes,
|
||||
text and ARIA relationships, layout diagnostics, response metadata, boot
|
||||
lifecycle, and console/page/request failure collection.
|
||||
|
||||
The only attributes normalized by name are diagnostics-confirmed framework
|
||||
outputs: React Router `data-discover`; Next Image `data-nimg`, `decoding`, and
|
||||
`srcset`; and Next SSR `selected` for a controlled select. Generated React IDs
|
||||
and CSS-module hashes are normalized by value; there is no broad attribute
|
||||
omission.
|
||||
|
||||
The final external comparison command was:
|
||||
|
||||
```bash
|
||||
TZ=Asia/Seoul corepack pnpm verify:tech-log-source-parity
|
||||
```
|
||||
|
||||
Result: **130/130 passed**, 0 failed, `totalDifferentPixels=0`, every recursive
|
||||
DOM/class/attribute/text/ARIA tree equal, all HTTP metadata equal, and 0
|
||||
unexplained source/target errors. Source
|
||||
screenshots were temporary comparison inputs; none was copied into target
|
||||
snapshots. The no-update target visual run also passed 130/130 with
|
||||
`maxDiffPixels=0` and `maxDiffPixelRatio=0`.
|
||||
|
||||
The durable evidence identifies source-tree digest
|
||||
`6724c2f898eefc62d2fc0ee695bccc3ae61a69c5153ed43c69f2cf99ee45bca5`,
|
||||
candidate `3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, build-manifest digest
|
||||
`3579650faa482f566553c00d8b4a05a05b4f7a1ab93b83e48676289bbcf02984`,
|
||||
Vite-manifest digest
|
||||
`cfd583ed7b6c27dce7f47389447608636b6b9df3e28df00c6416674da2c7c46d`,
|
||||
case-inventory digest
|
||||
`5689bcdb5d5637205cdeb92b7c57f72d99f0b039818b8f90afdde526bbafe0ac`,
|
||||
and evidence-payload digest
|
||||
`f046047beded19ce468be607dcf99c6b74d4e07323457ec7145c56d2f67d2c79`.
|
||||
|
||||
The comparison found and corrected actual integration defects rather than
|
||||
accepting drift: the TechLog Tailwind bootstrap is loaded exactly once in the
|
||||
same cascade order as source; the starter theme import is removed; CSS-module
|
||||
class mapping, shell navigation/focus, publication labels and states, router
|
||||
404 handling, and generated Vite route-chunk lookup now follow source. All five
|
||||
source/target CSS pairs pass `cmp -s`; their hashes are recorded in the
|
||||
operations baseline.
|
||||
|
||||
Source production returns missing dynamic slugs and an unmatched Public path as
|
||||
HTTP 404, `text/plain;charset=UTF-8`, with the exact nine-byte body `Not Found`.
|
||||
The target production boundary now returns that exact shell-free response;
|
||||
known Public paths and all known Studio paths remain SPA-served, while an
|
||||
unknown Studio path preserves the source's HTML Studio shell with HTTP 404.
|
||||
This is an observed source-production contract and satisfies the planned
|
||||
prohibition on a generic runtime error; it is not a redesign.
|
||||
|
||||
## Route, viewport, and state inventory
|
||||
|
||||
The 27 canonical contract routes were each compared at 360 and 1440 pixels:
|
||||
|
||||
- Public: `/`, `/explore`, `/explore/:kind`, `/cases/:slug`,
|
||||
`/references/:slug`, `/questions/:slug`, `/topics/:slug`, `/projects`,
|
||||
`/projects/:slug`, the `records`, `decisions`, and `activity` project views,
|
||||
`/releases`, `/releases/:version`, `/profile`, `/search`, and `*`.
|
||||
- Studio: `/studio`, `/studio/documents`, `/studio/documents/new`, the `edit`,
|
||||
`validation`, `preview`, and `publish` document views,
|
||||
`/studio/publications`, publication-event preview, and `/studio/*`.
|
||||
|
||||
All known Public fixtures were exercised: two cases, two references, two open
|
||||
questions, three topics, both projects and all three nested views, and release
|
||||
`0.1.0`. Ten unknown Public dynamic shapes were also compared at both widths.
|
||||
|
||||
The 130-case matrix is 54 canonical-route captures, 18 additional known Public
|
||||
fixture captures, 12 home breakpoints (1180, 1179, 1050, 1024, 980, 900, 820,
|
||||
768, 767, 420, 390, and 375), 19 Studio states, 20 unknown-Public captures, and
|
||||
7 interactions. Studio states cover the
|
||||
dashboard, list/new, Case/Reference/Question/conflict editors, valid/invalid
|
||||
validation, current/missing/expired previews, ready/blocked publish,
|
||||
publications/snapshot, missing document/publication, and unknown Studio route.
|
||||
Interactions cover Public search, Studio mobile menu, immediate preview,
|
||||
dirty-leave dialog, newly created current preview, unpublish confirmation, and
|
||||
warning acknowledgement through publish-ready state.
|
||||
|
||||
## Browser, responsive, and accessibility outcomes
|
||||
|
||||
- Required four-spec Chromium command: **48/48 passed**. It covers Public
|
||||
discovery, the full Studio workflow, responsive behavior, and accessibility.
|
||||
- Responsive plus accessibility focused command: **26/26 passed**.
|
||||
- Direct production HTTP contract: **39/39 passed**, including exact raw Public
|
||||
404s, the in-shell Studio 404, known SPA routes, and boot documents.
|
||||
- Exact target visual command: **130/130 passed** in 2.5 minutes with no masks
|
||||
and zero pixel tolerance.
|
||||
- Automated Chromium `@a11y`: **29/29 passed**. The 27 human review records are
|
||||
intentionally pending, so `corepack pnpm review:a11y-manual` exits 1 and
|
||||
lists missing status, candidate release ID, reviewer/signature/attestation,
|
||||
reviewed time, M1-M7, and screen-reader evidence for every route.
|
||||
- Keyboard/focus checks cover Public search dismissal/restoration, Studio mobile
|
||||
navigation, dirty-leave and unpublish dialogs, labels, heading/landmark order,
|
||||
and focus-visible behavior. Axe reports no violations in the required route
|
||||
and state inventory. Overflow assertions pass at the compact and transition
|
||||
widths, and no unexpected console, page, or request error remains.
|
||||
|
||||
Commands:
|
||||
|
||||
```bash
|
||||
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||
corepack pnpm exec playwright test tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||
corepack pnpm exec playwright test tests/e2e/tech-log-http-contract.spec.ts --project=chromium
|
||||
corepack pnpm exec playwright test tests/e2e/accessibility.spec.ts --project=chromium --grep @a11y
|
||||
corepack pnpm exec playwright test tests/visual/tech-log.visual.spec.ts --config=playwright.visual.config.ts --project=chromium
|
||||
corepack pnpm test:visual
|
||||
```
|
||||
|
||||
## Registry and supply-chain governance
|
||||
|
||||
The initial no-baseline registry artifact reported 23 migration-owned breaking
|
||||
IDs. Each now has owner `tech-log-frontend`, a TechLog contract-version reason,
|
||||
atomic route/runtime/manifest installation, same-release compatibility, and
|
||||
rollback to `05e3d50ba01f01c27f257d2e9040c2bc413ea053`:
|
||||
|
||||
- Contract: `$contract:{allowedValues,breakingFields,fieldTypes,requiredFields}`.
|
||||
- Removed route rows: `APP_HOME`, `EXAMPLES_AUTH`, `EXAMPLES_PLATFORM`,
|
||||
`EXAMPLES_STATES`, `EXAMPLES_UI`, `REFERENCE_RESOURCE_DETAIL`,
|
||||
`REFERENCE_RESOURCE_FORM`, `REFERENCE_RESOURCE_LIST`, and
|
||||
`REFERENCE_RESOURCE_STATUS`.
|
||||
- Runtime removals: the same nine route IDs.
|
||||
- Runtime change: `NOT_FOUND:moduleId:field-changed`.
|
||||
|
||||
The governed update used exactly:
|
||||
|
||||
```bash
|
||||
REGISTRY_BASELINE_OWNER=tech-log-frontend REGISTRY_BASELINE_REASON="Install approved TechLog Public and Studio route contract" node scripts/update-registry-baseline.ts artifacts/quality/registries.json
|
||||
corepack pnpm check:registries
|
||||
```
|
||||
|
||||
Final result: 11 registries pass, compatibility `none`, no unacknowledged
|
||||
change. Approved snapshot digest:
|
||||
`428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b`.
|
||||
|
||||
Six direct dependency additions have evidence owner `tech-log-frontend`,
|
||||
reviewer `frontend-platform-security`, product-specific reason, and atomic
|
||||
rollback: `@fontsource/ibm-plex-mono@5.3.0`, `pretendard@1.3.9`,
|
||||
`remark-directive@4.0.0`, `remark-gfm@4.0.1`, `remark-parse@11.0.0`, and
|
||||
`unified@11.0.5`. The dependency policy recognizes the font packages' OFL-1.1
|
||||
license. Supply-chain generation covered 641 packages. The pre-existing
|
||||
dependency baseline was not promoted; the denied promotion was unnecessary for
|
||||
the regular verification path, which passes with the committed evidence.
|
||||
|
||||
## Sample removal and fresh verification
|
||||
|
||||
The final staged-candidate command passed:
|
||||
|
||||
```bash
|
||||
corepack pnpm test:sample-removal
|
||||
```
|
||||
|
||||
Result: **PASS (13 checks, no fixture IDs)**. Its internal evidence included
|
||||
types; reduced architecture (382 modules/1,170 dependencies, 12 graph checks,
|
||||
9 forbidden fixtures); 11 registry checks; runtime schema 3 files/40 tests;
|
||||
unit 118/1,250; component 18/124; integration 8/74; recipes 2/17; coverage at
|
||||
77.40% statements, 73.26% branches, 83.88% functions, and 80.02% lines; risk
|
||||
coverage 381/381 with 76 thresholds; source evidence 202 files/129 baselines;
|
||||
artifact/CI checks; router smoke 9/9; and production build.
|
||||
|
||||
Fresh completion commands and results:
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `corepack pnpm exec vitest run tests/features/tech-log` | 21 files, 170 tests passed |
|
||||
| required four-spec Chromium command above | 48 tests passed |
|
||||
| `corepack pnpm exec playwright test tests/e2e/tech-log-http-contract.spec.ts --project=chromium` | 39 tests passed |
|
||||
| Chromium `@a11y` command above | 29 tests passed |
|
||||
| `corepack pnpm test:visual` | 130 tests passed |
|
||||
| `corepack pnpm check:types` | app/node/test/recipes/web-worker/service-worker passed |
|
||||
| `corepack pnpm lint` | passed with 0 warnings |
|
||||
| `corepack pnpm check:architecture` | 391 modules, 1,212 dependencies, 12 graph checks, 9 forbidden fixtures passed |
|
||||
| `corepack pnpm check:design-system` | 48 tokens and vendor boundaries passed |
|
||||
| `corepack pnpm check:i18n` | 194 keys across 4 locales passed |
|
||||
| `corepack pnpm check:registries` | 11 registries passed; compatibility `none` |
|
||||
| `corepack pnpm check:browser-security` | injection rejected; Public source maps absent |
|
||||
| `corepack pnpm build` | 2,351 modules transformed; build and manifest completed |
|
||||
| `git diff --check` | passed |
|
||||
|
||||
The fresh staged-candidate `corepack pnpm test:all` passed runtime schema 3/40,
|
||||
then its unit phase passed 122 files/1,773 tests and failed 19 tests in only
|
||||
`ci-artifact-contract`. The failures are the documented provider/cgroup,
|
||||
RLIMIT/EMFILE, restrictive-umask, `/tmp`, timing, and identity environment
|
||||
cases; no TechLog test failed. A pre-staging run had also exposed 39
|
||||
release-inventory `APP_HOME` failures because the new serving files were not
|
||||
yet visible to `git ls-files`; staging the complete candidate corrected that
|
||||
test precondition, and all 39 disappeared. Its one aggregate guardian timeout
|
||||
passed 1/1 in isolation and 21/21 in the fresh staged aggregate.
|
||||
|
||||
The earlier exact baseline-isolation command:
|
||||
|
||||
```bash
|
||||
corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts tests/unit/ci-workflow-generation.test.ts tests/unit/http-scenario-evidence.test.ts
|
||||
```
|
||||
|
||||
ran 3 files/529 tests: 510 passed; all 407 CI-workflow and all 14 HTTP-scenario
|
||||
tests passed, leaving the same 19 environment-only CI-artifact cases. Direct
|
||||
runs of the aggregate's remaining phases passed: component 18/124, integration
|
||||
11/82 under the required child-process scope, reference feature 4/13, and
|
||||
recipes 2/17. This environment-only baseline is also recorded in the
|
||||
operations baseline.
|
||||
|
||||
## Fixes, branch audit, and handoff
|
||||
|
||||
Root-cause-driven fixes added regressions for generated Vite manifest chunk
|
||||
resolution, source-compatible raw 404 responses, palette-source detection, and
|
||||
abort rejection. Presentation integration corrections preserve source DOM,
|
||||
ARIA, copy, assets, CSS, workflow state, and focus behavior; no design was
|
||||
introduced. The branch-wide audit of
|
||||
`05e3d50ba01f01c27f257d2e9040c2bc413ea053..HEAD` found no migrated
|
||||
presentation import of adapters, Next.js, Vinext, or Cloudflare. The 27 route
|
||||
chunks are present in the release manifest and derive from actual Vite output,
|
||||
not hard-coded generated filenames.
|
||||
|
||||
The immutable code candidate
|
||||
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a` contains the production 404
|
||||
boundary, parity runner, tests, snapshots, and 27 pending human-review records.
|
||||
Only after that commit existed was the target rebuilt cleanly and the final
|
||||
130-case parity, visual, HTTP, accessibility, and static verification rerun.
|
||||
This report and its JSON are committed separately as
|
||||
`docs: record TechLog migration evidence`; later human accessibility evidence
|
||||
must cite the candidate SHA, not the evidence-only commit.
|
||||
|
||||
The independent `final-review.md` remains the immutable review input with its
|
||||
historical `CHANGES_REQUESTED` verdict. This candidate addresses its production
|
||||
404 issue with a real build artifact and 39 direct HTTP tests; expands the
|
||||
source matrix from 112 to 130 and makes recursive tree equality part of pass;
|
||||
and replaces the missing `tsx` invocation with a checked Node runner and
|
||||
durable provenance. Its accessibility inventory issue is structurally fixed
|
||||
and automated Chromium coverage is green, but the reviewer-dependent 27 human
|
||||
records deliberately remain pending. No review verdict was rewritten or
|
||||
self-approved.
|
||||
|
||||
Manual completion requires a human to check out candidate
|
||||
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, review every route according to
|
||||
`docs/accessibility/manual-checklist.md`, fill each record's exact candidate
|
||||
Release ID, reviewer, signature, attestation, reviewed time, M1-M7, and screen
|
||||
reader result, commit that evidence separately, then rerun
|
||||
`corepack pnpm review:a11y-manual`. Until then `FE-GATE-009` remains pending.
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# The frontend deployment artifact. The repository had none — `dist/server.mjs`
|
||||
# is a preview server that applies neither the security headers nor the cache
|
||||
# policy `config/hosting/` declares — so a deployment had nothing to run.
|
||||
#
|
||||
# Two stages: the build produces `dist/` and, from the serving contract, the
|
||||
# nginx configuration that matches it; the runtime is nginx with both.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pinned by digest: the release-provenance gate requires an immutable runner
|
||||
# identity, and a floating tag cannot give one.
|
||||
FROM node@sha256:3638d9a6fe4030bd716be989438248074489337ba3275657f93595428be4fc03 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# The profile is baked at build time (scripts/generate-runtime-config.ts), so it
|
||||
# has to be chosen here rather than at `docker run`. `dist/config.json` stays a
|
||||
# separate file in the image, which is what makes build-once/promote possible:
|
||||
# a deployment can replace just that file without rebuilding the bundle.
|
||||
ARG APP_PROFILE=production
|
||||
ENV APP_PROFILE=${APP_PROFILE}
|
||||
|
||||
# The bundle and the nginx locations must agree on the prefix the deployment
|
||||
# serves this under: "/" at a domain root, "/dev/" behind a path prefix.
|
||||
ARG VITE_ROUTER_BASE_PATH=/
|
||||
ENV VITE_ROUTER_BASE_PATH=${VITE_ROUTER_BASE_PATH}
|
||||
|
||||
# `CI=true` turns on the release-provenance gate (scripts/lib/build-environment.ts),
|
||||
# which refuses to build without an identity for the artifact. That is the point:
|
||||
# a deployed bundle that cannot say which commit it came from is not traceable,
|
||||
# and the checklist asks exactly that. Supplied as build args so the caller —
|
||||
# a pipeline or the deploy script — owns the values.
|
||||
ENV CI=true
|
||||
ARG VITE_BUILD_ID
|
||||
ARG VITE_COMMIT_SHA
|
||||
ARG RELEASE_ID
|
||||
ARG CI_RUNNER_IMAGE
|
||||
ARG SOURCE_DATE_EPOCH
|
||||
ENV VITE_BUILD_ID=${VITE_BUILD_ID}
|
||||
ENV VITE_COMMIT_SHA=${VITE_COMMIT_SHA}
|
||||
ENV RELEASE_ID=${RELEASE_ID}
|
||||
ENV CI_RUNNER_IMAGE=${CI_RUNNER_IMAGE}
|
||||
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
|
||||
|
||||
# The two values a deployment is allowed to supply (scripts/generate-runtime-
|
||||
# config.ts OVERRIDES); everything else is fixed by the profile. API_BASE_URL
|
||||
# has to be absolute — the runtime canonicalises it with `new URL(value)` — so
|
||||
# even a same-origin deployment names its own origin here. The committed
|
||||
# production profile ships a placeholder (https://api.example.com/), which is
|
||||
# what a deployment that forgets this would silently serve.
|
||||
ARG RUNTIME_API_BASE_URL
|
||||
ARG RUNTIME_TELEMETRY_ENDPOINT
|
||||
ENV RUNTIME_API_BASE_URL=${RUNTIME_API_BASE_URL}
|
||||
ENV RUNTIME_TELEMETRY_ENDPOINT=${RUNTIME_TELEMETRY_ENDPOINT}
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
# Dependencies first so a source-only change does not re-resolve them.
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
COPY . .
|
||||
RUN corepack pnpm build \
|
||||
&& node scripts/generate-nginx-config.ts
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# runtime
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10 AS runtime
|
||||
|
||||
# Replaces the packaged default server block; the generated file is the whole
|
||||
# server definition, including the BFF proxy locations.
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /src/dist/nginx.conf /etc/nginx/conf.d/tech-log.conf
|
||||
COPY --from=build /src/dist/ /usr/share/nginx/html/
|
||||
|
||||
# The generated config is served from /usr/share/nginx/html as root, so the two
|
||||
# copies above would also publish nginx.conf itself. It is not secret, but it is
|
||||
# not a page either.
|
||||
RUN rm -f /usr/share/nginx/html/nginx.conf /usr/share/nginx/html/server.mjs \
|
||||
&& rm -rf /usr/share/nginx/html/.vite \
|
||||
# The build writes config.json 0600, which nginx (running as `nginx`) cannot
|
||||
# read — the container came up healthy and answered 403 for the one file the
|
||||
# SPA needs before it can boot. Normalise what is served to world-readable.
|
||||
&& chmod -R a+rX /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# No `nginx -t` here: proxy_pass names are resolved when the config loads, and
|
||||
# `backend`/`keycloak` only exist on the compose network. The container's own
|
||||
# startup is the check, and it fails loudly.
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1${VITE_ROUTER_BASE_PATH:-/}config.json || exit 1
|
||||
@@ -1,266 +1 @@
|
||||
# Tech Log Frontend
|
||||
|
||||
Initialized from `clean-architecture-frontend-template` revision
|
||||
`4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is
|
||||
recorded in `template.lock.json`.
|
||||
|
||||
A React/Vite TechLog application where architecture boundaries, integration
|
||||
behavior, release coherence, accessibility, performance, and operations are
|
||||
executable contracts rather than conventions.
|
||||
|
||||
## Start locally
|
||||
|
||||
Requirements: the exact Node.js version in `.nvmrc` (currently 24.14.0) and
|
||||
Corepack. The repository pins pnpm in `package.json`.
|
||||
|
||||
Product source, tests, build/quality scripts, and supported tool configuration
|
||||
are TypeScript/TSX. `allowJs` is disabled. Node-side `.ts` scripts run directly
|
||||
on the pinned Node 24 runtime and are checked with NodeNext resolution plus
|
||||
erasable-syntax enforcement. Project-owned executable source contains no
|
||||
JavaScript-family files; negative architecture, security, and type-compatibility
|
||||
fixtures are TypeScript/TSX as well.
|
||||
|
||||
```bash
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm dev
|
||||
```
|
||||
|
||||
Runtime-public settings live in `public/config.json` and are validated before
|
||||
the product tree mounts. Client secrets are forbidden.
|
||||
|
||||
## TechLog experience
|
||||
|
||||
The default build mounts the source-faithful TechLog Public and Studio
|
||||
experience. Public routes provide discovery, search, documents, topics,
|
||||
projects, releases, and profile content. `/studio` provides the session-scoped
|
||||
mock authoring workflow: create, edit, validate, preview, publish, unpublish,
|
||||
and immutable publication history.
|
||||
|
||||
The 27 canonical route definitions are divided into `PUBLIC` and `STUDIO`
|
||||
nested layouts. Studio authentication remains deliberately deferred; its mock
|
||||
gateway state lasts for one Studio shell session and resets on a full document
|
||||
load. The exact route, dependency, stylesheet, asset, and parity inventory is
|
||||
recorded in
|
||||
[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md).
|
||||
|
||||
`corepack pnpm build` emits a self-contained `dist/server.mjs` production
|
||||
boundary. `corepack pnpm preview --host 127.0.0.1 --port 4174` serves known
|
||||
Public and Studio routes as SPA documents, preserves the in-shell Studio 404,
|
||||
and returns source-exact raw `404 text/plain` responses for missing Public
|
||||
content. With the read-only source temp-copy server on `4375`, run:
|
||||
|
||||
```bash
|
||||
TECH_LOG_SOURCE_URL=http://127.0.0.1:4375 \
|
||||
TECH_LOG_TARGET_URL=http://127.0.0.1:4174 \
|
||||
corepack pnpm verify:tech-log-source-parity
|
||||
```
|
||||
|
||||
### Studio backend source
|
||||
|
||||
`TECH_LOG_STUDIO_SOURCE` (`MOCK` | `HTTP`) selects which `StudioGateway`
|
||||
adapter the composition root wires up. It defaults to `MOCK` — the
|
||||
session-scoped in-memory Studio described above — so the existing Studio
|
||||
workflow and its test suites are unaffected unless the switch is deliberately
|
||||
turned on. Setting it to `HTTP` wires the HTTP `StudioGateway` instead, which
|
||||
calls the canonical `@tech-log/studio-contract` operations against
|
||||
`API_BASE_URL`. With no backend reachable at that URL, Studio still boots and
|
||||
its shell renders; the specific panels that need the backend show an inline
|
||||
"failed to load" state rather than a blank screen or an unhandled exception.
|
||||
|
||||
The switch is a field on the versioned runtime config document
|
||||
(`RuntimeConfigV2`), not a build-time flag:
|
||||
|
||||
- `config/runtime/{local,development,staging,production}.json` are the
|
||||
deployment profiles `corepack pnpm build` (via
|
||||
`scripts/generate-runtime-config.ts`) materializes into `dist/config.json`
|
||||
for a real build.
|
||||
- `corepack pnpm dev` does not run that step. Plain `vite` serves
|
||||
`public/config.json` (and `public/release-manifest.json`) verbatim as dev
|
||||
fixtures — editing `config/runtime/local.json` alone has no effect on
|
||||
`pnpm dev`. To exercise `HTTP` mode under `pnpm dev`, set
|
||||
`TECH_LOG_STUDIO_SOURCE` in `public/config.json` directly.
|
||||
|
||||
### The dev release manifest must declare the compiled contract set
|
||||
|
||||
Contract-set verification runs unconditionally at boot, before any adapter is
|
||||
selected. It is not an `HTTP`-mode caveat: if `public/release-manifest.json`'s
|
||||
`contractSet` does not match the set the build compiled, `corepack pnpm dev`
|
||||
does not start the app at all — it renders the fail-closed boot screen
|
||||
(`CONTRACT_SET_MISMATCH` / `CONTRACT_SET_PACKAGE_MISSING`) in the **default
|
||||
`MOCK` mode** too. A developer who runs `pnpm dev` and gets a boot error has a
|
||||
broken dev server, however tidy the screen looks; treat it as a defect in the
|
||||
fixture, never as expected behaviour.
|
||||
|
||||
The expectation comes from `EXPECTED_CONTRACT_SET_PACKAGES`
|
||||
(`src/features/installed-contract-contributions.ts`), and a real build writes it
|
||||
into `dist/release-manifest.json` from `scripts/generate-contract-set.ts`, so
|
||||
production builds are always self-consistent. Only the hand-maintained dev
|
||||
fixture can drift, and it drifts whenever either half moves — a regenerated
|
||||
contract (new package digest or version) or a contribution added to or removed
|
||||
from `installed-contract-contributions.ts`. Two things keep it honest:
|
||||
|
||||
- `corepack pnpm generate:tech-log-contract` refreshes the fixture's
|
||||
`contractSet` block as its last step, so regenerating the contract can never
|
||||
leave the two out of step. `corepack pnpm generate:dev-release-manifest`
|
||||
refreshes the same block on its own, for the contribution-list case that does
|
||||
not go through contract generation.
|
||||
- `corepack pnpm check:dev-release-manifest` is the gate. It compares the
|
||||
fixture's `setAlgorithm`, `setDigest` and package set against the compiled
|
||||
set and fails on any difference. It runs in CI as part of FE-GATE-010, which
|
||||
is what catches the changes the generation step cannot see.
|
||||
|
||||
### TechLog contract generation
|
||||
|
||||
The Studio HTTP contract is vendored from a canonical OpenAPI source, not
|
||||
hand-written:
|
||||
|
||||
- `corepack pnpm generate:tech-log-contract` regenerates
|
||||
`src/features/tech-log/contracts/studio/studio-api.openapi.yaml`,
|
||||
`generated.ts`, and `canonical-source.json` from the canonical
|
||||
`tech-log-design-package` repository (path from `TECH_LOG_DESIGN_PACKAGE`,
|
||||
default `/home/donghyeon/workspace/tech-log-design-package`). It needs that
|
||||
repository checked out locally and network access, because type generation
|
||||
runs in an isolated `pnpm dlx` sandbox (this repo pins TypeScript 7, which
|
||||
has no classic compiler API for `openapi-typescript` to use). Run it after
|
||||
the canonical contract changes, then commit the regenerated files.
|
||||
- `corepack pnpm check:tech-log-contract` is the drift gate: it hashes the
|
||||
vendored yaml against the recorded digest and confirms every recorded
|
||||
`operationId` is present in both the yaml and the generated types. It needs
|
||||
neither the canonical repository nor the network, so it runs in CI and in
|
||||
this sandbox. Run it any time to confirm the vendored contract has not
|
||||
drifted from what was last generated.
|
||||
|
||||
## Architecture
|
||||
|
||||
Dependencies point inward:
|
||||
|
||||
```text
|
||||
presentation -> application -> domain
|
||||
adapters -----^
|
||||
bootstrap composes concrete adapters
|
||||
contracts own cross-cutting registries
|
||||
```
|
||||
|
||||
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||
`docs/architecture/starter-experience.md`. TechLog is one feature boundary
|
||||
under `src/features/tech-log`. Its immutable Public catalog and session-scoped
|
||||
Studio mock gateway are injected through the application feature input;
|
||||
Public and Studio presentation code shares the typed content renderer without
|
||||
importing concrete adapters. The retained reference feature remains a
|
||||
non-product platform contract fixture and can be removed without changing the
|
||||
TechLog route set.
|
||||
|
||||
### Platform capability review
|
||||
|
||||
The starter shell is implemented, but the repository review also records the
|
||||
remaining work required before feature teams can use every declared contract
|
||||
through one end-to-end application path:
|
||||
|
||||
- [platform capability review](docs/architecture/frontend-platform-capability-review.md)
|
||||
- [ports, adapters, and feature boundaries](docs/architecture/frontend-ports-adapters-and-boundaries.md)
|
||||
- [REST, GraphQL, Connect/gRPC-Web, Schema, Mapper, and Server State](docs/architecture/api-contract-schema-mapper-and-server-state.md)
|
||||
- [Protobuf browser transports and REST Gateway](docs/architecture/protobuf-browser-transport-and-rest-gateway.md)
|
||||
- [backend API and Server State handoff contract](docs/architecture/backend-api-and-server-state-contract.md)
|
||||
- [TypeScript, state ownership, and data flow](docs/architecture/typescript-state-and-data-flow.md)
|
||||
- [routing, page templates, and reusable patterns](docs/architecture/routing-pages-and-patterns.md)
|
||||
- [browser data capability completion ledger](docs/architecture/browser-data-capability-completion-ledger.md)
|
||||
- [browser file and origin-storage platform](docs/architecture/browser-file-and-origin-storage.md)
|
||||
- [client cache and storage](docs/architecture/client-cache-and-storage.md)
|
||||
- [realtime events, Web Push, and bounded polling](docs/architecture/realtime-events-web-push-and-bounded-polling.md)
|
||||
- [presigned transfer, resumable upload, streaming download, and Image CDN](docs/architecture/presigned-transfer-and-image-cdn.md)
|
||||
- [server file capability infrastructure](docs/architecture/server-file-capability-infrastructure.md)
|
||||
- [design-system platform](docs/styling/design-system-platform.md)
|
||||
- [frontend platform testing strategy](docs/testing/frontend-platform-testing-strategy.md)
|
||||
- [implementation roadmap](docs/architecture/frontend-platform-implementation-roadmap.md)
|
||||
|
||||
These documents distinguish repository defaults from opt-in adapters and
|
||||
project-owned integrations. They are target designs and review findings; a
|
||||
capability is not treated as implemented until its branch acceptance criteria
|
||||
and executable gates pass.
|
||||
|
||||
## Verification
|
||||
|
||||
Common local checks:
|
||||
|
||||
```bash
|
||||
corepack pnpm lint
|
||||
corepack pnpm check:types
|
||||
corepack pnpm check:types:app
|
||||
corepack pnpm check:types:node
|
||||
corepack pnpm check:types:test
|
||||
corepack pnpm check:architecture
|
||||
corepack pnpm test:all
|
||||
corepack pnpm test:e2e
|
||||
corepack pnpm test:a11y
|
||||
corepack pnpm build
|
||||
corepack pnpm check:bundle
|
||||
corepack pnpm test:performance
|
||||
corepack pnpm verify:compatibility
|
||||
corepack pnpm verify:release
|
||||
corepack pnpm check:registries
|
||||
corepack pnpm drill:runbooks
|
||||
corepack pnpm check:ci
|
||||
corepack pnpm exec vitest run tests/features/tech-log
|
||||
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||
corepack pnpm test:visual
|
||||
```
|
||||
|
||||
`check:types`는 source, Node scripts/config와 tests를 분리된 TypeScript
|
||||
project로 모두 검사한다. type/architecture/security/registry의 invalid
|
||||
fixture는 `config/ci/gates.json`에서 “실패해야 통과”하는 negative gate로
|
||||
실행된다. 도구 호환성 결정은
|
||||
[VD-01](docs/architecture/decisions/VD-01-typescript-lint-tooling.md)에 기록돼
|
||||
있다.
|
||||
|
||||
Application feature input은 module augmentation으로 닫힌 ID와 정확한 input
|
||||
shape를 제공하며, 공통 `Result<Value, Failure = AppFailure>`는 error registry의
|
||||
failure kind만 application/presentation 경계를 통과시킨다. Architecture gate는
|
||||
TypeScript/TSX의 static, dynamic, type import를 별도 정적 그래프로 분석하고
|
||||
runtime/source 영역의 JavaScript 재유입도 거절한다. 해석되지 않은 import,
|
||||
parse failure, 금지 계층 edge와 순환 의존은 모두 fail-closed이며 전용
|
||||
TypeScript/TSX negative fixture로도 검증된다.
|
||||
|
||||
Install the pinned Playwright browser engines before the first cross-browser
|
||||
run:
|
||||
|
||||
```bash
|
||||
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 27 registered routes:
|
||||
`NOT_FOUND`, `TECH_LOG_CASE`, `TECH_LOG_EXPLORE`, `TECH_LOG_EXPLORE_KIND`,
|
||||
`TECH_LOG_HOME`, `TECH_LOG_PROFILE`, `TECH_LOG_PROJECT`,
|
||||
`TECH_LOG_PROJECTS`, `TECH_LOG_PROJECT_ACTIVITY`,
|
||||
`TECH_LOG_PROJECT_DECISIONS`, `TECH_LOG_PROJECT_RECORDS`,
|
||||
`TECH_LOG_QUESTION`, `TECH_LOG_REFERENCE`, `TECH_LOG_RELEASE`,
|
||||
`TECH_LOG_RELEASES`, `TECH_LOG_SEARCH`, `TECH_LOG_STUDIO_DOCUMENTS`,
|
||||
`TECH_LOG_STUDIO_DOCUMENT_EDIT`, `TECH_LOG_STUDIO_DOCUMENT_NEW`,
|
||||
`TECH_LOG_STUDIO_DOCUMENT_PREVIEW`, `TECH_LOG_STUDIO_DOCUMENT_PUBLISH`,
|
||||
`TECH_LOG_STUDIO_DOCUMENT_VALIDATION`, `TECH_LOG_STUDIO_HOME`,
|
||||
`TECH_LOG_STUDIO_NOT_FOUND`, `TECH_LOG_STUDIO_PUBLICATIONS`,
|
||||
`TECH_LOG_STUDIO_PUBLICATION_PREVIEW`, `TECH_LOG_TOPIC`.
|
||||
`verify:documentation` derives that list from the route registry and fails if
|
||||
this paragraph falls behind it.
|
||||
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
||||
eligible-sample threshold and 28 days of production data exist.
|
||||
|
||||
Live release verification additionally requires `HOSTING_BASE_URL`.
|
||||
|
||||
## CI and evidence
|
||||
|
||||
The 26-gate registry is `config/ci/gates.json`; the Gitea workflow is
|
||||
`.gitea/workflows/quality-gates.yml`. It follows:
|
||||
|
||||
```text
|
||||
MERGE_READY -> RELEASE_READY -> PROD_PROMOTION_READY -> FIELD_SLO_READY
|
||||
```
|
||||
|
||||
`DOCUMENTATION_READY` is independent. No gate is downgraded to a warning.
|
||||
Machine-readable evidence is written below `artifacts/`; generated evidence is
|
||||
ignored by Git while `.gitkeep` files preserve the taxonomy.
|
||||
|
||||
Operational details are in `docs/operations/`, with incident procedures in
|
||||
`docs/runbooks/`.
|
||||
tech-log
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# NOT_FOUND accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: NOT_FOUND
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_CASE accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_CASE
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_EXPLORE accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_EXPLORE
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_EXPLORE_KIND accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_EXPLORE_KIND
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_HOME accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_HOME
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_PROFILE accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_PROFILE
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_PROJECT accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_PROJECT
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_PROJECTS accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_PROJECTS
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_PROJECT_ACTIVITY accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_PROJECT_ACTIVITY
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_PROJECT_DECISIONS accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_PROJECT_DECISIONS
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_PROJECT_RECORDS accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_PROJECT_RECORDS
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_QUESTION accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_QUESTION
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_REFERENCE accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_REFERENCE
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_RELEASE accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_RELEASE
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_RELEASES accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_RELEASES
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_SEARCH accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_SEARCH
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_ASSETS accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_ASSETS
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_DOCUMENTS accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_DOCUMENTS
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_DOCUMENT_EDIT accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_DOCUMENT_EDIT
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_DOCUMENT_NEW accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_DOCUMENT_NEW
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_DOCUMENT_PREVIEW accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_DOCUMENT_PREVIEW
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_DOCUMENT_PUBLISH accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_DOCUMENT_PUBLISH
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_DOCUMENT_VALIDATION accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_DOCUMENT_VALIDATION
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_HOME accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_HOME
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_NOT_FOUND accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_NOT_FOUND
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_PROJECT_EDIT accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_PROJECT_EDIT
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_PUBLICATIONS accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_PUBLICATIONS
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_PUBLICATION_PREVIEW accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_PUBLICATION_PREVIEW
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_RELEASES accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_RELEASES
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_RELEASE_EDIT accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_RELEASE_EDIT
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_STUDIO_TAXONOMY accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_STUDIO_TAXONOMY
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
@@ -1,18 +0,0 @@
|
||||
# TECH_LOG_TOPIC accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: TECH_LOG_TOPIC
|
||||
Release ID:
|
||||
Reviewer:
|
||||
Reviewed at:
|
||||
Signature:
|
||||
Attestation: pending
|
||||
M1 Keyboard: pending
|
||||
M2 Visible focus: pending
|
||||
M3 Route focus: pending
|
||||
M4 Modal focus: pending
|
||||
M5 Error association: pending
|
||||
M6 Color signal: pending
|
||||
M7 Reduced motion: pending
|
||||
Screen reader: pending
|
||||
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"layers": {
|
||||
"domain": {
|
||||
"root": "src/domain",
|
||||
"mayImport": ["src/domain"]
|
||||
},
|
||||
"application": {
|
||||
"root": "src/application",
|
||||
"mayImport": ["src/application", "src/domain", "src/contracts"]
|
||||
},
|
||||
"presentation": {
|
||||
"root": "src/presentation",
|
||||
"mayImport": ["src/presentation", "src/application", "src/domain", "src/contracts"]
|
||||
},
|
||||
"adapters": {
|
||||
"root": "src/adapters",
|
||||
"mayImport": ["src/adapters", "src/application", "src/domain", "src/contracts"]
|
||||
},
|
||||
"bootstrap": {
|
||||
"root": "src/bootstrap",
|
||||
"mayImport": ["src"]
|
||||
}
|
||||
},
|
||||
"forbidden": [
|
||||
["domain", "application"],
|
||||
["domain", "presentation"],
|
||||
["domain", "adapters"],
|
||||
["domain", "bootstrap"],
|
||||
["application", "presentation"],
|
||||
["application", "adapters"],
|
||||
["application", "bootstrap"],
|
||||
["presentation", "adapters"],
|
||||
["presentation", "bootstrap"],
|
||||
["adapters", "presentation"],
|
||||
["adapters", "bootstrap"]
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"families": {
|
||||
"api": {
|
||||
"additive": {
|
||||
"before": { "required": ["id"], "properties": { "id": {} } },
|
||||
"after": {
|
||||
"required": ["id"],
|
||||
"properties": { "id": {}, "displayName": {} }
|
||||
}
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "required": ["id"], "properties": { "id": {} } },
|
||||
"after": {
|
||||
"required": ["id", "name"],
|
||||
"properties": { "id": {}, "name": {} }
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"additive": {
|
||||
"before": { "required": ["APP_ENV"], "properties": { "APP_ENV": {} } },
|
||||
"after": {
|
||||
"required": ["APP_ENV"],
|
||||
"properties": { "APP_ENV": {}, "OPTIONAL_FLAG": {} }
|
||||
}
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "required": ["APP_ENV"], "properties": { "APP_ENV": {} } },
|
||||
"after": {
|
||||
"required": ["APP_ENV", "NEW_REQUIRED"],
|
||||
"properties": { "APP_ENV": {}, "NEW_REQUIRED": {} }
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"additive": {
|
||||
"before": { "properties": { "theme": {} } },
|
||||
"after": { "properties": { "theme": {}, "contrast": {} } }
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "properties": { "theme": {} } },
|
||||
"after": { "properties": {} }
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"additive": {
|
||||
"before": { "required": ["buildId"], "properties": { "buildId": {} } },
|
||||
"after": {
|
||||
"required": ["buildId"],
|
||||
"properties": { "buildId": {}, "builtAt": {} }
|
||||
}
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "required": ["buildId"], "properties": { "buildId": {} } },
|
||||
"after": {
|
||||
"required": ["buildId", "assetManifestHash"],
|
||||
"properties": { "buildId": {}, "assetManifestHash": {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"snapshotDigest": "428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b",
|
||||
"owner": "tech-log-frontend",
|
||||
"reason": "Install approved TechLog Public and Studio route contract",
|
||||
"approvedAt": "2026-08-15T16:32:32.042Z"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,365 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"changes": [
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:allowedValues:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:breakingFields:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:fieldTypes:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:requiredFields:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:APP_HOME:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_AUTH:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_PLATFORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_STATES:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_UI:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_DETAIL:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_FORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_LIST:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_STATUS:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:APP_HOME:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_AUTH:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_PLATFORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_STATES:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_UI:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:NOT_FOUND:moduleId:field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_DETAIL:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_FORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_LIST:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_STATUS:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ENV:API_CONTRACT_VERSION:*:removed",
|
||||
"versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.",
|
||||
"migration": "Release Manifest V2 contractSet replaces the scalar: the compiled external contract package set is canonicalized and digested, and boot compares it against the manifest.",
|
||||
"compatibilityWindow": "The V1 config and V1 manifest readers stay in place for one release. A V1 document may still carry the scalar; a V2 document is rejected if it does.",
|
||||
"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.",
|
||||
"migration": "Every installed query row declares invalidate-only; older tabs remain local-only.",
|
||||
"compatibilityWindow": "Mixed releases are isolated by release cacheEpoch and never exchange query keys or values.",
|
||||
"rollback": "Remove the coordinator composition and the two query registry fields.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-QUERY:$contract:breakingFields:contract-field-changed",
|
||||
"versionBump": "Cross-context invalidation wire protocol starts at version 1.",
|
||||
"migration": "Every installed query row declares its opaque topic and transport policy.",
|
||||
"compatibilityWindow": "A release cacheEpoch rejects messages from a different deployed contract.",
|
||||
"rollback": "Remove the coordinator composition and restore the prior query registry contract.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-QUERY:$contract:fieldTypes:contract-field-changed",
|
||||
"versionBump": "Cross-context invalidation wire protocol starts at version 1.",
|
||||
"migration": "The installed reference query row and all consumers were updated atomically.",
|
||||
"compatibilityWindow": "Old clients do not consume the new fields; new clients validate them before boot.",
|
||||
"rollback": "Restore the previous query registry field types and local-only invalidation.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-QUERY:$contract:requiredFields:contract-field-changed",
|
||||
"versionBump": "Cross-context invalidation wire protocol starts at version 1.",
|
||||
"migration": "Missing topics now fail registry validation instead of silently degrading at runtime.",
|
||||
"compatibilityWindow": "Only a fully built release consumes its own installed registry snapshot.",
|
||||
"rollback": "Remove the newly required fields and cross-context composition together.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-QUERY:$contract:uniqueFields:contract-field-changed",
|
||||
"versionBump": "Cross-context invalidation wire protocol starts at version 1.",
|
||||
"migration": "Existing query namespaces received unique registry-issued opaque topics.",
|
||||
"compatibilityWindow": "Topics are scoped by release cacheEpoch, so mixed releases cannot collide.",
|
||||
"rollback": "Drop invalidationTopic uniqueness after removing the transport consumer.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-RELEASE:apiContractVersion:compatibilityRole:field-changed",
|
||||
"versionBump": "The release token registry adds contractSetDigest and demotes apiContractVersion to a legacy V1 scalar.",
|
||||
"migration": "compareReleaseToRuntime no longer sources the scalar from Runtime Config; when neither side declares it there is nothing to compare and contractSet verification owns contract coherence.",
|
||||
"compatibilityWindow": "A V1 manifest still supplies apiContractVersion and is still compared against a V1 config that declares one.",
|
||||
"rollback": "Restore the previous compatibilityRole text and remove the contractSetDigest token together with the V2 manifest writer.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-STORAGE:$contract:allowedValues:contract-field-changed",
|
||||
"versionBump": "Web Storage value codec contracts start at version 1.",
|
||||
"migration": "Existing keys retain their physical schema version; values outside the selected codec are discarded.",
|
||||
"compatibilityWindow": "Valid existing color-scheme and opaque-string records remain readable.",
|
||||
"rollback": "Restore the previous registry contract; no physical key deletion is required.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-STORAGE:$contract:breakingFields:contract-field-changed",
|
||||
"versionBump": "Web Storage value codec contracts start at version 1.",
|
||||
"migration": "Each existing row received an explicit codec; executable migration was never consumed and is now forbidden.",
|
||||
"compatibilityWindow": "Schema-versioned physical keys and the envelope shape remain unchanged.",
|
||||
"rollback": "Remove valueCodec enforcement and restore the prior metadata declaration.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-STORAGE:$contract:fieldTypes:contract-field-changed",
|
||||
"versionBump": "Web Storage value codec contracts start at version 1.",
|
||||
"migration": "The dead function migration union was narrowed to the only implemented discard policy.",
|
||||
"compatibilityWindow": "All installed definitions already used discard before this contract change.",
|
||||
"rollback": "Restore the former type metadata without rewriting persisted records.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-STORAGE:$contract:requiredFields:contract-field-changed",
|
||||
"versionBump": "Web Storage value codec contracts start at version 1.",
|
||||
"migration": "All installed storage rows now declare their closed value codec.",
|
||||
"compatibilityWindow": "Existing valid envelopes continue to decode; invalid values fail closed.",
|
||||
"rollback": "Remove the required codec field and runtime codec dispatch together.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_HOME:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENTS:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_NEW:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_EDIT:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_VALIDATION:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_PREVIEW:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_PUBLISH:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_PUBLICATIONS:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_PUBLICATION_PREVIEW:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_NOT_FOUND:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"sourceDirectories": [
|
||||
"src/application",
|
||||
"src/presentation",
|
||||
"src/domain"
|
||||
],
|
||||
"registries": [
|
||||
{
|
||||
"registryId": "FE-REG-ROUTE",
|
||||
"path": "src/features/installed-feature-contracts.ts",
|
||||
"exportName": "ROUTE_REGISTRY",
|
||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||
"keyField": "routeId",
|
||||
"requiredFields": [
|
||||
"routeId",
|
||||
"path",
|
||||
"layoutGroup",
|
||||
"paramsSchema",
|
||||
"searchSchema",
|
||||
"access",
|
||||
"loadingSurface",
|
||||
"errorSurface",
|
||||
"chunkId",
|
||||
"title",
|
||||
"navigationLabel",
|
||||
"navigationOrder"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"routeId": "string",
|
||||
"path": "string",
|
||||
"layoutGroup": "string",
|
||||
"paramsSchema": "string|null",
|
||||
"searchSchema": "string|null",
|
||||
"access": "string",
|
||||
"loadingSurface": "string",
|
||||
"errorSurface": "string",
|
||||
"chunkId": "string",
|
||||
"title": "string",
|
||||
"navigationLabel": "string|null",
|
||||
"navigationOrder": "integer|null"
|
||||
},
|
||||
"uniqueFields": ["routeId", "path", "chunkId"],
|
||||
"allowedValues": {
|
||||
"access": ["public", "session-required"],
|
||||
"layoutGroup": ["PUBLIC", "STUDIO"],
|
||||
"paramsSchema": [
|
||||
null,
|
||||
"NotFoundSplat",
|
||||
"ReferenceResourceParams",
|
||||
"TechLogExploreKindParams",
|
||||
"TechLogSlugParams",
|
||||
"TechLogVersionParams",
|
||||
"TechLogDocumentIdParams",
|
||||
"TechLogPublicationEventIdParams",
|
||||
"TechLogStudioSplat"
|
||||
],
|
||||
"searchSchema": [
|
||||
null,
|
||||
"ReferenceResourceListQuery",
|
||||
"TechLogHomeSearch",
|
||||
"TechLogExploreSearch",
|
||||
"TechLogExploreKindSearch",
|
||||
"TechLogSearchQuery",
|
||||
"TechLogCaseStateSearch"
|
||||
],
|
||||
"loadingSurface": [
|
||||
"app-shell",
|
||||
"example-page",
|
||||
"reference-resource-list",
|
||||
"reference-resource-detail",
|
||||
"reference-resource-form",
|
||||
"reference-resource-status",
|
||||
"none"
|
||||
],
|
||||
"errorSurface": [
|
||||
"route-boundary",
|
||||
"feature-boundary",
|
||||
"not-found"
|
||||
]
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"field": "routeId",
|
||||
"registryId": "FE-REG-ROUTE-RUNTIME",
|
||||
"targetField": "routeId"
|
||||
},
|
||||
{
|
||||
"field": "paramsSchema",
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"targetField": "schemaId"
|
||||
},
|
||||
{
|
||||
"field": "searchSchema",
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"targetField": "schemaId"
|
||||
}
|
||||
],
|
||||
"consumers": [
|
||||
{
|
||||
"path": "src/presentation/routes/app-router.tsx",
|
||||
"token": "ROUTE_REGISTRY"
|
||||
}
|
||||
],
|
||||
"breakingFields": [
|
||||
"routeId",
|
||||
"path",
|
||||
"layoutGroup",
|
||||
"paramsSchema",
|
||||
"searchSchema",
|
||||
"access",
|
||||
"chunkId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-ROUTE-RUNTIME",
|
||||
"path": "src/features/installed-feature-contracts.ts",
|
||||
"exportName": "ROUTE_RUNTIME_CONTRACT",
|
||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||
"keyField": "routeId",
|
||||
"requiredFields": [
|
||||
"routeId",
|
||||
"moduleId",
|
||||
"paramsCodec",
|
||||
"searchCodec"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"routeId": "string",
|
||||
"moduleId": "string",
|
||||
"paramsCodec": "string",
|
||||
"searchCodec": "string"
|
||||
},
|
||||
"uniqueFields": ["routeId", "moduleId"],
|
||||
"references": [
|
||||
{
|
||||
"field": "routeId",
|
||||
"registryId": "FE-REG-ROUTE",
|
||||
"targetField": "routeId"
|
||||
},
|
||||
{
|
||||
"field": "paramsCodec",
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"targetField": "schemaId"
|
||||
},
|
||||
{
|
||||
"field": "searchCodec",
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"targetField": "schemaId"
|
||||
}
|
||||
],
|
||||
"consumers": [
|
||||
{
|
||||
"path": "src/presentation/routes/route-codecs.ts",
|
||||
"token": "ROUTE_RUNTIME_CONTRACT"
|
||||
}
|
||||
],
|
||||
"breakingFields": [
|
||||
"routeId",
|
||||
"moduleId",
|
||||
"paramsCodec",
|
||||
"searchCodec"
|
||||
]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-API",
|
||||
"path": "src/features/installed-feature-contracts.ts",
|
||||
"exportName": "API_OPERATIONS",
|
||||
"owner": "feature-frontend-api-client-response-envelope-contract",
|
||||
"keyField": "operationId",
|
||||
"requiredFields": [
|
||||
"method",
|
||||
"path",
|
||||
"operationId",
|
||||
"auth",
|
||||
"timeoutMs",
|
||||
"idempotency",
|
||||
"retry",
|
||||
"requestSource",
|
||||
"requestSchema",
|
||||
"responseSchema",
|
||||
"owner"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"method": "string",
|
||||
"path": "string",
|
||||
"operationId": "string",
|
||||
"auth": "string",
|
||||
"timeoutMs": "integer|null",
|
||||
"idempotency": "string",
|
||||
"retry": "string",
|
||||
"requestSource": "string",
|
||||
"requestSchema": "string",
|
||||
"responseSchema": "string",
|
||||
"owner": "string"
|
||||
},
|
||||
"uniqueFields": ["operationId"],
|
||||
"allowedValues": {
|
||||
"method": ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
||||
"auth": ["none", "external-session"],
|
||||
"idempotency": ["safe", "keyed", "none"],
|
||||
"retry": ["runtime", "never"],
|
||||
"requestSource": ["none", "search", "body"]
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"field": "requestSchema",
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"targetField": "schemaId"
|
||||
},
|
||||
{
|
||||
"field": "responseSchema",
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"targetField": "schemaId"
|
||||
}
|
||||
],
|
||||
"consumerIdentityField": "operationId",
|
||||
"consumerDirectories": [
|
||||
"src/features/reference-feature/adapters",
|
||||
"src/features/reference-feature/application"
|
||||
],
|
||||
"breakingFields": [
|
||||
"method",
|
||||
"path",
|
||||
"operationId",
|
||||
"auth",
|
||||
"idempotency",
|
||||
"requestSource",
|
||||
"requestSchema",
|
||||
"responseSchema"
|
||||
]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"path": "src/features/installed-feature-contracts.ts",
|
||||
"exportName": "SCHEMA_REGISTRY",
|
||||
"owner": "feature-frontend-contract-schema-registry",
|
||||
"keyField": "schemaId",
|
||||
"requiredFields": ["schemaId", "boundary", "owner", "runtime"],
|
||||
"fieldTypes": {
|
||||
"schemaId": "string",
|
||||
"boundary": "string",
|
||||
"owner": "string",
|
||||
"runtime": "string"
|
||||
},
|
||||
"uniqueFields": ["schemaId"],
|
||||
"allowedValues": {
|
||||
"boundary": [
|
||||
"route-params",
|
||||
"route-search",
|
||||
"route-search-api-request",
|
||||
"api-request",
|
||||
"api-response"
|
||||
],
|
||||
"runtime": ["zod"]
|
||||
},
|
||||
"consumerIdentityField": "schemaId",
|
||||
"consumerDirectories": [
|
||||
"src/presentation/routes",
|
||||
"src/features/tech-log/presentation",
|
||||
"src/features/reference-feature/contracts"
|
||||
],
|
||||
"breakingFields": ["schemaId", "boundary", "runtime"]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-ENV",
|
||||
"path": "src/contracts/env.ts",
|
||||
"exportName": "ENV_REGISTRY",
|
||||
"owner": "feature-frontend-env-runtime-config-contract",
|
||||
"requiredFields": ["phase", "classification", "required", "defaultValue"],
|
||||
"fieldTypes": {
|
||||
"phase": "string",
|
||||
"classification": "string",
|
||||
"required": "boolean",
|
||||
"defaultValue": "string|integer|boolean|null"
|
||||
},
|
||||
"allowedValues": {
|
||||
"phase": ["build", "runtime"],
|
||||
"classification": [
|
||||
"public",
|
||||
"public-sensitive",
|
||||
"public-metadata",
|
||||
"compile-time"
|
||||
]
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"path": "src/bootstrap/runtime-config-schema.ts",
|
||||
"token": "APP_ENV"
|
||||
},
|
||||
{
|
||||
"path": "src/contracts/env.ts",
|
||||
"token": "getBuildConfig"
|
||||
}
|
||||
],
|
||||
"breakingFields": ["phase", "classification", "required"]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-STORAGE",
|
||||
"path": "src/contracts/storage-keys.ts",
|
||||
"exportName": "STORAGE_REGISTRY",
|
||||
"owner": "feature-frontend-storage-registry-contract",
|
||||
"keyField": "logicalName",
|
||||
"requiredFields": [
|
||||
"logicalName",
|
||||
"physicalKey",
|
||||
"backend",
|
||||
"classification",
|
||||
"schemaVersion",
|
||||
"valueCodec",
|
||||
"ttl",
|
||||
"migration",
|
||||
"quotaFallback"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"logicalName": "string",
|
||||
"physicalKey": "string",
|
||||
"backend": "string",
|
||||
"classification": "string",
|
||||
"schemaVersion": "integer",
|
||||
"valueCodec": "string",
|
||||
"ttl": "integer|string|null",
|
||||
"migration": "string",
|
||||
"quotaFallback": "string"
|
||||
},
|
||||
"uniqueFields": ["logicalName", "physicalKey"],
|
||||
"allowedValues": {
|
||||
"backend": [
|
||||
"memory",
|
||||
"sessionStorage",
|
||||
"localStorage",
|
||||
"disabled",
|
||||
"forbidden"
|
||||
],
|
||||
"classification": [
|
||||
"public-preference",
|
||||
"opaque-cache",
|
||||
"sensitive-forbidden"
|
||||
],
|
||||
"valueCodec": [
|
||||
"color-scheme-v1",
|
||||
"opaque-string-v1",
|
||||
"none"
|
||||
],
|
||||
"migration": ["discard"],
|
||||
"quotaFallback": ["memory", "no-persist", "feature-disable"]
|
||||
},
|
||||
"consumerIdentityField": "logicalName",
|
||||
"consumerDirectories": ["src", "tests"],
|
||||
"orphanExemptRows": ["QUERY_PERSISTENCE", "AUTH_TOKEN"],
|
||||
"breakingFields": [
|
||||
"logicalName",
|
||||
"physicalKey",
|
||||
"backend",
|
||||
"classification",
|
||||
"schemaVersion",
|
||||
"valueCodec",
|
||||
"migration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-ERROR",
|
||||
"path": "src/contracts/errors.ts",
|
||||
"exportName": "ERROR_REGISTRY",
|
||||
"owner": "feature-frontend-error-classification-boundary-contract",
|
||||
"keyField": "kind",
|
||||
"requiredFields": [
|
||||
"kind",
|
||||
"defaultRetryable",
|
||||
"severity",
|
||||
"userMessageKey",
|
||||
"action",
|
||||
"telemetryEvent",
|
||||
"redaction"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"kind": "string",
|
||||
"defaultRetryable": "boolean",
|
||||
"severity": "string",
|
||||
"userMessageKey": "string",
|
||||
"action": "string",
|
||||
"telemetryEvent": "string",
|
||||
"redaction": "array"
|
||||
},
|
||||
"uniqueFields": ["kind"],
|
||||
"allowedValues": {
|
||||
"severity": ["info", "warning", "error"],
|
||||
"action": [
|
||||
"retry",
|
||||
"reauth",
|
||||
"navigate",
|
||||
"reload-once",
|
||||
"contact-support",
|
||||
"none"
|
||||
]
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"path": "src/adapters/http/client.ts",
|
||||
"token": "failure("
|
||||
}
|
||||
],
|
||||
"breakingFields": ["kind", "userMessageKey", "action", "telemetryEvent"]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-QUERY-INVALIDATION",
|
||||
"path": "src/features/installed-feature-contracts.ts",
|
||||
"exportName": "INVALIDATION_REGISTRY",
|
||||
"rowsPath": "edges",
|
||||
"rowKeyFields": [
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
],
|
||||
"owner": "feature-frontend-server-state-caching-contract",
|
||||
"requiredFields": [
|
||||
"topicId",
|
||||
"namespace.namespaceId",
|
||||
"namespace.namespaceVersion"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"topicId": "string",
|
||||
"namespace.namespaceId": "string",
|
||||
"namespace.namespaceVersion": "integer"
|
||||
},
|
||||
"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/bootstrap/runtime-adapters.ts",
|
||||
"token": "indexInvalidationRegistry(INVALIDATION_REGISTRY)"
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"path": "src/contracts/telemetry.ts",
|
||||
"exportName": "TELEMETRY_REGISTRY",
|
||||
"owner": "feature-frontend-diagnostics-telemetry-runtime",
|
||||
"keyField": "eventName",
|
||||
"requiredFields": [
|
||||
"eventName",
|
||||
"trigger",
|
||||
"requiredAttributes",
|
||||
"optionalAttributes",
|
||||
"forbiddenAttributes",
|
||||
"sampling",
|
||||
"delivery"
|
||||
],
|
||||
"fieldTypes": {
|
||||
"eventName": "string",
|
||||
"trigger": "string",
|
||||
"requiredAttributes": "array",
|
||||
"optionalAttributes": "array",
|
||||
"forbiddenAttributes": "array",
|
||||
"sampling": "string",
|
||||
"delivery": "string"
|
||||
},
|
||||
"uniqueFields": ["eventName"],
|
||||
"allowedValues": {
|
||||
"delivery": ["best-effort"]
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"path": "scripts/check-diagnostics.ts",
|
||||
"token": "TELEMETRY_REGISTRY"
|
||||
}
|
||||
],
|
||||
"breakingFields": [
|
||||
"eventName",
|
||||
"requiredAttributes",
|
||||
"forbiddenAttributes",
|
||||
"delivery"
|
||||
]
|
||||
},
|
||||
{
|
||||
"registryId": "FE-REG-RELEASE",
|
||||
"path": "src/contracts/release-tokens.ts",
|
||||
"exportName": "RELEASE_TOKEN_REGISTRY",
|
||||
"owner": "feature-frontend-release-cache-rollback-contract",
|
||||
"keyField": "token",
|
||||
"requiredFields": ["token", "source", "compatibilityRole"],
|
||||
"fieldTypes": {
|
||||
"token": "string",
|
||||
"source": "string",
|
||||
"compatibilityRole": "string"
|
||||
},
|
||||
"uniqueFields": ["token"],
|
||||
"consumers": [
|
||||
{
|
||||
"path": "src/bootstrap/load-release-manifest.ts",
|
||||
"token": "assetManifestHash"
|
||||
}
|
||||
],
|
||||
"breakingFields": ["token", "source", "compatibilityRole"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"surfaces": {
|
||||
"index": {
|
||||
"path": "/",
|
||||
"cacheControl": "no-cache",
|
||||
"contentTypes": ["text/html"],
|
||||
"securityHeaders": true
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"path": "/config.json",
|
||||
"cacheControl": "no-store",
|
||||
"contentTypes": ["application/json"],
|
||||
"securityHeaders": true
|
||||
},
|
||||
"releaseManifest": {
|
||||
"path": "/release-manifest.json",
|
||||
"cacheControl": "no-store",
|
||||
"contentTypes": ["application/json"],
|
||||
"securityHeaders": true
|
||||
},
|
||||
"hashedAsset": {
|
||||
"pathPattern": "/assets/*",
|
||||
"cacheControl": "public, max-age=31536000, immutable",
|
||||
"contentTypes": ["text/javascript", "application/javascript"],
|
||||
"securityHeaders": false
|
||||
},
|
||||
"sourceMap": {
|
||||
"public": false
|
||||
},
|
||||
"serviceWorker": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"responses": {
|
||||
"index": {
|
||||
"cache-control": "no-cache",
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
||||
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
||||
"x-frame-options": "DENY",
|
||||
"referrer-policy": "strict-origin-when-cross-origin",
|
||||
"x-content-type-options": "nosniff",
|
||||
"permissions-policy": "camera=(), microphone=(), geolocation=()"
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"cache-control": "no-store",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
||||
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
||||
"x-frame-options": "DENY",
|
||||
"referrer-policy": "strict-origin-when-cross-origin",
|
||||
"x-content-type-options": "nosniff",
|
||||
"permissions-policy": "camera=(), microphone=(), geolocation=()"
|
||||
},
|
||||
"releaseManifest": {
|
||||
"cache-control": "no-store",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
||||
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
||||
"x-frame-options": "DENY",
|
||||
"referrer-policy": "strict-origin-when-cross-origin",
|
||||
"x-content-type-options": "nosniff",
|
||||
"permissions-policy": "camera=(), microphone=(), geolocation=()"
|
||||
},
|
||||
"hashedAsset": {
|
||||
"cache-control": "public, max-age=31536000, immutable",
|
||||
"content-type": "text/javascript; charset=utf-8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"headers": {
|
||||
"Content-Security-Policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
||||
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Permissions-Policy": "camera=(), microphone=(), geolocation=()"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"bundle": {
|
||||
"initialJsGzipBytes": 204800,
|
||||
"lazyChunkGzipBytes": 122880
|
||||
},
|
||||
"lab": {
|
||||
"lcpMs": 2500,
|
||||
"cls": 0.1,
|
||||
"namedInteractionMs": 200
|
||||
},
|
||||
"field": {
|
||||
"p75LcpMs": 2500,
|
||||
"p75Cls": 0.1,
|
||||
"p75InpMs": 200,
|
||||
"minimumEligibleSamples": null
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"releaseId": "local-release",
|
||||
"environment": "replace-with-production",
|
||||
"source": {
|
||||
"system": "",
|
||||
"exportId": ""
|
||||
},
|
||||
"privacy": {
|
||||
"approved": false,
|
||||
"approvalRef": ""
|
||||
},
|
||||
"window": {
|
||||
"start": "2026-06-01T00:00:00Z",
|
||||
"end": "2026-06-29T00:00:00Z"
|
||||
},
|
||||
"thresholdDecision": {
|
||||
"status": "pending",
|
||||
"minimumEligibleSamples": null,
|
||||
"owner": "",
|
||||
"reviewedAt": "",
|
||||
"evidenceRef": ""
|
||||
},
|
||||
"samples": []
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
{
|
||||
"$schema": "../../schemas/config/frontend-capability-recipes.schema.json",
|
||||
"schemaVersion": 1,
|
||||
"decisionId": "VD-10",
|
||||
"defaultStatus": "NOT_INSTALLED",
|
||||
"productionRuntimeDependencies": [],
|
||||
"catalogOwner": "frontend-platform",
|
||||
"reviewOn": "project-capability-selection",
|
||||
"vendorPackagePatterns": [
|
||||
"@launchdarkly/*",
|
||||
"@sentry/*",
|
||||
"@opentelemetry/*",
|
||||
"@openapitools/openapi-generator-cli",
|
||||
"@reduxjs/toolkit",
|
||||
"@tanstack/react-virtual",
|
||||
"@uppy/*",
|
||||
"firebase",
|
||||
"idb",
|
||||
"react-window",
|
||||
"redux",
|
||||
"socket.io-client",
|
||||
"tus-js-client",
|
||||
"workbox-window",
|
||||
"xstate",
|
||||
"zustand"
|
||||
],
|
||||
"recipes": [
|
||||
{
|
||||
"id": "realtime",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"referenceRuntime": {
|
||||
"status": "AVAILABLE_NOT_COMPOSED",
|
||||
"coveredCapabilities": [
|
||||
"transport-independent event authority and recovery",
|
||||
"bounded reconnect ownership",
|
||||
"fetch-stream SSE",
|
||||
"bounded polling",
|
||||
"single-writer live and polling handoff",
|
||||
"WebSocket closed protocol",
|
||||
"Web Push window and Service Worker control"
|
||||
],
|
||||
"sourceRoots": [
|
||||
"src/application/ports/realtime",
|
||||
"src/application/ports/out/web-push-control.ts",
|
||||
"src/application/policies/bounded-polling.ts",
|
||||
"src/contracts/realtime-events.ts",
|
||||
"src/contracts/realtime-streams.ts",
|
||||
"src/contracts/web-push.ts",
|
||||
"src/adapters/realtime",
|
||||
"src/adapters/web-push"
|
||||
],
|
||||
"conformanceScripts": [
|
||||
"test:unit",
|
||||
"check:realtime-boundaries",
|
||||
"check:realtime-boundaries:fixture",
|
||||
"check:optional-recipes",
|
||||
"test:realtime-removal"
|
||||
],
|
||||
"productionComposition": false
|
||||
},
|
||||
"trigger": "The backend exposes ordered push events with a documented resume and authorization protocol.",
|
||||
"forbiddenWhen": ["Polling satisfies the measured freshness requirement.", "Event ordering and reconnect ownership are undefined."],
|
||||
"boundary": "transport-independent event authority plus separately owned SSE, WebSocket, bounded polling and Web Push adapters",
|
||||
"port": "RealtimeEventAuthority / WebPushControlPort / transport-specific connection and polling factories",
|
||||
"fake": "Deterministic event authority, transport facade, clock, repository and Service Worker test doubles",
|
||||
"failureKinds": ["abort", "disconnect-or-timeout", "protocol-or-mapping-mismatch", "duplicate-or-stale", "sequence-gap-or-cursor-expiry", "queue-overflow", "scope-fenced", "poll-budget-exhausted", "push-permission-or-subscription-failure"],
|
||||
"lifecycleMethods": ["close-or-dispose", "unsubscribe", "cancel-via-AbortSignal", "bounded-poll-lease", "revoke-push-association"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Validate every event envelope and closed transport frame before application effects.", "Bind stream state to the current opaque scope generation and advance checkpoints only after committed effects or authoritative recovery.", "Use fixed same-origin endpoints and an exact WebSocket subprotocol; never place credentials, cursors, subscription material or scope bindings in URLs or telemetry.", "Treat Web Push as a notification hint, fence registration and revocation with one durable compare-and-swap control record, and allow only registry-owned notification and route intents.", "Keep every queue, parser, reconnect, poll, notification and storage operation bounded and abortable."],
|
||||
"bundleBudgetGzipBytes": 40000,
|
||||
"fallback": "Bounded polling or explicitly stale UI.",
|
||||
"removal": ["Disable admission and close active readers, sockets, poll leases and worker handlers.", "Revoke and purge only the owned Web Push association and notification state.", "Remove composition, registries, adapters and any selected vendor dependency.", "Run realtime boundary, runtime-removal and production-bundle gates."],
|
||||
"serverStatePolicy": "query-cache-owned"
|
||||
},
|
||||
{
|
||||
"id": "offline-indexeddb",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"referenceRuntime": {
|
||||
"status": "AVAILABLE_NOT_COMPOSED",
|
||||
"coveredCapabilities": [
|
||||
"IndexedDB",
|
||||
"OPFS",
|
||||
"StorageManager estimate/persistence"
|
||||
],
|
||||
"sourceRoots": [
|
||||
"src/application/ports/browser-file-storage/indexeddb-port.ts",
|
||||
"src/application/ports/browser-file-storage/opfs-ports.ts",
|
||||
"src/application/ports/browser-file-storage/storage-durability-port.ts",
|
||||
"src/adapters/browser-file-storage",
|
||||
"src/adapters/storage/indexeddb",
|
||||
"src/adapters/storage/opfs"
|
||||
],
|
||||
"conformanceScripts": [
|
||||
"test:unit",
|
||||
"test:browser-capabilities",
|
||||
"verify:browser-capability-evidence",
|
||||
"check:browser-file-storage-boundaries",
|
||||
"check:optional-recipes",
|
||||
"test:browser-file-storage-removal"
|
||||
],
|
||||
"productionComposition": false
|
||||
},
|
||||
"trigger": "A product requirement needs indexed offline records, an unsynced command queue, or a large local binary sidecar beyond small public preferences.",
|
||||
"forbiddenWhen": ["The data contains credentials.", "The browser would connect directly to a server database or object store.", "A normal HTTP cache is sufficient.", "Partition, retention, quota and recovery ownership are undefined."],
|
||||
"boundary": "feature-specific async repository with registry-issued opaque dataset scope and immutable full-policy binding, plus an OPFS large-object sidecar whose logical commit authority and bidirectional scope binding are owned by an IndexedDB journal",
|
||||
"port": "IndexedDbRepositoryPort / IndexedDbMaintenancePort / DurableObjectStorePort / DurableObjectMaintenancePort / StorageDurabilityPort",
|
||||
"fake": "MemoryStructuredOfflineStore / MemoryDurableObjectStore / MemoryStorageDurabilityAdapter",
|
||||
"failureKinds": ["open-blocked", "versionchange", "quota", "corruption", "migration-rollback", "revision-conflict", "storage-eviction", "partial-object-write", "dataset-binding-mismatch", "dataset-budget-exceeded", "lifecycle-authorization-denied", "expired-resource"],
|
||||
"lifecycleMethods": ["close", "cancel-via-AbortSignal", "enforce-bounded-lifecycle-batch", "prune-expired-receipts", "reconcile", "enforcePolicies-with-composition-authority"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Classify every persisted field and binary namespace.", "Encrypting in the same client is not a credential protection boundary.", "Keep database schema and record codec versions separate.", "Derive the physical IndexedDB name only from registry-issued authority, namespace and partition tokens; readable namespace, business and account IDs are forbidden.", "Persist and revalidate an immutable scope plus full BrowserStoragePolicy binding during upgrade, post-open and maintenance; missing or mismatched existing bindings fail closed.", "Use the actual IndexedDB wire split: StoredRecord contains only key, codecVersion, revision and payload; writtenAtEpochMs, synchronization, measuredBytes and eligibleAtEpochMs belong to the retention sidecar, while idempotency receipts and governance binding/budget use separate stores.", "Include every store registered in lifecycleMetadataStores in bounded full-partition purge while retaining immutable governance identity.", "Measure conservative logical bytes through the codec and atomically enforce dataset usedBytes plus receiptCount with record, lifecycle and migration writes.", "Enforce TTL before sweep visibility, delete UNTIL_SYNCED only after explicit confirmation, and require a composition-authorized short-lived proof for every deleting IndexedDB lifecycle batch.", "Bound idempotency receipt retention to 31 days and configured count to the implementation ceiling of 1000000; bound migration to old-writer-drained batches no larger than 500 rows or 30000ms.", "Bind OPFS readable and physical scopes in both directions and use only /ca-frontend-opfs-v1/authorities/<authorityToken>/<namespaceToken>/<partitionToken>/ for physical dataset layout.", "For OPFS LOGOUT, UNTIL_SYNCED and ACCOUNT_DELETION maintenance, composition must provide both requestMaintenanceAuthority and consumeMaintenanceAuthority; issue a fresh proof bound to the exact frozen reason, scope and policy for at most five minutes, then atomically consume it to reject replay.", "Never expose an OPFS authority proof through the application request, persistence, diagnostics or telemetry.", "Never place user file names or identifiers in OPFS paths or diagnostics."],
|
||||
"bundleBudgetGzipBytes": 36000,
|
||||
"fallback": "Read-only or online-only query path; OPFS may degrade to a size-capped IndexedDB Blob only when the product policy approves it.",
|
||||
"removal": ["Stop writes and background migration.", "Reconcile or export unsynced data, then purge only governance-bound owned partitions and OPFS namespaces through authorized bounded lifecycle operations.", "Close all database, channel, worker and file handles.", "Remove repository composition and dependency."],
|
||||
"serverStatePolicy": "reference-or-command-only"
|
||||
},
|
||||
{
|
||||
"id": "service-worker-pwa",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"referenceRuntime": {
|
||||
"status": "AVAILABLE_NOT_COMPOSED",
|
||||
"coveredCapabilities": [
|
||||
"Cache Storage public-response administration"
|
||||
],
|
||||
"sourceRoots": [
|
||||
"src/application/ports/browser-file-storage/cache-storage-ports.ts",
|
||||
"src/adapters/browser-file-storage",
|
||||
"src/adapters/cache-storage"
|
||||
],
|
||||
"conformanceScripts": [
|
||||
"test:unit",
|
||||
"test:browser-capabilities",
|
||||
"verify:browser-capability-evidence",
|
||||
"check:browser-file-storage-boundaries",
|
||||
"check:optional-recipes",
|
||||
"test:browser-file-storage-removal"
|
||||
],
|
||||
"productionComposition": false
|
||||
},
|
||||
"trigger": "Installability, a measured offline-shell requirement, or an explicitly owned public HTTP representation cache is approved.",
|
||||
"forbiddenWhen": ["Hosting cache and worker cache ownership conflict.", "Update and rollback UX is undefined.", "Authenticated, private, opaque or personal responses would be cached.", "Cache freshness, byte and entry limits are undefined."],
|
||||
"boundary": "bootstrap update controller plus platform-local public Request/Response cache administration",
|
||||
"port": "ServiceWorkerUpdatePort / PublicResponseCacheAdmin recipe / PublicResponseCachePort / PublicResponseCacheAdminPort reference runtime",
|
||||
"fake": "FakeServiceWorkerUpdateAdapter / MemoryPublicResponseCache",
|
||||
"failureKinds": ["stale-worker", "update-loop", "offline-fallback", "incomplete-candidate", "integrity-mismatch", "cache-policy-rejection", "quota"],
|
||||
"lifecycleMethods": ["unregister", "rollback", "delete-owned-caches"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Cache only explicit same-origin public GET representations.", "Never cache authenticated, cookie-dependent, private, no-store, opaque or personal responses.", "Bind candidate cache names to release identity and a canonical manifest digest that includes normalized expectedContentType, exact request identity, expected byte length and integrity digest.", "Reject a response whose normalized Content-Type differs from manifest expectedContentType even when body integrity matches.", "Derive cleanup retention only from the verified active pointer and composition retainedPreviousReleaseCount; cleanup callers cannot submit cache names, release registry IDs or any retain set.", "Read active-pointer and release-marker control JSON through a strict UTF-8 stream capped at exactly 2 MiB (2097152 bytes), cancel on overflow and fail closed before parsing oversized metadata.", "Keep exact query and Vary semantics; ignoreSearch and ignoreVary are forbidden.", "Fail closed on malformed update metadata or integrity mismatch."],
|
||||
"bundleBudgetGzipBytes": 10000,
|
||||
"fallback": "Normal network application with hosting cache headers.",
|
||||
"removal": ["Deploy an unregister migration.", "Delete only parsed, owned cache namespaces after old controlled clients drain.", "Remove worker registration, cache metadata and manifest."],
|
||||
"serverStatePolicy": "network-cache-policy-only"
|
||||
},
|
||||
{
|
||||
"id": "file-transfer",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"referenceRuntime": {
|
||||
"status": "AVAILABLE_NOT_COMPOSED",
|
||||
"coveredCapabilities": [
|
||||
"File",
|
||||
"Blob",
|
||||
"native file input",
|
||||
"system file picker",
|
||||
"object URL preview",
|
||||
"download delivery",
|
||||
"presigned URL capability",
|
||||
"bounded streaming download",
|
||||
"multipart/resumable upload",
|
||||
"durable non-secret upload checkpoint",
|
||||
"Image CDN responsive delivery"
|
||||
],
|
||||
"sourceRoots": [
|
||||
"src/application/ports/browser-file-storage/file.ts",
|
||||
"src/application/ports/browser-transfer",
|
||||
"src/adapters/browser-file-storage",
|
||||
"src/adapters/browser-files",
|
||||
"src/adapters/browser-transfer"
|
||||
],
|
||||
"conformanceScripts": [
|
||||
"test:unit",
|
||||
"test:browser-capabilities",
|
||||
"verify:browser-capability-evidence",
|
||||
"check:browser-file-storage-boundaries",
|
||||
"check:optional-recipes",
|
||||
"test:browser-file-storage-removal"
|
||||
],
|
||||
"productionComposition": false
|
||||
},
|
||||
"trigger": "The product selects, inspects, previews, uploads, downloads or delivers image renditions with bounded memory, resumability, cancellation, expiry and integrity requirements.",
|
||||
"forbiddenWhen": ["Allowed count, byte, extension, MIME and content-signature policy is missing.", "The BFF does not own authorization, short-lived capability issuance, upload session reconciliation, quarantine and orphan cleanup.", "Long-lived credentials, presigned URLs or signed headers would enter persistence, application state or telemetry.", "Native File, Blob, object URL or file-system handles would cross into domain state or persistence.", "Large downloads would be returned as one in-memory byte array or Blob.", "Image callers could submit arbitrary CDN source URLs or transform parameters."],
|
||||
"boundary": "BFF-owned transfer control plane plus adapter-owned browser/object-storage data plane; presentation receives opaque file/image references, registered policies and bounded result streams only",
|
||||
"port": "FilePickerPort / FileContentPort / TransientPreviewPort / DownloadDeliveryPort / PresignedDownloadSourcePort / PresignedUploadPartPort / ResumableUploadPort / ImageCdnPresentationPort",
|
||||
"fake": "Memory file/preview/download adapters plus injected deterministic capability, upload-control-plane, part-executor and image-verifier test doubles",
|
||||
"failureKinds": ["dismissed", "permission-denied", "count-or-size-rejection", "type-or-signature-rejection", "file-changed", "abort", "integrity-failure", "partial-save", "expired-or-revoked-capability", "part-or-session-conflict", "checkpoint-conflict", "quarantined", "image-policy-rejection"],
|
||||
"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": 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"
|
||||
},
|
||||
{
|
||||
"id": "generated-api",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "A versioned backend contract justifies generated transport code.",
|
||||
"forbiddenWhen": ["Generated DTOs would escape into domain or presentation.", "Contract drift cannot block CI."],
|
||||
"boundary": "generated client wrapped by a feature gateway facade and mapper",
|
||||
"port": "GeneratedApiFacade",
|
||||
"fake": "FakeGeneratedApiAdapter",
|
||||
"failureKinds": ["contract-drift", "unsupported-field"],
|
||||
"lifecycleMethods": ["cancel-via-AbortSignal"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Generate from an authenticated source.", "Review generator execution and output.", "Do not log request bodies."],
|
||||
"bundleBudgetGzipBytes": 16000,
|
||||
"fallback": "Existing typed request builder and runtime response schema.",
|
||||
"removal": ["Restore handwritten gateway.", "Remove generated output and generator.", "Verify DTOs do not remain in public types."],
|
||||
"serverStatePolicy": "query-cache-owned"
|
||||
},
|
||||
{
|
||||
"id": "feature-flag",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "A staged rollout or kill switch has a named owner, default and stale policy.",
|
||||
"forbiddenWhen": ["A flag is used as authorization.", "Unknown and unavailable behavior is undefined."],
|
||||
"boundary": "application feature policy output port",
|
||||
"port": "FeatureFlagPort",
|
||||
"fake": "FakeFeatureFlagAdapter",
|
||||
"failureKinds": ["provider-unavailable", "unknown-flag", "stale-value"],
|
||||
"lifecycleMethods": ["dispose-provider-if-installed"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Flags are hints, never access control.", "Minimize targeting attributes.", "Apply consent rules to personal attributes."],
|
||||
"bundleBudgetGzipBytes": 10000,
|
||||
"fallback": "Typed local default with an explicit stale decision.",
|
||||
"removal": ["Resolve the rollout permanently.", "Delete flag key and branches.", "Remove provider composition and dependency."],
|
||||
"serverStatePolicy": "policy-cache-only"
|
||||
},
|
||||
{
|
||||
"id": "web-worker",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "Profiling shows CPU work blocking the main thread beyond the performance budget.",
|
||||
"forbiddenWhen": ["The task is primarily network I/O.", "Cancellation and stale-result ownership are undefined."],
|
||||
"boundary": "request/result/cancel output port with a validated message adapter",
|
||||
"port": "WorkerTaskPort",
|
||||
"fake": "FakeWorkerTaskAdapter",
|
||||
"failureKinds": ["crash", "stale-result", "transfer-failure"],
|
||||
"lifecycleMethods": ["cancel", "dispose"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Validate worker messages.", "Do not send credentials.", "Bound transferred data and worker count."],
|
||||
"bundleBudgetGzipBytes": 14000,
|
||||
"fallback": "Chunked or deferred main-thread execution within a measured limit.",
|
||||
"removal": ["Stop and dispose workers.", "Restore synchronous facade implementation.", "Remove worker entry and chunk."],
|
||||
"serverStatePolicy": "no-server-state"
|
||||
},
|
||||
{
|
||||
"id": "multi-tab",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "A documented workflow must synchronize non-sensitive events across tabs.",
|
||||
"forbiddenWhen": ["The server is the correct conflict authority.", "Event version and source identity are undefined."],
|
||||
"boundary": "versioned browser event output/input adapter",
|
||||
"port": "MultiTabPort",
|
||||
"fake": "FakeMultiTabAdapter",
|
||||
"failureKinds": ["self-echo", "duplicate", "conflict"],
|
||||
"lifecycleMethods": ["unsubscribe", "close"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Broadcast no credentials or personal payload.", "Validate versions.", "Treat events as hints rather than authorization."],
|
||||
"bundleBudgetGzipBytes": 4000,
|
||||
"fallback": "Refresh from the authoritative server on focus.",
|
||||
"removal": ["Close channels.", "Remove event registry entries.", "Restore focus-based refresh."],
|
||||
"serverStatePolicy": "invalidation-only"
|
||||
},
|
||||
{
|
||||
"id": "browser-permission",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "A user-initiated flow requires clipboard, notification or media access.",
|
||||
"forbiddenWhen": ["Permission would be requested at boot.", "Denied, dismissed and unsupported UX are not designed."],
|
||||
"boundary": "presentation input action through a browser capability output port",
|
||||
"port": "BrowserPermissionPort",
|
||||
"fake": "FakeBrowserPermissionAdapter",
|
||||
"failureKinds": ["denied", "dismissed", "unsupported"],
|
||||
"lifecycleMethods": ["stop-media-tracks-if-opened"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Require an explicit user gesture.", "Minimize requested scope.", "Do not persist permission as authorization."],
|
||||
"bundleBudgetGzipBytes": 3000,
|
||||
"fallback": "Manual input or copy/download instruction.",
|
||||
"removal": ["Stop acquired resources.", "Remove permission action and adapter.", "Retest denied-path accessibility."],
|
||||
"serverStatePolicy": "no-server-state"
|
||||
},
|
||||
{
|
||||
"id": "client-workflow",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "A measured cross-page client-only workflow cannot be represented by URL, local state, context or query cache.",
|
||||
"forbiddenWhen": ["The store would duplicate server response collections.", "A library is selected before state ownership is documented.", "Zustand and Redux Toolkit would both be installed."],
|
||||
"boundary": "workflow-specific local facade; vendor types remain in its adapter",
|
||||
"port": "ClientWorkflowPort",
|
||||
"fake": "FakeClientWorkflowAdapter",
|
||||
"failureKinds": ["reset", "version-mismatch", "server-state-duplication"],
|
||||
"lifecycleMethods": ["unsubscribe", "reset"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Persist only explicitly classified workflow fields.", "Never persist credentials.", "Define logout and version reset."],
|
||||
"bundleBudgetGzipBytes": 9000,
|
||||
"fallback": "URL, component state, context and TanStack Query ownership.",
|
||||
"removal": ["Move remaining state to its natural owner.", "Remove facade and one selected store dependency.", "Verify logout/reset."],
|
||||
"serverStatePolicy": "reference-only"
|
||||
},
|
||||
{
|
||||
"id": "large-data-ui",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "Production-like profiling proves a list or grid exceeds interaction and rendering budgets.",
|
||||
"forbiddenWhen": ["Pagination solves the scale requirement.", "Keyboard and screen-reader focus behavior is undefined."],
|
||||
"boundary": "presentation facade around virtualizer or data-grid behavior",
|
||||
"port": "LargeDataUiFacade",
|
||||
"fake": "FakeLargeDataUiAdapter",
|
||||
"failureKinds": ["focus-loss", "stale-row", "scale-limit"],
|
||||
"lifecycleMethods": ["dispose-observers-if-installed"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Render only authorized rows.", "Do not expose hidden row data to telemetry.", "Preserve accessible row identity."],
|
||||
"bundleBudgetGzipBytes": 30000,
|
||||
"fallback": "Accessible pagination and bounded result sets.",
|
||||
"removal": ["Restore paginated primitive.", "Remove facade adapter and dependency.", "Run keyboard and performance evidence."],
|
||||
"serverStatePolicy": "query-cache-owned"
|
||||
},
|
||||
{
|
||||
"id": "analytics-error-sink",
|
||||
"status": "RECIPE_AVAILABLE",
|
||||
"trigger": "A production provider, consent policy, retention owner and event registry are approved.",
|
||||
"forbiddenWhen": ["Consent and essential diagnostics are not separated.", "Arbitrary message or attribute keys can bypass redaction."],
|
||||
"boundary": "closed diagnostics/analytics port with provider adapter",
|
||||
"port": "AnalyticsErrorSink",
|
||||
"fake": "RecordingAnalyticsAdapter",
|
||||
"failureKinds": ["consent-denied", "queue-full", "provider-unavailable"],
|
||||
"lifecycleMethods": ["flush", "dispose"],
|
||||
"owner": "project-owner-required",
|
||||
"securityPrivacy": ["Allowlist events and attributes.", "Redact before queueing.", "Apply consent, sampling and retention policy."],
|
||||
"bundleBudgetGzipBytes": 25000,
|
||||
"fallback": "Existing bounded local diagnostics and best-effort telemetry port.",
|
||||
"removal": ["Disable provider delivery.", "Flush or discard by policy.", "Remove adapter, runtime config and dependency."],
|
||||
"serverStatePolicy": "no-server-state"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"fixtures": [
|
||||
{
|
||||
"name": "coherent-release",
|
||||
"expectedCompatible": true,
|
||||
"frontend": {
|
||||
"buildId": "build-a",
|
||||
"configSchemaVersion": "1.0",
|
||||
"apiContractVersion": "1.0",
|
||||
"assetManifestHash": "assets-a",
|
||||
"releaseId": "release-a"
|
||||
},
|
||||
"runtime": {
|
||||
"buildId": "build-a",
|
||||
"configSchemaVersion": "1.1",
|
||||
"apiContractVersion": "1.2",
|
||||
"assetManifestHash": "assets-a",
|
||||
"releaseId": "release-a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mixed-html-and-assets",
|
||||
"expectedCompatible": false,
|
||||
"frontend": {
|
||||
"buildId": "build-a",
|
||||
"configSchemaVersion": "1.0",
|
||||
"apiContractVersion": "1.0",
|
||||
"assetManifestHash": "assets-a",
|
||||
"releaseId": "release-a"
|
||||
},
|
||||
"runtime": {
|
||||
"buildId": "build-b",
|
||||
"configSchemaVersion": "1.0",
|
||||
"apiContractVersion": "1.0",
|
||||
"assetManifestHash": "assets-b",
|
||||
"releaseId": "release-b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "incompatible-runtime-config",
|
||||
"expectedCompatible": false,
|
||||
"frontend": {
|
||||
"buildId": "build-a",
|
||||
"configSchemaVersion": "1.0",
|
||||
"apiContractVersion": "1.0",
|
||||
"assetManifestHash": "assets-a",
|
||||
"releaseId": "release-a"
|
||||
},
|
||||
"runtime": {
|
||||
"buildId": "build-a",
|
||||
"configSchemaVersion": "2.0",
|
||||
"apiContractVersion": "1.0",
|
||||
"assetManifestHash": "assets-a",
|
||||
"releaseId": "release-a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "incompatible-api-contract",
|
||||
"expectedCompatible": false,
|
||||
"frontend": {
|
||||
"buildId": "build-a",
|
||||
"configSchemaVersion": "1.0",
|
||||
"apiContractVersion": "1.0",
|
||||
"assetManifestHash": "assets-a",
|
||||
"releaseId": "release-a"
|
||||
},
|
||||
"runtime": {
|
||||
"buildId": "build-a",
|
||||
"configSchemaVersion": "1.0",
|
||||
"apiContractVersion": "2.0",
|
||||
"assetManifestHash": "assets-a",
|
||||
"releaseId": "release-a"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"runbooks": {
|
||||
"FE-RB-001": {
|
||||
"title": "Boot configuration failure",
|
||||
"gateId": "FE-GATE-021",
|
||||
"triggerKinds": ["BOOT_CONFIG_FAILURE"],
|
||||
"containment": "stop product route mount, show the safe support shell, and refetch at most once",
|
||||
"window": "owner triage planned-default 5m",
|
||||
"escalation": ["env-config owner", "release owner"],
|
||||
"recoveryEvidence": [
|
||||
"clean-session boot",
|
||||
"product root mount",
|
||||
"config validation",
|
||||
"no repeated boot error"
|
||||
],
|
||||
"negativeFixture": "a valid config followed by an injected mount failure must fail recovery"
|
||||
},
|
||||
"FE-RB-002": {
|
||||
"title": "Chunk, manifest, or deployment mismatch",
|
||||
"gateId": "FE-GATE-022",
|
||||
"triggerKinds": [
|
||||
"CHUNK_LOAD_FAILURE",
|
||||
"RELEASE_MANIFEST_FAILURE",
|
||||
"DEPLOY_MISMATCH"
|
||||
],
|
||||
"containment": "warn for dirty state, fetch manifest no-store once, and allow one guarded reload",
|
||||
"window": "release owner triage planned-default 5m",
|
||||
"escalation": ["release-cache owner", "hosting/CDN owner"],
|
||||
"recoveryEvidence": [
|
||||
"entry and lazy assets reachable",
|
||||
"release tuple coherent",
|
||||
"second reload blocked",
|
||||
"critical route smoke"
|
||||
],
|
||||
"negativeFixture": "a second failure for the same release pair must not reload"
|
||||
},
|
||||
"FE-RB-003": {
|
||||
"title": "Backend API degradation",
|
||||
"gateId": "FE-GATE-023",
|
||||
"triggerKinds": [
|
||||
"TERMINAL_NETWORK_RATE",
|
||||
"REQUEST_TIMEOUT_RATE",
|
||||
"SERVER_FAILURE_RATE",
|
||||
"SCHEMA_MISMATCH"
|
||||
],
|
||||
"containment": "do not expand retry caps, serve safe stale reads, and never retry an unkeyed mutation",
|
||||
"window": "rolling 5m trigger; first classification planned-default 10m",
|
||||
"escalation": [
|
||||
"api-client owner",
|
||||
"backend operation owner",
|
||||
"release compatibility owner"
|
||||
],
|
||||
"recoveryEvidence": [
|
||||
"terminal failure rate at baseline",
|
||||
"no retry amplification",
|
||||
"critical read/write smoke",
|
||||
"schema fixtures"
|
||||
],
|
||||
"negativeFixture": "an unkeyed POST receiving 503 must not retry"
|
||||
},
|
||||
"FE-RB-004": {
|
||||
"title": "Telemetry sink failure",
|
||||
"gateId": "FE-GATE-024",
|
||||
"triggerKinds": ["TELEMETRY_FAILURE"],
|
||||
"containment": "keep product flow available, bound the queue, and never report recursively to the failing sink",
|
||||
"window": "platform triage planned-default 15m",
|
||||
"escalation": ["observability owner", "telemetry platform owner"],
|
||||
"recoveryEvidence": [
|
||||
"product flow unaffected",
|
||||
"delivery self-check",
|
||||
"queue drained within bound",
|
||||
"forbidden attributes absent"
|
||||
],
|
||||
"negativeFixture": "raw URL and query data must be removed from telemetry"
|
||||
},
|
||||
"FE-RB-005": {
|
||||
"title": "Coherent release rollback",
|
||||
"gateId": "FE-GATE-025",
|
||||
"triggerKinds": ["RELEASE_BLOCKING_DEFECT"],
|
||||
"containment": "select a prior immutable tuple, verify asset/config/API compatibility, atomically switch, and smoke",
|
||||
"window": "provider recovery target TBD",
|
||||
"escalation": ["release-cache owner", "release approver/hosting owner"],
|
||||
"recoveryEvidence": [
|
||||
"compatibility gate",
|
||||
"release coherence gate",
|
||||
"critical smoke",
|
||||
"release ID in incident timeline"
|
||||
],
|
||||
"negativeFixture": "HTML build A with asset manifest B must be rejected"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"APP_ENV": "development",
|
||||
"API_BASE_URL": "https://api.dev.example.com/",
|
||||
"REQUEST_TIMEOUT_MS": 15000,
|
||||
"MAX_RETRY_ATTEMPTS": 2,
|
||||
"TELEMETRY_ENABLED": false,
|
||||
"AUTH_MODE": "external",
|
||||
"CONFIG_SCHEMA_VERSION": "2.0",
|
||||
"RELEASE_MANIFEST_URL": "/release-manifest.json",
|
||||
"CAPABILITY_OVERRIDES": {
|
||||
"REALTIME": "DEFAULT",
|
||||
"WEB_WORKER": "DEFAULT",
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "HTTP",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"APP_ENV": "local",
|
||||
"API_BASE_URL": "http://localhost:8080/",
|
||||
"REQUEST_TIMEOUT_MS": 10000,
|
||||
"MAX_RETRY_ATTEMPTS": 2,
|
||||
"TELEMETRY_ENABLED": false,
|
||||
"AUTH_MODE": "demo",
|
||||
"CONFIG_SCHEMA_VERSION": "2.0",
|
||||
"RELEASE_MANIFEST_URL": "/release-manifest.json",
|
||||
"CAPABILITY_OVERRIDES": {
|
||||
"REALTIME": "DEFAULT",
|
||||
"WEB_WORKER": "DEFAULT",
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "MOCK",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "MOCK",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"APP_ENV": "production",
|
||||
"API_BASE_URL": "https://api.example.com/",
|
||||
"REQUEST_TIMEOUT_MS": 10000,
|
||||
"MAX_RETRY_ATTEMPTS": 2,
|
||||
"TELEMETRY_ENABLED": true,
|
||||
"TELEMETRY_ENDPOINT": "https://telemetry.example.com/v1/events",
|
||||
"AUTH_MODE": "external",
|
||||
"CONFIG_SCHEMA_VERSION": "2.0",
|
||||
"RELEASE_MANIFEST_URL": "/release-manifest.json",
|
||||
"CAPABILITY_OVERRIDES": {
|
||||
"REALTIME": "DEFAULT",
|
||||
"WEB_WORKER": "DEFAULT",
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "HTTP",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"APP_ENV": "staging",
|
||||
"API_BASE_URL": "https://api.staging.example.com/",
|
||||
"REQUEST_TIMEOUT_MS": 10000,
|
||||
"MAX_RETRY_ATTEMPTS": 2,
|
||||
"TELEMETRY_ENABLED": true,
|
||||
"TELEMETRY_ENDPOINT": "https://telemetry.staging.example.com/v1/events",
|
||||
"AUTH_MODE": "external",
|
||||
"CONFIG_SCHEMA_VERSION": "2.0",
|
||||
"RELEASE_MANIFEST_URL": "/release-manifest.json",
|
||||
"CAPABILITY_OVERRIDES": {
|
||||
"REALTIME": "DEFAULT",
|
||||
"WEB_WORKER": "DEFAULT",
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "HTTP",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "ART-FE-FIELD-WEB-VITALS@1",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"generatedAt",
|
||||
"window",
|
||||
"context",
|
||||
"metrics",
|
||||
"thresholds",
|
||||
"eligibility",
|
||||
"status",
|
||||
"passed"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 1 },
|
||||
"generatedAt": { "type": "string", "format": "date-time" },
|
||||
"window": { "type": "object", "required": ["days", "start", "end"] },
|
||||
"context": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source",
|
||||
"sourceSystem",
|
||||
"exportId",
|
||||
"network",
|
||||
"routeAggregation",
|
||||
"releaseId",
|
||||
"privacyApprovalRef",
|
||||
"thresholdDecisionRef",
|
||||
"validationFailures"
|
||||
],
|
||||
"properties": {
|
||||
"source": { "type": "string" },
|
||||
"sourceSystem": { "type": ["string", "null"] },
|
||||
"exportId": { "type": ["string", "null"] },
|
||||
"network": { "const": "production-real-user" },
|
||||
"routeAggregation": { "const": "route-id-only" },
|
||||
"releaseId": { "type": ["string", "null"] },
|
||||
"privacyApprovalRef": { "type": ["string", "null"] },
|
||||
"thresholdDecisionRef": { "type": ["string", "null"] },
|
||||
"validationFailures": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"thresholds": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"p75LcpMs",
|
||||
"p75Cls",
|
||||
"p75InpMs",
|
||||
"minimumEligibleSamples"
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"required": ["p75LcpMs", "p75Cls", "p75InpMs"]
|
||||
},
|
||||
"eligibility": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"consentRequired",
|
||||
"totalSamples",
|
||||
"eligibleSamples",
|
||||
"minimumEligibleSamples",
|
||||
"routeSamples"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"enum": ["PASS", "FAIL_THRESHOLD", "FAIL_UNVERIFIED"]
|
||||
},
|
||||
"passed": { "type": "boolean" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "ART-FE-LAB@1",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"generatedAt",
|
||||
"context",
|
||||
"metrics",
|
||||
"thresholds",
|
||||
"fixtures",
|
||||
"passed"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 1 },
|
||||
"generatedAt": { "type": "string", "format": "date-time" },
|
||||
"context": {
|
||||
"type": "object",
|
||||
"required": ["runner", "browser", "viewport", "network", "cpu", "cache", "build"]
|
||||
},
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"required": ["lcpMs", "cls", "namedInteractionMs"]
|
||||
},
|
||||
"thresholds": { "type": "object" },
|
||||
"fixtures": { "type": "array", "minItems": 2 },
|
||||
"passed": { "type": "boolean" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "ART-FE-003@1",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "generatedAt", "artifact", "fixtures", "passed"],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 1 },
|
||||
"generatedAt": { "type": "string", "format": "date-time" },
|
||||
"artifact": {
|
||||
"type": "object",
|
||||
"required": ["checked", "compatible", "mismatches"]
|
||||
},
|
||||
"fixtures": { "type": "array", "minItems": 2 },
|
||||
"passed": { "type": "boolean" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "ART-FE-RUNBOOK-DRILL@1",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"runbookId",
|
||||
"releaseId",
|
||||
"drillTimestamp",
|
||||
"triggerInjected",
|
||||
"triggerAsserted",
|
||||
"containmentAsserted",
|
||||
"escalationPathAsserted",
|
||||
"recoveryAssertions",
|
||||
"negativeFixtureFailedAsExpected",
|
||||
"windowObservedBucket",
|
||||
"passed"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 1 },
|
||||
"runbookId": { "pattern": "^FE-RB-00[1-5]$" },
|
||||
"releaseId": { "type": "string", "minLength": 1 },
|
||||
"drillTimestamp": { "type": "string", "format": "date-time" },
|
||||
"triggerInjected": { "type": "string" },
|
||||
"triggerAsserted": { "type": "boolean" },
|
||||
"containmentAsserted": { "type": "boolean" },
|
||||
"escalationPathAsserted": { "type": "boolean" },
|
||||
"recoveryAssertions": {
|
||||
"type": "array",
|
||||
"minItems": 4,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["assertion", "evidence", "passed"]
|
||||
}
|
||||
},
|
||||
"negativeFixtureFailedAsExpected": { "type": "boolean" },
|
||||
"windowObservedBucket": { "type": "string" },
|
||||
"providerVerificationRequired": { "type": "boolean" },
|
||||
"passed": { "type": "boolean" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"snapshotDigest": "ce4fa9b7944f27553067228bd6c9e73e7dc05875c283255b50d7eb3ad2923f6d",
|
||||
"owner": "frontend-platform",
|
||||
"reason": "RP-11-initial-transitive-inventory",
|
||||
"approvedAt": "2026-07-26T08:27:17.874Z"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"changes": [
|
||||
{
|
||||
"changeId": "add:@fontsource/ibm-plex-mono@5.3.0",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Preserve the source application's IBM Plex Mono typography and bundled font assets without a runtime font request.",
|
||||
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
|
||||
},
|
||||
{
|
||||
"changeId": "add:pretendard@1.3.9",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Preserve the source application's Pretendard Variable typography using its pinned bundled font asset.",
|
||||
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
|
||||
},
|
||||
{
|
||||
"changeId": "add:remark-directive@4.0.0",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Parse the source TechLog document directive syntax through the migrated deterministic content pipeline.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
},
|
||||
{
|
||||
"changeId": "add:remark-gfm@4.0.1",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Preserve source TechLog GitHub-flavored Markdown tables, task lists, and autolink parsing.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
},
|
||||
{
|
||||
"changeId": "add:remark-parse@11.0.0",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Parse the source TechLog Markdown records into the migrated typed public-render model.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
},
|
||||
{
|
||||
"changeId": "add:unified@11.0.5",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Compose the source-equivalent Markdown and directive parsing stages without framework coupling.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"allowedLicenses": [
|
||||
"(MIT OR CC0-1.0)",
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BlueOak-1.0.0",
|
||||
"CC-BY-4.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0",
|
||||
"OFL-1.1"
|
||||
],
|
||||
"deniedLicensePatterns": [
|
||||
"(^|\\s)AGPL",
|
||||
"(^|\\s)GPL",
|
||||
"SSPL",
|
||||
"BUSL"
|
||||
],
|
||||
"unknownLicensePolicy": "allow-only-unmaterialized-platform-optional"
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"trackedRoots": [
|
||||
"src",
|
||||
"recipes",
|
||||
"scripts",
|
||||
"tests",
|
||||
"config",
|
||||
"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.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"
|
||||
],
|
||||
"allowlist": [
|
||||
{
|
||||
"path": "tests/fixtures/security/secret-detection/allowed/test-credentials.ts",
|
||||
"ruleId": "assigned-secret",
|
||||
"owner": "frontend-platform",
|
||||
"reason": "Synthetic credential verifies the scoped test-only allowlist.",
|
||||
"expiresAt": "2027-07-26T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"exceptions": []
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"providerMode": "external-file",
|
||||
"inputEnvironment": "VULNERABILITY_REPORT_PATH",
|
||||
"blockAtSeverity": "high",
|
||||
"allowedSeverities": ["unknown", "low", "moderate", "high", "critical"],
|
||||
"missingProviderStatus": "FAIL_UNVERIFIED"
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"repositoryBaseline": 285,
|
||||
"generatedPaths": [],
|
||||
"summary": {
|
||||
"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, "functions": 95, "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, "functions": 70, "branches": 60 }
|
||||
},
|
||||
{
|
||||
"path": "src/adapters/telemetry/best-effort-telemetry.ts",
|
||||
"owner": "telemetry-runtime",
|
||||
"minimum": { "lines": 85, "statements": 85, "functions": 70, "branches": 75 }
|
||||
},
|
||||
{
|
||||
"path": "src/application/create-application.ts",
|
||||
"owner": "application-runtime",
|
||||
"minimum": { "lines": 90, "statements": 90, "functions": 80, "branches": 68 }
|
||||
},
|
||||
{
|
||||
"path": "src/application/policies/compatibility.ts",
|
||||
"owner": "application-policy",
|
||||
"minimum": { "lines": 95, "statements": 95, "functions": 95, "branches": 75 }
|
||||
},
|
||||
{
|
||||
"path": "src/application/policies/performance-budgets.ts",
|
||||
"owner": "application-policy",
|
||||
"minimum": { "lines": 80, "statements": 80, "functions": 80, "branches": 40 }
|
||||
},
|
||||
{
|
||||
"path": "src/application/policies/promotion-readiness.ts",
|
||||
"owner": "release-runtime",
|
||||
"minimum": { "lines": 95, "statements": 95, "functions": 95, "branches": 95 }
|
||||
},
|
||||
{
|
||||
"path": "src/application/use-cases/decide-chunk-recovery.ts",
|
||||
"owner": "application-runtime",
|
||||
"minimum": { "lines": 90, "statements": 90, "functions": 95, "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, "functions": 95, "branches": 58 }
|
||||
},
|
||||
{
|
||||
"path": "src/features/reference-feature/adapters/reference-http-gateway.ts",
|
||||
"owner": "reference-feature",
|
||||
"minimum": { "lines": 90, "statements": 90, "functions": 90, "branches": 90 }
|
||||
},
|
||||
{
|
||||
"path": "src/presentation/adapters/query/application-query.ts",
|
||||
"owner": "presentation-runtime",
|
||||
"minimum": { "lines": 90, "statements": 90, "functions": 90, "branches": 80 }
|
||||
}
|
||||
],
|
||||
"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/application/policies/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,22 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"scenarioCatalogs": [
|
||||
{
|
||||
"owner": "reference-feature",
|
||||
"path": "tests/mocks/scenarios/catalog.ts",
|
||||
"expectationExport": "HTTP_SCENARIO_EXPECTATIONS",
|
||||
"receiptPath": "artifacts/tests/http-scenario-executions.json",
|
||||
"receiptSchemaVersion": 1
|
||||
}
|
||||
],
|
||||
"sourceContracts": [
|
||||
{
|
||||
"owner": "reference-feature",
|
||||
"path": "tests/mocks/handlers/reference-resources.ts",
|
||||
"requiredTokens": [
|
||||
"assertOperationScenario",
|
||||
"../scenarios/catalog.ts"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# Keycloak realm
|
||||
|
||||
`tech-log-realm.json` is imported by the `keycloak` service at start
|
||||
(`--import-realm`). It exists because the realm was previously created by hand,
|
||||
which meant §27 of the release checklist — "Keycloak Realm 설정을 복원할 수
|
||||
있다" — had no answer: nothing in either repository described the realm.
|
||||
|
||||
What it declares, and why each part is load-bearing:
|
||||
|
||||
- **`studio-author` realm role.** `StudioAuthzEnvironmentPostProcessor` maps this
|
||||
name to `studio:read` and `studio:write`. The name is configurable through
|
||||
`APP_STUDIO_AUTHOR_ROLE`; if you change it here, change it there too.
|
||||
- **`tech-log-bff` confidential client.** The Authorization Code flow belongs to
|
||||
the backend, not the browser — the SPA never holds a token. `redirectUris` is
|
||||
relative so the same realm works on any origin the deployment is served from.
|
||||
- **`realm-roles` protocol mapper.** Without it the roles never reach the token,
|
||||
the registry resolves zero permissions, and every Studio call answers 403.
|
||||
|
||||
## Values that must be replaced
|
||||
|
||||
`CHANGE_ME_BFF_SECRET` and `CHANGE_ME_STUDIO_PASSWORD` are placeholders, and the
|
||||
deploy script substitutes them from the environment before import. They are left
|
||||
visible rather than pre-filled so a realm file committed with a real secret is an
|
||||
obvious mistake rather than a quiet one.
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"realm": "tech-log",
|
||||
"enabled": true,
|
||||
"sslRequired": "none",
|
||||
"registrationAllowed": false,
|
||||
"loginTheme": "keycloak",
|
||||
"accessTokenLifespan": 300,
|
||||
"ssoSessionIdleTimeout": 28800,
|
||||
"ssoSessionMaxLifespan": 86400,
|
||||
"roles": {
|
||||
"realm": [
|
||||
{ "name": "studio-author", "description": "Tech Log Studio 편집 권한 (studio:read + studio:write)" }
|
||||
]
|
||||
},
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "tech-log-bff",
|
||||
"name": "Tech Log BFF",
|
||||
"description": "백엔드가 소유하는 Authorization Code 클라이언트. SPA 는 토큰을 직접 들지 않는다.",
|
||||
"enabled": true,
|
||||
"publicClient": false,
|
||||
"secret": "CHANGE_ME_BFF_SECRET",
|
||||
"standardFlowEnabled": true,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"serviceAccountsEnabled": false,
|
||||
"redirectUris": ["/login/oauth2/code/*"],
|
||||
"webOrigins": ["+"],
|
||||
"protocolMappers": [
|
||||
{
|
||||
"name": "realm-roles",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-usermodel-realm-role-mapper",
|
||||
"config": {
|
||||
"claim.name": "realm_access.roles",
|
||||
"jsonType.label": "String",
|
||||
"multivalued": "true",
|
||||
"access.token.claim": "true",
|
||||
"id.token.claim": "true",
|
||||
"userinfo.token.claim": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"username": "studio",
|
||||
"enabled": true,
|
||||
"emailVerified": true,
|
||||
"email": "studio@tech-log.local",
|
||||
"firstName": "Studio",
|
||||
"lastName": "Author",
|
||||
"credentials": [{ "type": "password", "value": "CHANGE_ME_STUDIO_PASSWORD", "temporary": false }],
|
||||
"realmRoles": ["default-roles-tech-log", "studio-author"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
# The Tech Log dev stack: one origin, five services.
|
||||
#
|
||||
# nginx is the only published port. Everything the browser touches — the SPA,
|
||||
# /api, the OIDC redirect chain, and Keycloak under /auth — arrives on the same
|
||||
# origin, which is what lets the session be a plain first-party httpOnly cookie
|
||||
# instead of a cross-site one needing SameSite=None.
|
||||
#
|
||||
# browser ──> frontend(nginx) ──┬─> / SPA bundle
|
||||
# ├─> /api backend
|
||||
# ├─> /oauth2 /login /logout backend (BFF)
|
||||
# └─> /auth keycloak
|
||||
#
|
||||
# Secrets here are development values and are meant to be replaced by the
|
||||
# deployment; they are named in .env so nothing is baked into an image.
|
||||
|
||||
name: tech-log
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-tech_log}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-tech_log}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
||||
TZ: UTC
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tech_log} -d ${POSTGRES_DB:-tech_log}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
restart: unless-stopped
|
||||
networks: [tech-log]
|
||||
|
||||
redis:
|
||||
# Holds the Studio session. Losing it signs everyone out; it holds nothing
|
||||
# else, so it is not backed by a volume on purpose.
|
||||
image: redis:7-alpine
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks: [tech-log]
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.7.0
|
||||
command: ["start-dev", "--import-realm", "--http-relative-path=/auth"]
|
||||
environment:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN:-admin}
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?set KEYCLOAK_ADMIN_PASSWORD}
|
||||
KC_HTTP_ENABLED: "true"
|
||||
# Behind nginx: Keycloak must build its URLs from the forwarded host, or
|
||||
# the redirect back from the login page points at the container.
|
||||
KC_HOSTNAME: ${PUBLIC_ORIGIN:?set PUBLIC_ORIGIN}/auth
|
||||
KC_HOSTNAME_STRICT: "false"
|
||||
KC_PROXY_HEADERS: xforwarded
|
||||
KC_HEALTH_ENABLED: "true"
|
||||
volumes:
|
||||
- ${KEYCLOAK_IMPORT_DIR:-./deploy/keycloak}:/opt/keycloak/data/import:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000 && echo -e 'GET /auth/health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '\"status\": \"UP\"'"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 40s
|
||||
restart: unless-stopped
|
||||
networks: [tech-log]
|
||||
|
||||
backend:
|
||||
image: ${BACKEND_IMAGE:-tech-log-backend:local}
|
||||
volumes:
|
||||
- tls-public:/tls-public:ro
|
||||
# The image entrypoint is `java -jar /app/app.jar`; this wraps it so the
|
||||
# frontend's certificate lands in the JVM truststore first. Without it the
|
||||
# OIDC metadata fetch fails PKIX validation and the process crash-loops.
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
until [ -f /tls-public/server.crt ]; do sleep 1; done
|
||||
# The image runs as a non-root user, so the JVM's own cacerts is not
|
||||
# writable — importing there silently did nothing and the metadata fetch
|
||||
# kept failing PKIX. Copy it somewhere writable, add the edge
|
||||
# certificate, and point the JVM at that.
|
||||
cp "/opt/java/openjdk/lib/security/cacerts" /tmp/truststore.jks
|
||||
keytool -importcert -noprompt -trustcacerts -alias tech-log-edge \
|
||||
-file /tls-public/server.crt \
|
||||
-keystore /tmp/truststore.jks -storepass changeit
|
||||
exec java \
|
||||
-Djavax.net.ssl.trustStore=/tmp/truststore.jks \
|
||||
-Djavax.net.ssl.trustStorePassword=changeit \
|
||||
-jar /app/app.jar
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: local
|
||||
# Persistence
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-tech_log}
|
||||
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-tech_log}
|
||||
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver
|
||||
SPRING_FLYWAY_ENABLED: "true"
|
||||
SPRING_JPA_HIBERNATE_DDL_AUTO: none
|
||||
CA_SKELETON_PERSISTENCE_VENDOR: postgresql
|
||||
# BFF session
|
||||
CA_SKELETON_SECURITY_AUTH_MODE: redis-session
|
||||
CA_SKELETON_SECURITY_SESSION_COOKIE_NAME: TECHLOG_SESSION
|
||||
APP_REDIS_ENABLED: "true"
|
||||
APP_REDIS_AUTHENTICATION_ANONYMOUS_ACCESS_ACCEPTED: "true"
|
||||
SPRING_DATA_REDIS_HOST: redis
|
||||
SPRING_DATA_REDIS_PORT: "6379"
|
||||
# OIDC. The issuer is the browser-facing URL because the tokens carry it
|
||||
# and the browser is redirected there; the container reaches the same
|
||||
# Keycloak through nginx on the compose network.
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENT_ID: ${OIDC_CLIENT_ID:-tech-log-bff}
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:?set OIDC_CLIENT_SECRET}
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_SCOPE: openid,profile,email
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_AUTHORIZATION_GRANT_TYPE: authorization_code
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_REDIRECT_URI: "${PUBLIC_ORIGIN}/login/oauth2/code/keycloak"
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KEYCLOAK_ISSUER_URI: ${PUBLIC_ORIGIN}/auth/realms/${KEYCLOAK_REALM:-tech-log}
|
||||
APP_STUDIO_AUTHOR_ROLE: ${STUDIO_AUTHOR_ROLE:-studio-author}
|
||||
APP_STUDIO_POST_LOGIN_REDIRECT: "${PUBLIC_ORIGIN}/studio"
|
||||
# Behind a proxy: trust the forwarded headers nginx sets, so redirect URLs
|
||||
# and client IPs are the browser's, not the container's.
|
||||
APP_SERVER_FORWARD_HEADERS_STRATEGY: framework
|
||||
TZ: UTC
|
||||
# The issuer in a token is the browser-facing URL, and the backend has to
|
||||
# both validate that exact string and fetch the realm's metadata from it.
|
||||
# Inside the container that host does not resolve, so discovery failed and
|
||||
# the process crash-looped. Mapping the public host to the docker gateway
|
||||
# makes one URL work from both sides — the browser reaches nginx directly,
|
||||
# the backend reaches the same nginx through the published port.
|
||||
extra_hosts:
|
||||
- "${PUBLIC_HOST:?set PUBLIC_HOST}:host-gateway"
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
redis: { condition: service_healthy }
|
||||
keycloak: { condition: service_healthy }
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/healthcheck"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 45s
|
||||
restart: unless-stopped
|
||||
networks: [tech-log]
|
||||
|
||||
frontend:
|
||||
image: ${FRONTEND_IMAGE:-tech-log-frontend:local}
|
||||
# The backend fetches the realm metadata from the same HTTPS origin the
|
||||
# browser uses, so it has to trust this certificate. Publishing it to a
|
||||
# shared volume keeps one certificate for both sides; a deployment that
|
||||
# mounts a CA-issued certificate over /etc/nginx/tls needs neither this nor
|
||||
# the backend's import step.
|
||||
volumes:
|
||||
- tls-public:/tls-public
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- "cp /etc/nginx/tls/server.crt /tls-public/server.crt && exec nginx -g 'daemon off;'"
|
||||
|
||||
ports:
|
||||
- "${PUBLIC_HTTP_PORT:-8088}:80"
|
||||
- "${PUBLIC_PORT:-8443}:443"
|
||||
depends_on:
|
||||
backend: { condition: service_started }
|
||||
keycloak: { condition: service_started }
|
||||
restart: unless-stopped
|
||||
networks: [tech-log]
|
||||
|
||||
networks:
|
||||
tech-log:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
tls-public:
|
||||
@@ -1,67 +0,0 @@
|
||||
# Manual accessibility review checklist
|
||||
|
||||
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
||||
review all 27 route records in `artifacts/tests/a11y-manual/` against one
|
||||
release candidate and sign them. The required TechLog Public and Studio scope is
|
||||
derived from the installed route registry, so the gate rejects stale, missing,
|
||||
or additional route records as well as blank identity/timestamp/signature
|
||||
fields, pending verdicts, and mismatched release IDs. The scope is:
|
||||
`NOT_FOUND`, `TECH_LOG_CASE`, `TECH_LOG_EXPLORE`, `TECH_LOG_EXPLORE_KIND`,
|
||||
`TECH_LOG_HOME`, `TECH_LOG_PROFILE`, `TECH_LOG_PROJECT`,
|
||||
`TECH_LOG_PROJECTS`, `TECH_LOG_PROJECT_ACTIVITY`,
|
||||
`TECH_LOG_PROJECT_DECISIONS`, `TECH_LOG_PROJECT_RECORDS`,
|
||||
`TECH_LOG_QUESTION`, `TECH_LOG_REFERENCE`, `TECH_LOG_RELEASE`,
|
||||
`TECH_LOG_RELEASES`, `TECH_LOG_SEARCH`, `TECH_LOG_STUDIO_DOCUMENTS`,
|
||||
`TECH_LOG_STUDIO_DOCUMENT_EDIT`, `TECH_LOG_STUDIO_DOCUMENT_NEW`,
|
||||
`TECH_LOG_STUDIO_DOCUMENT_PREVIEW`, `TECH_LOG_STUDIO_DOCUMENT_PUBLISH`,
|
||||
`TECH_LOG_STUDIO_DOCUMENT_VALIDATION`, `TECH_LOG_STUDIO_HOME`,
|
||||
`TECH_LOG_STUDIO_NOT_FOUND`, `TECH_LOG_STUDIO_PUBLICATIONS`,
|
||||
`TECH_LOG_STUDIO_PUBLICATION_PREVIEW`, `TECH_LOG_TOPIC`.
|
||||
|
||||
This list is not maintained by hand: `verify:documentation` compares it against
|
||||
the installed route registry and fails when a registered route is absent. The
|
||||
template carried the same rule for its own example screens; the route set is
|
||||
this product's, the rule is the template's.
|
||||
|
||||
Allowed item verdicts:
|
||||
|
||||
- `pass`
|
||||
- `not-applicable (<specific reason>)`
|
||||
|
||||
Required record:
|
||||
|
||||
```text
|
||||
Status: reviewed
|
||||
Route ID: <exact installed route ID>
|
||||
Release ID: <immutable release ID>
|
||||
Reviewer: <human reviewer identity>
|
||||
Reviewed at: <RFC 3339 timestamp>
|
||||
Signature: <reviewer identity or approved signature reference>
|
||||
Attestation: accepted
|
||||
M1 Keyboard: pass
|
||||
M2 Visible focus: pass
|
||||
M3 Route focus: pass
|
||||
M4 Modal focus: not-applicable (no modal on this route)
|
||||
M5 Error association: not-applicable (no form error on this route)
|
||||
M6 Color signal: pass
|
||||
M7 Reduced motion: pass
|
||||
Screen reader: pass
|
||||
Notes: <observations and linked defect IDs>
|
||||
```
|
||||
|
||||
The reviewer must verify:
|
||||
|
||||
- M1: every action works without a pointing device
|
||||
- M2: every focused element has a visible indicator
|
||||
- M3: route transitions move focus to a deterministic target
|
||||
- M4: modal focus is trapped and restored, when a modal exists
|
||||
- M5: errors are programmatically associated with their controls, when present
|
||||
- M6: state never relies on color alone
|
||||
- M7: non-essential motion is suppressed with reduced-motion preference
|
||||
- Screen reader: headings, live regions, errors, and actions are announced once
|
||||
|
||||
Routes with dialogs or form errors require real M4 modal-focus or M5
|
||||
error-association review; those items must not be marked not-applicable when the
|
||||
reviewed route exposes the relevant behavior. Passing automated evidence means
|
||||
only that tested pages had no critical or serious axe findings under the
|
||||
recorded browser runs.
|
||||
-5957
File diff suppressed because it is too large
Load Diff
@@ -1,973 +0,0 @@
|
||||
# API contract, Schema, Mapper와 Server State platform
|
||||
|
||||
> **정본 안내 (non-authoritative for runtime capability decisions)**
|
||||
>
|
||||
> Runtime Config/boot, Fetch HTTP client, Router, Query/Mutation, realtime 공통 경계, Web Worker,
|
||||
> Service Worker, offline command와 Background Sync의 구현 결정은
|
||||
> [프론트엔드 런타임 Capability 저장소 정합형 구현 결정 폐쇄 상세 설계](./2026-07-30-frontend-runtime-capability-repository-aligned-implementation-closed-deep-design.md)가 정본이다.
|
||||
> 본 문서의 해당 서술이 정본과 충돌하면 정본을 따른다.
|
||||
|
||||
|
||||
- 상태: capability별 current/target 분리, production design accepted
|
||||
- 기준일: 2026-07-28
|
||||
- 범위: REST, GraphQL over HTTP, Connect-Web/Connect, gRPC-Web,
|
||||
Protobuf/REST Gateway, runtime Schema, boundary Mapper, TanStack Query 기반
|
||||
Server State Cache
|
||||
- 관련 결정:
|
||||
- [VD-23 API transport selection과 REST execution](./decisions/VD-23-api-transport-selection-and-rest-execution.md)
|
||||
- [VD-24 Runtime schema와 boundary mapper](./decisions/VD-24-runtime-schema-and-boundary-mapper.md)
|
||||
- [VD-25 Server state cache lifecycle](./decisions/VD-25-server-state-cache-lifecycle.md)
|
||||
- [VD-26 Persisted GraphQL operation](./decisions/VD-26-persisted-graphql-operation.md)
|
||||
- [VD-27 gRPC-Web unary와 server stream](./decisions/VD-27-grpc-web-unary-and-server-stream.md)
|
||||
- [VD-29 Connect-Web와 browser Protobuf runtime](./decisions/VD-29-connect-web-and-browser-protobuf-runtime.md)
|
||||
- [VD-30 Protobuf contract와 REST Gateway](./decisions/VD-30-protobuf-contract-and-rest-gateway.md)
|
||||
- browser Protobuf/gateway 상세 설계:
|
||||
[Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)
|
||||
- backend handoff:
|
||||
[Backend API와 Server State contract](./backend-api-and-server-state-contract.md)
|
||||
- 운영 절차:
|
||||
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 다음 질문을 하나의 production 계약으로 닫는다.
|
||||
|
||||
- REST, GraphQL, Connect와 gRPC-Web 중 무엇을 어디에 사용하는가
|
||||
- Protobuf contract와 REST Gateway가 transport/runtime과 어떻게 분리되는가
|
||||
- request/response가 어느 지점까지 untrusted wire data인가
|
||||
- TypeScript type, generated code와 runtime validation의 역할은 무엇인가
|
||||
- DTO를 domain/application projection으로 누가 변환하는가
|
||||
- server response를 어떤 query identity와 lifecycle로 cache하는가
|
||||
- schema, mapper, transport와 cache가 바뀔 때 어떻게 배포·관측·rollback하는가
|
||||
|
||||
각 protocol은 서로 대체 가능한 URL 호출 문법이 아니다. transport-specific
|
||||
codec, proxy와 failure semantics는 adapter가 소유한다. application은 transport
|
||||
종류, URL, GraphQL document, protobuf message나 TanStack Query를 직접 알지 않고
|
||||
feature-owned gateway와 application input만 호출한다.
|
||||
|
||||
## 2. 상태 모델
|
||||
|
||||
이 문서는 browser data 설계와 동일한 primary current-status literal을 사용한다.
|
||||
|
||||
| primary status | 의미 |
|
||||
| --- | --- |
|
||||
| `COMPOSED` | production bootstrap 또는 설치된 feature 호출 경로에 concrete runtime이 실제 연결돼 있다. |
|
||||
| `AVAILABLE_NOT_COMPOSED` | 실행 가능한 reference runtime과 test가 있지만 production graph에는 연결하지 않았다. |
|
||||
| `DESIGNED_NOT_IMPLEMENTED` | 계약·불변조건·failure와 promotion 기준은 승인됐지만 해당 runtime 또는 필수 orchestration이 없다. |
|
||||
| `NOT_SELECTED` | 제품 요구, owner와 비용이 승인되지 않아 의도적으로 선택하지 않았다. |
|
||||
| `PLATFORM_LIMITED` | target browser/protocol이 요구 semantics를 공통으로 보장하지 못한다. |
|
||||
|
||||
primary status와 다음 readiness 축을 섞지 않는다.
|
||||
|
||||
```text
|
||||
Selection
|
||||
NOT_SELECTED | SELECTED | REMOVING
|
||||
|
||||
TrafficAdmission
|
||||
DISABLED | SHADOW | CANARY | ENABLED
|
||||
|
||||
RuntimeHealth
|
||||
UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
|
||||
|
||||
PromotionEvidence
|
||||
MISSING | PARTIAL | COMPLETE | EXPIRED
|
||||
```
|
||||
|
||||
`COMPOSED`는 traffic이 켜졌거나 provider가 conformant라는 뜻이 아니다.
|
||||
`AVAILABLE_NOT_COMPOSED`도 제품 bundle에 dependency가 들어갔다는 뜻이 아니다.
|
||||
|
||||
## 3. 현재 capability ledger
|
||||
|
||||
| capability | primary current status | 현재 증거 | 목표 또는 잔여 |
|
||||
| --- | --- | --- | --- |
|
||||
| installed REST reference vertical | `COMPOSED` | operation registry → request schema → HTTP → envelope/payload schema → mapper → application input → Query 화면 경로 | 아래 REST hardening delta와 실제 제품 provider 계약 |
|
||||
| shared REST JSON executor | `COMPOSED` | path/search/body codec projection, shared deadline/retry-sleep budget, AbortSignal, bounded auth recovery, exact envelope/media/status, safe failure와 diagnostics | 204/304/412 execution join과 actual provider conformance |
|
||||
| REST v2 security/execution baseline | `COMPOSED` | collision-aware operation composition, path placeholder↔codec key exact join, prefix-preserving HTTPS/loopback provider, named bearer/CSRF profile와 credential-mode ceiling, auth fail-before-fetch, bounded JSON, outbound correlation/status·physical-attempt 관측 | cookie-CSRF/CORS provider evidence, 204/304/412 conditional execution과 compatibility artifact |
|
||||
| GraphQL provider-neutral reference adapter | `DESIGNED_NOT_IMPLEMENTED` | source/dependency/codegen/runtime 없음 | persisted-operation-only transport, GraphQL response decoder, mapper binding과 contract harness |
|
||||
| product GraphQL composition | `NOT_SELECTED` | endpoint/schema/persisted manifest/owner 없음 | 제품 query가 REST보다 GraphQL aggregation을 정당화할 때 선택 |
|
||||
| GraphQL batching, subscription, `@defer`/`@stream` | `NOT_SELECTED` | 없음 | 각각 독립 ADR, proxy/browser lifecycle과 cache semantics 필요 |
|
||||
| provider-neutral Browser RPC V3 contract/runtime | `AVAILABLE_NOT_COMPOSED` | operation/profile/schema/mapper/encoder/transport exact join, typed application port, bounded unary retry/deadline/abort, server-stream idle/total/message/terminal/generation fence와 fail-closed unavailable adapter test | selected descriptor/generated client와 protocol-specific bounded transport를 붙이고 actual provider/browser conformance |
|
||||
| gRPC-Web unary reference adapter | `DESIGNED_NOT_IMPLEMENTED` | source/dependency/generated message 없음 | fixed method registry, protobuf codec, trailers/status/deadline와 proxy conformance |
|
||||
| gRPC-Web server-stream reference adapter | `DESIGNED_NOT_IMPLEMENTED` | source 없음 | bounded frame/idle/total budget, sequence/resume application protocol과 stream port |
|
||||
| product gRPC-Web composition | `NOT_SELECTED` | service descriptor/proxy/owner 없음 | browser-facing gRPC-Web gateway가 실제 이점을 줄 때 선택 |
|
||||
| gRPC-Web client-streaming/bidi guarantee | `PLATFORM_LIMITED` | gRPC-Web browser baseline이 해당 semantics를 제공하지 않음 | REST upload, WebSocket/WebTransport 또는 별도 protocol을 선택 |
|
||||
| Protobuf schema/codegen governance | `DESIGNED_NOT_IMPLEMENTED` | `.proto`, Buf config, descriptor와 generated output 없음 | authenticated source, immutable descriptor, deterministic codegen과 compatibility evidence |
|
||||
| Connect-Web unary/server-stream reference adapter | `DESIGNED_NOT_IMPLEMENTED` | `@connectrpc/*`, `@bufbuild/protobuf`, generated service와 provider 없음 | exact Connect/gRPC-Web transport row, bounded decode와 actual browser/provider conformance |
|
||||
| product Connect protocol composition | `NOT_SELECTED` | service/provider/owner 없음 | Protobuf-first backend의 selected browser operation이 있을 때만 선택 |
|
||||
| Connect browser client-streaming/bidi guarantee | `PLATFORM_LIMITED` | Connect protocol 기능과 browser request-stream 지원은 다름 | 별도 duplex/application protocol 선택 |
|
||||
| Protobuf REST Gateway reference contract/harness | `DESIGNED_NOT_IMPLEMENTED` | HttpRule/transcoder/OpenAPI/provider fixture 없음 | selected kind의 deterministic/provider conformance |
|
||||
| product Protobuf REST Gateway composition | `NOT_SELECTED` | route/provider/owner 없음 | curated BFF, grpc-gateway 또는 Envoy transcoder 중 하나와 public HTTP contract 승인 |
|
||||
| feature runtime request/response Schema | `COMPOSED` | Zod request/payload schemas와 installed schema registry가 reference feature에 연결 | byte/depth/node ceiling, unknown-field profile, schema artifact/digest와 multi-protocol source governance |
|
||||
| schema/mapper v2 reference baseline | `COMPOSED` | collision-aware schema codec/mapper contribution install, operation schema/mapper reference resolution, request reject/response strip 방향, bounded collection, typed no-throw mapping result와 operation별 cast-free result guard | actual codec fingerprint, source provenance와 multi-protocol compatibility policy |
|
||||
| generated contract artifact governance | `DESIGNED_NOT_IMPLEMENTED` | `generated-api` recipe만 있고 generator/provider 선택 없음 | OpenAPI/GraphQL/proto source authentication, pinned generation, drift/breaking gate와 N/N-1 |
|
||||
| feature boundary Mapper | `COMPOSED` | installed mapper metadata composer와 response-schema exact join 뒤 typed no-throw MappingResult → immutable domain/application view 실행 | numeric/date/null/enum canonical rules와 generated-artifact join |
|
||||
| TanStack Query memory Server State | `COMPOSED` | QueryClient, cancellation, stale-degraded UI, session-generation cancel/clear fence와 invalidation coordinator | account identity projection과 bounded topic↔namespace many-to-many registry |
|
||||
| reference bound-query/server-state profile | `COMPOSED` | bound definition, strict canonical input, scope-private opaque identity, active lease/LRU/collision/entry-byte ceiling, per-profile policy와 result admission | account projection, conditional HTTP execution join과 pagination composition |
|
||||
| mutation duplicate coordinator baseline | `COMPOSED` | QueryClient runtime/scope 단위 exact semantic input identity로 identical만 join하고 distinct input을 합치지 않으며 late scope result를 폐기 | logical-key serialization과 effect certainty/reconcile |
|
||||
| optimistic ordered-layer runtime | `AVAILABLE_NOT_COMPOSED` | out-of-order commit/rollback, authoritative external update 재적용과 expired-scope 제거 test | 제품 mutation의 deterministic membership/revision contract 승인 뒤 definition에 연결 |
|
||||
| conditional validator CAS sidecar | `AVAILABLE_NOT_COMPOSED` | scope/representation/cache revision exact binding, ETag validation과 bounded capacity test; session 전환 clear는 production infrastructure에 연결 | HTTP If-None-Match/304 query transaction과 query removal lifecycle join |
|
||||
| bounded cursor chain runtime | `AVAILABLE_NOT_COMPOSED` | page invariant, cursor loop, snapshot drift, page/item/byte/cursor ceiling과 abort test | backend CursorPage DTO/next cursor 계약 후 reference/infinite-query binding |
|
||||
| cross-context server-state invalidation | `COMPOSED` | singular opaque topic 기반 invalidate-only coordinator와 session-generation local reset | account projection과 bounded topic↔namespace many-to-many registry |
|
||||
| normalized GraphQL entity cache | `NOT_SELECTED` | 없음 | TanStack operation-result cache로 해결되지 않는 측정된 요구가 있을 때 별도 선택 |
|
||||
| persisted query cache | reference `DESIGNED_NOT_IMPLEMENTED`, product `NOT_SELECTED` | Web Storage persistence는 금지, IndexedDB persister 없음 | VD-13의 scope/retention/restore gate를 별도 통과 |
|
||||
| offline mutation queue | `NOT_SELECTED` | foreground mutation만 존재 | backend idempotency/cursor/conflict protocol과 durable command owner 필요 |
|
||||
|
||||
현재 REST reference는 `Response.json()`이 아니라 byte-bounded reader를 사용하고,
|
||||
external auth owner는 allowlisted header patch만 반환한다. 인증 통합 실패는 fetch
|
||||
전에 닫히며 correlation, success status와 physical attempt가 terminal observation에
|
||||
반영된다. 다만 이 baseline을 conditional response, complete pagination,
|
||||
provider conformance나 GraphQL/gRPC runtime의 증거로 재사용하지 않는다.
|
||||
마찬가지로 Browser RPC V3 공통 coordinator의 `AVAILABLE_NOT_COMPOSED` 판정은
|
||||
vendor wire adapter의 구현 판정이 아니다. `@connectrpc/*`, official grpc-web,
|
||||
generated message와 descriptor가 없는 현재 상태에서 Connect/gRPC-Web 각 row는
|
||||
계속 `DESIGNED_NOT_IMPLEMENTED`다.
|
||||
|
||||
## 4. 최상위 경계
|
||||
|
||||
```text
|
||||
presentation
|
||||
-> feature application input
|
||||
-> feature use case
|
||||
-> feature gateway port
|
||||
-> operation registry
|
||||
-> REST adapter
|
||||
-> GraphQL adapter
|
||||
-> Connect adapter
|
||||
-> gRPC-Web adapter
|
||||
-> bounded wire decoder
|
||||
-> runtime schema / semantic validation
|
||||
-> boundary mapper
|
||||
-> immutable application projection
|
||||
-> server-state query adapter
|
||||
-> registry-owned query identity and policy
|
||||
-> mapped application result only
|
||||
```
|
||||
|
||||
금지 경로:
|
||||
|
||||
```text
|
||||
page -> fetch / GraphQL SDK / generated Connect/gRPC client
|
||||
page -> raw URL / query document / protobuf message
|
||||
transport DTO -> domain or presentation public type
|
||||
Response / GraphQL response / generated message -> Query cache
|
||||
Query cache -> authorization or business conflict authority
|
||||
```
|
||||
|
||||
application port는 use-case 의미를 표현한다. 예를 들어
|
||||
`listResources(filters)`, `createResource(command)`는 허용하지만
|
||||
`executeGraphql(document, variables)`, `grpcCall(service, method, bytes)`와
|
||||
`request(url, options)`는 허용하지 않는다.
|
||||
|
||||
## 5. Protocol-neutral operation contract
|
||||
|
||||
각 외부 호출은 build-time registry의 discriminated row 하나로 고정한다.
|
||||
application caller는 `operationId`와 schema가 허용한 input만 제출한다.
|
||||
|
||||
```text
|
||||
ApiOperationContractV3
|
||||
registryVersion
|
||||
operationId
|
||||
owner
|
||||
protocol = REST | GRAPHQL_HTTP | CONNECT_HTTP | GRPC_WEB
|
||||
semantics = QUERY | COMMAND | SERVER_STREAM
|
||||
authProfileId
|
||||
csrfProfileId
|
||||
replayPolicy = SAFE | IDEMPOTENT | KEYED_COMMAND | NON_REPLAYABLE
|
||||
idempotencyKeyPolicy = NONE | REQUIRED
|
||||
requestSchemaId
|
||||
responseSchemaId
|
||||
mapperId
|
||||
errorProfileId
|
||||
deadlineProfileId
|
||||
retryProfileId
|
||||
serverStateProfileId | null
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
dataClassification
|
||||
compatibility
|
||||
globalApiContractVersion
|
||||
protocolArtifactId
|
||||
minimumServerVersion
|
||||
retirementEpoch | null
|
||||
protocolBinding
|
||||
```
|
||||
|
||||
`protocolBinding`은 transport별 closed union이다.
|
||||
|
||||
```text
|
||||
REST
|
||||
method
|
||||
relativePathTemplate
|
||||
requestProjection
|
||||
requestMediaProfile
|
||||
responseMediaProfile
|
||||
conditionalProfile
|
||||
|
||||
GRAPHQL_HTTP
|
||||
endpointId
|
||||
graphqlHttpProfileRevision
|
||||
persistedEnvelopeProfileId
|
||||
responseStatusMediaProfileId
|
||||
persistedOperationId
|
||||
persistedOperationSha256
|
||||
operationType
|
||||
partialDataPolicy
|
||||
|
||||
GRPC_WEB
|
||||
endpointId
|
||||
clientRuntimeId
|
||||
grpcWebWireSpecRevision
|
||||
transportProfile
|
||||
responseHttpStatusProfileId
|
||||
fullyQualifiedService
|
||||
method
|
||||
rpcKind = UNARY | SERVER_STREAM
|
||||
requestMessageId
|
||||
responseMessageId
|
||||
|
||||
CONNECT_HTTP
|
||||
endpointId
|
||||
clientRuntimeId
|
||||
connectProtocolRevision
|
||||
encoding = PROTO_JSON | PROTO_BINARY
|
||||
requestMethod = POST | GET
|
||||
fullyQualifiedService
|
||||
method
|
||||
rpcKind = UNARY | SERVER_STREAM
|
||||
descriptorArtifactId
|
||||
descriptorDigest
|
||||
```
|
||||
|
||||
registry validation은 다음을 build/boot 전에 거절한다.
|
||||
|
||||
- 중복 operation/mapper/profile ID
|
||||
- protocol과 맞지 않는 binding field
|
||||
- 등록되지 않은 schema, mapper, auth, deadline, retry와 cache profile
|
||||
- `QUERY`인데 replay policy가 `SAFE | IDEMPOTENT`가 아님
|
||||
- `KEYED_COMMAND`인데 idempotency key policy가 `REQUIRED`가 아니거나 backend
|
||||
dedupe/reconcile profile이 없음
|
||||
- `NON_REPLAYABLE`인데 network/401 replay가 enabled
|
||||
- `COMMAND`인데 cache profile이 query data owner로 지정됨
|
||||
- `SERVER_STREAM`인데 ordinary query cache profile을 사용
|
||||
- opening retry가 enabled인데 replay policy가 `SAFE | IDEMPOTENT`가 아님
|
||||
- `KEYED_COMMAND | NON_REPLAYABLE` server stream인데 opening retry가 enabled거나
|
||||
explicit resume/reconcile/dedupe profile이 없음
|
||||
- `SERVER_STREAM`인데 `protocol=CONNECT_HTTP | GRPC_WEB`과
|
||||
`rpcKind=SERVER_STREAM` 조합이 아님. REST SSE와 GraphQL subscription을 이
|
||||
registry 의미로 암묵 등록하지 않음
|
||||
- unsafe REST method 또는 GraphQL mutation인데 CSRF/replay/key 결정이 없음
|
||||
- gRPC-Web client/bidi method
|
||||
- Connect browser client/bidi method 또는 descriptor의 `NO_SIDE_EFFECTS`가 없는
|
||||
Connect GET
|
||||
- absolute URL, runtime GraphQL document 또는 caller-provided service/method
|
||||
- implementation hard ceiling보다 큰 timeout, byte, frame, page와 retry 값
|
||||
- GraphQL operation↔provider의 HTTP revision/envelope/status-media profile 또는
|
||||
Connect/gRPC-Web operation↔provider의 runtime/wire/status/capability tuple
|
||||
mismatch
|
||||
|
||||
현재 installed reference operation은 REST v2 metadata를 사용한다. operation,
|
||||
runtime schema codec과 mapper contribution은 object spread가 아니라 각각의
|
||||
collision-aware composer로 설치되며 duplicate ID를 덮어쓰기 전에 거절한다.
|
||||
boot-time binding 검증은 path placeholder와 codec key, provider/auth/CSRF profile,
|
||||
path/request/response schema와 mapper input schema를 exact resolve한 뒤 immutable
|
||||
registry를 발행한다. GraphQL/Connect/gRPC-Web discriminant와 protocol-specific
|
||||
binding은 해당 reference adapter가 아직 없으므로 source에 구현됐다고 표현하지
|
||||
않는다.
|
||||
|
||||
현재 `API_CONTRACT_VERSION`은 runtime config와 release manifest의 문자열 일치
|
||||
gate다. 목표 contract set은 REST/OpenAPI artifact, GraphQL schema/persisted
|
||||
manifest, protobuf descriptor, runtime schema/mapper registry digest를 포함한
|
||||
bounded manifest를 만들고 global compatibility version과 함께 release tuple에
|
||||
binding한다. 문자열 일치만 actual backend compatibility 증거로 사용하지 않는다.
|
||||
|
||||
## 6. 공통 실행 lifecycle
|
||||
|
||||
```text
|
||||
lookup exact operation
|
||||
-> freeze session/account/runtime generation
|
||||
-> validate and canonicalize application input
|
||||
-> derive exact query/command identity
|
||||
-> allocate total operation deadline
|
||||
-> encode transport request from registry binding
|
||||
-> attach credential/CSRF through approved owner
|
||||
-> execute bounded attempt
|
||||
-> bounded response/frame decode
|
||||
-> validate transport envelope/status
|
||||
-> validate operation DTO/message semantics
|
||||
-> map to immutable application projection
|
||||
-> re-check scope/generation
|
||||
-> return Result
|
||||
-> query adapter may admit mapped value to memory cache
|
||||
```
|
||||
|
||||
현재 v2 auth owner는 `Request`를 반환하지 않고 transport가 만든 immutable
|
||||
request binding에 대해 allowlisted credential patch만 제공한다. 최소한
|
||||
transport는 attach 뒤에도 URL, origin, method, body digest, content headers,
|
||||
idempotency와 conditional binding이 바뀌지 않았음을 다시 검증한다.
|
||||
auth-required operation은 session state가 unauthenticated/integration-failed이거나
|
||||
credential attachment가 실패하면 **fetch 0회**로 닫는다. 동시 401 recovery는
|
||||
session owner의 single-flight 한 번만 공유하며 replay-safe operation만 동일
|
||||
logical deadline/idempotency binding으로 한 번 재실행한다.
|
||||
|
||||
모든 async boundary와 terminal cache write 전에 captured generation을 확인한다.
|
||||
logout/account switch 뒤 끝난 response, mapper와 stream frame은 old runtime
|
||||
결과로 폐기한다.
|
||||
|
||||
deadline은 attempt마다 새로 시작하지 않는다.
|
||||
|
||||
```text
|
||||
total budget
|
||||
= credential attach
|
||||
+ network attempts
|
||||
+ retry delay
|
||||
+ body/frame read
|
||||
+ schema validation
|
||||
+ mapper
|
||||
```
|
||||
|
||||
각 phase에 별도 하위 ceiling을 둘 수 있지만 전체 deadline을 늘릴 수 없다.
|
||||
caller abort, runtime teardown, timeout과 provider cancellation은 서로 다른 safe
|
||||
failure로 정규화한다.
|
||||
|
||||
## 7. Transport 선택 기준
|
||||
|
||||
| 요구 | 기본 선택 | 이유 |
|
||||
| --- | --- | --- |
|
||||
| resource/command, HTTP cache/conditional semantics, 파일 handoff | REST | Web/BFF·CDN·운영 도구와 자연스럽고 failure/status가 명확함 |
|
||||
| 여러 aggregate를 한 화면 shape로 읽고 client별 selection이 유의미 | persisted GraphQL query | allowlisted operation으로 over/under-fetch를 줄일 수 있음 |
|
||||
| Protobuf-first backend의 내부 web UI, unary 또는 bounded server stream | Connect-Web/Connect 우선 평가 | generated descriptor와 Fetch 기반 browser RPC를 재사용 |
|
||||
| 기존 gRPC-Web proxy/conformance 자산 | selected gRPC-Web runtime | runtime별 binary/text/stream capability를 exact profile로 고정 |
|
||||
| ProtoJSON/HttpRule 자체가 승인된 public HTTP contract | generated REST Gateway 검토 | envelope/status/cache/idempotency를 별도 증명 |
|
||||
| 현재 REST envelope·ETag·Range·제품 DTO가 중요 | curated REST BFF 유지 | generated transcoder가 제품 HTTP 의미를 자동 제공하지 않음 |
|
||||
| browser client/bidi streaming | Connect/gRPC-Web 사용 금지 | protocol 자체 기능과 browser 공통 지원을 혼동하지 않음 |
|
||||
| arbitrary ad-hoc query | GraphQL 사용 금지 | cost, authorization, cache identity와 operation governance를 우회 |
|
||||
| 단순 CRUD인데 GraphQL/gRPC dependency만 추가 | REST 유지 | 복잡도와 bundle/proxy 비용을 정당화하지 못함 |
|
||||
|
||||
한 feature가 여러 protocol을 사용할 수 있지만 한 `operationId`는 한 protocol에만
|
||||
binding한다. query read를 shadow 비교하는 경우에도 secondary 결과는 사용자와
|
||||
cache에 반영하지 않는다. command는 protocol 장애를 이유로 자동 failover/replay
|
||||
하지 않는다.
|
||||
|
||||
## 8. REST 설계 요약
|
||||
|
||||
REST 상세 결정은 VD-23이 소유한다. 공통 baseline은 다음과 같다.
|
||||
|
||||
- base origin과 relative path template은 composition/registry가 소유한다.
|
||||
- provider base URL은 HTTPS와 exact origin/path-prefix를 고정하고 userinfo,
|
||||
query와 fragment를 금지한다. path join은 선택한 base prefix를 보존하며
|
||||
leading slash가 prefix를 조용히 제거하지 않는다.
|
||||
- method는 closed union이며 path/search/header/body는 각 runtime schema를 지난다.
|
||||
- replay semantics는 `SAFE`, `IDEMPOTENT`, `KEYED_COMMAND`,
|
||||
`NON_REPLAYABLE`로 method와 교차 검증한다.
|
||||
- caller-provided URL, header, `credentials`, redirect와 cache option을 금지한다.
|
||||
- cookie session이면 unsafe method에 approved CSRF owner가 필요하다.
|
||||
- `SAFE | IDEMPOTENT | KEYED_COMMAND` 중 exact retry profile과 실제 provider
|
||||
replay evidence가 있는 operation만 network retry한다.
|
||||
- keyed retry와 401 recovery는 같은 logical idempotency key를 유지한다.
|
||||
- total deadline, attempts, backoff와 `Retry-After`는 implementation ceiling 안이다.
|
||||
- JSON/error body는 present/valid `Content-Length` advisory preflight와 actual
|
||||
decoded-byte bounded stream reader 뒤 parse한다. encoded transfer cap은
|
||||
BFF/proxy/CDN가 집행한다.
|
||||
- status, content type, response media profile과 envelope 조합을 exact하게 검증한다.
|
||||
- 204, 304, 412, 422와 problem/envelope profile은 operation이 명시한 경우만 허용한다.
|
||||
- strong ETag는 cache identity가 아니라 exact representation revalidation
|
||||
metadata다. validator를 diagnostics에 기록하지 않는다. `Last-Modified`는
|
||||
별도 weak/time validator profile이 승인되기 전 이번 target에 포함하지 않는다.
|
||||
- cursor는 opaque하며 filter/sort/scope와 binding한다. arbitrary URL을
|
||||
`next` link로 따라가지 않는다.
|
||||
- browser HTTP cache와 application ETag/TanStack revalidation owner 중 하나를
|
||||
operation별로 선택한다. 두 cache의 freshness를 서로 추측해 합치지 않는다.
|
||||
- request/response body, URL query, authorization, CSRF, idempotency key와 raw
|
||||
backend copy를 log/telemetry에 넣지 않는다.
|
||||
|
||||
## 9. GraphQL 설계 요약
|
||||
|
||||
GraphQL 상세 결정은 VD-26이 소유한다. reference target은
|
||||
**persisted operation only**다.
|
||||
|
||||
- production bundle은 arbitrary GraphQL document string을 runtime에 받지 않는다.
|
||||
- build artifact가 operation name, stable ID, SHA-256, variables/result schema,
|
||||
schema digest와 owner를 manifest로 만든다.
|
||||
- BFF/router는 allowlist에 없는 ID/hash와 cost/depth limit 초과를 거절한다.
|
||||
- endpoint는 fixed HTTPS registry ID이고 POST가 기본이다.
|
||||
- selected provider가 지원하는 GraphQL-over-HTTP revision과 persisted-envelope
|
||||
extension을 profile에 고정한다. ID/hash-only request를 generic 표준 envelope로
|
||||
가장하지 않는다.
|
||||
- `Accept: application/graphql-response+json`을 우선하고 final URL/media/body cap
|
||||
뒤에는 허용 HTTP status의 GraphQL envelope를 bounded decode한 다음 status/body
|
||||
matrix를 교차 검증한다. legacy `application/json`은 별도 profile이다.
|
||||
- public cacheable query의 GET은 별도 threat/cache review 뒤에만 허용한다.
|
||||
- variables는 request schema와 byte/depth/node ceiling을 통과한다.
|
||||
- HTTP status와 GraphQL `data/errors/extensions`를 두 단계로 검증한다.
|
||||
- default `partialDataPolicy=REJECT`; 승인 operation만 typed completeness metadata와
|
||||
함께 partial을 application으로 투영할 수 있다.
|
||||
- error message, path value와 arbitrary extensions를 노출하지 않고 registered
|
||||
safe code/category만 `AppFailure`로 mapping한다.
|
||||
- APQ miss에서 full document를 자동 전송하지 않는다. manifest/version mismatch로
|
||||
fail-closed하고 coherent frontend/router artifact를 복구한다.
|
||||
- batching은 auth, deadline, cancel, observation과 partial failure owner가
|
||||
별도 승인되기 전 `NOT_SELECTED`다.
|
||||
- subscription, `@defer`, `@stream`은 ordinary query adapter에 암묵적으로 넣지
|
||||
않는다.
|
||||
- Apollo/urql normalized cache는 기본 dependency가 아니다. mapped operation
|
||||
result의 memory owner는 TanStack Query다.
|
||||
|
||||
## 10. Browser Protobuf RPC와 REST Gateway 설계 요약
|
||||
|
||||
축과 선택 기준의 상세 계약은
|
||||
[Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)가
|
||||
소유한다. Protobuf는 IDL/serialization, Connect와 gRPC-Web은 browser wire
|
||||
protocol, Connect-Web/official grpc-web은 client runtime, REST Gateway는 HTTP
|
||||
노출 방식이다. 네 이름을 하나의 대안 목록이나 하나의 auto-negotiating
|
||||
executor로 합치지 않는다.
|
||||
|
||||
### 10.1 gRPC-Web
|
||||
|
||||
gRPC-Web 상세 결정은 VD-27이 소유한다. reference target은 unary와 bounded
|
||||
server-stream만 다룬다.
|
||||
|
||||
- checked-in/generated artifact는 pinned proto descriptor/module digest에 묶는다.
|
||||
- vendor generated client/message는 feature adapter 내부에만 존재한다.
|
||||
- fully-qualified service/method와 endpoint는 registry가 고정한다.
|
||||
- client runtime과 gRPC-Web wire-spec revision을 operation/provider에 고정한다.
|
||||
official grpc-web runtime 기준 binary profile은 unary에만 사용하고 server
|
||||
stream은 `grpcwebtext`에 binding한다.
|
||||
- Connect-Web의 `createGrpcWebTransport()`는 Fetch 기반 binary/JSON unary와
|
||||
server stream profile이며 `grpcwebtext` profile이 아니다. official XHR runtime의
|
||||
capability matrix를 이 runtime에 적용하지 않는다. platform-authored custom
|
||||
binary streaming도 세 browser와 actual proxy의 incremental evidence가 있는
|
||||
별도 runtime profile만 허용한다.
|
||||
- frame header, message length, compression flag, total bytes와 frame count를
|
||||
bounded decoder가 검증한다.
|
||||
- HTTP status와 terminal status source를 함께 검사한다. terminal status source는
|
||||
body trailer frame 또는 zero-body trailers-only response header 중 정확히
|
||||
하나이며 중복/충돌을 거절한다.
|
||||
- `grpc-message`, binary error details와 metadata는 allowlist projection 없이
|
||||
application에 반환하지 않는다.
|
||||
- deadline은 `grpc-timeout`과 local total deadline의 더 짧은 값이며
|
||||
AbortSignal이 fetch/stream reader를 cancel한다.
|
||||
- unary `SAFE | IDEMPOTENT | KEYED_COMMAND` 중 provider evidence가 있는
|
||||
operation만 retry한다. stream reconnect는 retry가 아니라
|
||||
server-owned sequence/resume-token을 가진 별도 application protocol이다.
|
||||
- idle deadline, total deadline, max frame/message/count/buffer를 모두 둔다.
|
||||
- client-streaming/bidi는 지원한다고 가장하지 않는다.
|
||||
- Envoy/BFF/Connect/gRPC-Web proxy의 CORS, exposed trailers, content type,
|
||||
auth와 maximum message 설정을 actual provider conformance로 검증한다.
|
||||
- int64/uint64는 JavaScript number로 변환하지 않고 safe integer 범위를
|
||||
증명하거나 decimal string/adapter-private bigint로 mapping한다.
|
||||
|
||||
### 10.2 Connect-Web과 Connect protocol
|
||||
|
||||
Connect 상세 결정은 VD-29가 소유한다.
|
||||
|
||||
- `createConnectTransport()`와 `createGrpcWebTransport()`는 같은 package의 서로
|
||||
다른 wire protocol이다. decoder/status/terminal profile을 공유하지 않는다.
|
||||
- Connect unary의 JSON/binary와 POST를 operation row에 고정한다.
|
||||
- GET은 unary + `NO_SIDE_EFFECTS` descriptor + non-sensitive bounded input +
|
||||
exact URL/cache/CORS profile에서만 별도 승인한다.
|
||||
- Connect server stream은 final EndStream envelope를 확인하기 전 성공이 아니다.
|
||||
- stock runtime의 whole-body decode와 streaming compression 한계를 exact package
|
||||
version evidence로 확인한다. proxy cap이나 custom bounded transport가 없으면
|
||||
production raw-byte ceiling을 완료로 표시하지 않는다.
|
||||
- interceptor의 resolved onion order, auth 이후 final invariant, total deadline,
|
||||
exactly-one retry owner와 cancel handle을 manifest에 고정한다.
|
||||
- `@connectrpc/connect-query`를 기본 도입하지 않는다. generated service/message는
|
||||
adapter-private이고 mapped application value만 기존 TanStack Query에 들어간다.
|
||||
|
||||
### 10.3 Protobuf contract와 REST Gateway
|
||||
|
||||
Protobuf/REST Gateway 상세 결정은 VD-30이 소유한다.
|
||||
|
||||
- authenticated proto/Buf source, descriptor, generator/runtime/plugin version과
|
||||
generated digest를 coherent release artifact로 고정한다.
|
||||
- JSON 노출은 최소 `WIRE_JSON` compatibility를 요구하고 canonical HttpRule
|
||||
route manifest와 generated OpenAPI를 별도 semantic diff한다.
|
||||
- current reference REST envelope에는 curated BFF를 유지한다.
|
||||
- direct grpc-gateway/Envoy transcoder는 ProtoJSON, method/path/query/body,
|
||||
status/error/CORS/cache contract가 그대로 제품 API로 승인된 unary operation에만
|
||||
적용한다.
|
||||
- gateway는 idempotency store, CursorPage snapshot, ETag/304/412, file Range나
|
||||
안전한 domain error vocabulary를 자동 제공하지 않는다.
|
||||
- REST server streaming은 generated gateway의 부수 동작으로 활성화하지 않고
|
||||
framing/terminal/cache를 소유하는 별도 ADR 없이는 `NOT_SELECTED`다.
|
||||
|
||||
## 11. Schema trust boundary
|
||||
|
||||
TypeScript type과 generated code는 compile-time convenience이지 runtime proof가
|
||||
아니다. trust transition은 다음 순서를 지킨다.
|
||||
|
||||
```text
|
||||
untrusted bytes/frames
|
||||
-> bounded transport decoder
|
||||
-> transport envelope/status proof
|
||||
-> operation DTO/message runtime or semantic proof
|
||||
-> ValidatedWireValue (adapter-private)
|
||||
-> boundary mapper
|
||||
-> immutable application projection
|
||||
```
|
||||
|
||||
schema profile은 최소 다음을 고정한다. browser가 직접 집행하는 decoded ceiling과
|
||||
provider/BFF/proxy가 집행하는 wire/encoded ceiling의 owner를 분리한다.
|
||||
|
||||
```text
|
||||
schemaId
|
||||
schemaVersion
|
||||
boundary
|
||||
protocol
|
||||
sourceArtifactId + sourceDigest
|
||||
unknownFieldPolicy
|
||||
providerMaxEncodedBytes
|
||||
maxDecodedBytes
|
||||
maxDepth
|
||||
maxNodes
|
||||
maxStringBytes
|
||||
maxCollectionItems
|
||||
compatibilityPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
unknown-field 기본 정책:
|
||||
|
||||
| 경계 | 정책 |
|
||||
| --- | --- |
|
||||
| request, config, capability, control envelope | `REJECT_UNKNOWN` |
|
||||
| evolvable ordinary response DTO | `STRIP_UNKNOWN` 후 mapper에 전달 |
|
||||
| discriminant/security/authorization 의미를 가진 union | unknown variant 거절 |
|
||||
| unknown data 보존 | adapter 내부 forward proxy가 아닌 한 금지 |
|
||||
|
||||
현재 reference request/path DTO는 `.strict()`로 unknown field를 거절하고,
|
||||
ordinary response DTO는 `.strip()` projection으로 additive server field를
|
||||
cache/domain 경계 밖에 버린다. discriminant/security union을 포함한 다른
|
||||
operation은 각 compatibility profile에 따라 별도로 결정한다.
|
||||
|
||||
## 12. Mapper 경계
|
||||
|
||||
mapper는 transport가 아니라 feature contract가 소유한다.
|
||||
|
||||
```text
|
||||
MapperDefinition
|
||||
mapperId
|
||||
inputSchemaId
|
||||
outputContractId
|
||||
mapperVersion
|
||||
collectionPolicy
|
||||
temporalPolicy
|
||||
numericPolicy
|
||||
nullabilityPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
mapper는 pure, deterministic, side-effect-free이며 다음 union을 반환한다.
|
||||
|
||||
```text
|
||||
MappingResult<T>
|
||||
= { ok: true, value: T }
|
||||
| { ok: false, error: MAPPING_CONTRACT_VIOLATION }
|
||||
```
|
||||
|
||||
현재 reference mapper는 예상 가능한 drift를 throw하지 않고 closed
|
||||
`MappingResult` failure로 반환한다. registry 실행 경계는 예상하지 못한 mapper
|
||||
throw도 fail-closed mapping failure로 바꾸며 raw DTO, value, path와 backend
|
||||
message를 버린다.
|
||||
|
||||
공통 scalar 규칙:
|
||||
|
||||
- opaque ID는 trim/재해석하지 않는 bounded branded string이다.
|
||||
- ISO timestamp는 offset/precision 정책을 검증한 뒤 application instant로
|
||||
변환한다. locale date string과 invalid date normalization을 금지한다.
|
||||
- `int64`, decimal money와 high-precision value는 JSON number로 받지 않는다.
|
||||
- `null`, absent와 empty string/list는 schema와 domain에서 별도 의미로 결정한다.
|
||||
- unknown enum은 domain이 explicit `UNKNOWN`을 소유한 경우만 mapping한다.
|
||||
- collection mapping은 count/byte ceiling 안에서 fail-fast하고 partial array를
|
||||
cache하지 않는다.
|
||||
- mapper는 network, clock, storage, QueryClient, locale formatter와 telemetry를
|
||||
호출하지 않는다.
|
||||
|
||||
## 13. Server State Cache 경계
|
||||
|
||||
TanStack Query는 transport response cache가 아니라 mapped application projection의
|
||||
memory lifecycle owner다.
|
||||
|
||||
cache에 허용:
|
||||
|
||||
- immutable plain application projection
|
||||
- registered query identity로 찾을 수 있는 bounded collection/page
|
||||
- UI가 stale/refresh 상태를 계산하는 library metadata
|
||||
|
||||
cache에 금지:
|
||||
|
||||
- `Response`, raw JSON/GraphQL envelope, generated protobuf message
|
||||
- auth/CSRF/idempotency token, request header와 arbitrary URL
|
||||
- raw ETag, trace/span, backend error/details
|
||||
- File/Blob/stream/native handle
|
||||
- domain service, class instance, function, Promise와 `AbortSignal`
|
||||
|
||||
query profile은 caller가 raw option을 전달하는 대신 registry에서 선택한다.
|
||||
|
||||
```text
|
||||
ServerStateProfileV1
|
||||
profileId
|
||||
queryKeyCodecId
|
||||
scopePersistencePolicyId
|
||||
staleTimeMs
|
||||
gcTimeMs
|
||||
refetchOnFocus
|
||||
refetchOnReconnect
|
||||
networkMode
|
||||
maxResultBytes
|
||||
maxCollectionItems
|
||||
paginationProfileId | null
|
||||
revalidationProfileId | null
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
placeholderPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
query key는 validated/canonical application input과 scope projection으로 만든다.
|
||||
REST URL, GraphQL document/hash, protobuf bytes와 generated message serialization은
|
||||
query key가 아니다. transport 교체가 use-case identity를 바꾸지 않으면 같은
|
||||
application query family를 유지할 수 있지만, old/new representation을 한 cache
|
||||
entry에 shadow write하지 않는다.
|
||||
|
||||
network retry는 transport adapter가 소유하고 Query retry는 기본 `false`다.
|
||||
refresh failure에서 유효한 previous data는 stale-degraded로 유지한다. schema,
|
||||
mapper, scope, authorization와 contract mismatch는 stale data를 계속 노출해도
|
||||
되는지 query profile이 명시해야 하며 기본은 security-sensitive scope에서
|
||||
즉시 숨김/clear다.
|
||||
|
||||
VD-13이 scope/persistence profile, normative key layout, account/session
|
||||
generation과 late-result fence를 소유한다. 이 문서는 그 profile을 exact join하고
|
||||
operation/cache policy, pagination, revalidation과 mutation coherence를 소유한다.
|
||||
|
||||
## 14. Pagination과 conditional revalidation
|
||||
|
||||
cursor pagination은 다음 binding을 갖는다.
|
||||
|
||||
```text
|
||||
PaginationBinding
|
||||
query family fingerprint
|
||||
canonical filters/sort
|
||||
scope fingerprint
|
||||
server snapshot/revision policy
|
||||
page size ceiling
|
||||
opaque next cursor
|
||||
```
|
||||
|
||||
- cursor를 decode하거나 URL로 취급하지 않는다.
|
||||
- `hasMore === (nextCursor !== null)`을 codec에서 강제한다.
|
||||
- single-page cache는 runtime-scoped cursor fingerprint를 semantic key에 포함하고,
|
||||
infinite query만 cursor를 root key에서 제외해 bounded `pageParam`으로 둔다.
|
||||
- max pages/items/estimated bytes를 넘으면 더 불러오지 않는다.
|
||||
- 동일 cursor 반복, loop와 non-progress page를 contract failure로 닫는다.
|
||||
- offset pagination의 insert/delete drift를 자동 deduplicate로 숨기지 않는다.
|
||||
- page merge는 mapper가 보장한 stable identity가 있을 때만 deterministic하다.
|
||||
- previous filters의 page를 새 filter key에 재사용하지 않는다.
|
||||
|
||||
REST `304`는 cached data가 있다는 뜻이 아니라 representation이 바뀌지 않았다는
|
||||
transport 결과다. exact query fingerprint, scope/generation과 cached mapped
|
||||
value에 binding된 validator record가 모두 있을 때만 freshness를 갱신한다.
|
||||
cached value가 없거나 binding이 다르면 unconditional request를 한 번 수행하거나
|
||||
closed failure로 끝낸다.
|
||||
|
||||
GraphQL persisted operation과 gRPC-Web unary는 기본적으로 application-level
|
||||
validator가 없다. backend가 revision을 제공하면 response schema/mapper가 opaque
|
||||
revision을 application revalidation policy로 투영해야 하며 HTTP/gRPC metadata를
|
||||
임의로 ETag처럼 해석하지 않는다.
|
||||
|
||||
## 15. Mutation coherence
|
||||
|
||||
mutation과 query cache는 server commit authority가 아니다.
|
||||
|
||||
```text
|
||||
validate command
|
||||
-> derive logical key + exact command equality/opaque identity token
|
||||
-> runtime/scope coordinator applies concurrency + duplicate admission
|
||||
-> separately acquire invalidation-topic hint-coalescing lease
|
||||
-> cancel exact affected query reads
|
||||
-> capture bounded base cache revision and inverse patch
|
||||
-> install own ordered optimistic layer with revision CAS
|
||||
-> transport derives backend idempotency binding from operation policy
|
||||
-> execute command once with transport-owned retry policy
|
||||
-> success: registered exact seed/compare-and-apply + list/aggregate invalidation
|
||||
-> rejection: remove or invert only own layer with revision CAS
|
||||
-> uncertainty/CAS miss: preserve other commits + invalidate/authoritative refetch
|
||||
-> release invalidation lease + concurrency admission
|
||||
```
|
||||
|
||||
- duplicate submit 정책은 `JOIN_IDENTICAL`, `REJECT_DUPLICATE`,
|
||||
`ALLOW_INDEPENDENT` 중 operation별로 고정하고, 동일성은 전체 validated semantic
|
||||
input의 runtime-private exact equality guard와 opaque identity token으로
|
||||
판정한다.
|
||||
- 같은 hook instance의 Promise dedupe를 server idempotency로 간주하지 않는다.
|
||||
- logical key/identity admission은 local ordering이고 invalidation-topic lease는
|
||||
remote hint coalescing일 뿐이다. 둘 다 backend idempotency authority가 아니다.
|
||||
- optimistic patch는 raw DTO나 generated message를 만들지 않는다.
|
||||
- snapshot item/byte ceiling을 넘으면 optimistic update를 하지 않고 pending UX만
|
||||
제공한다.
|
||||
- 409/GraphQL conflict code/gRPC `ABORTED`는 동일한 safe conflict vocabulary로
|
||||
mapping하되 server revision과 merge policy는 feature use case가 소유한다.
|
||||
- server가 commit한 뒤 local invalidation 실패를 command 실패로 되돌리지 않는다.
|
||||
cache health를 degraded로 기록하고 bounded refetch/recovery를 예약한다.
|
||||
- command의 자동 protocol failover는 중복 side effect 위험 때문에 금지한다.
|
||||
|
||||
## 16. Server streaming과 cache
|
||||
|
||||
gRPC-Web server stream, GraphQL subscription과 incremental delivery는 ordinary
|
||||
queryFn과 다르다.
|
||||
|
||||
현재 installed API operation registry는 terminal REST operation만 소유한다.
|
||||
GraphQL HTTP와 gRPC-Web unary/server-stream은 각 reference adapter가 구현될 때
|
||||
protocol discriminant와 전용 binding으로 확장한다. SSE, WebSocket과 Web Push는
|
||||
VD-28 realtime registry가 소유하며 bounded polling은 registered terminal REST
|
||||
`QUERY`의 scheduling policy이지 새 protocol이나 automatic failover가 아니다.
|
||||
GraphQL `@defer`/`@stream`은 선택될 경우에도 한 HTTP operation의 finite
|
||||
incremental response이며 subscription과 같은 장기 realtime stream이 아니다.
|
||||
|
||||
```text
|
||||
ServerStreamPort
|
||||
open(frozen request, signal)
|
||||
-> AsyncIterable<Result<MappedEvent>>
|
||||
-> close()
|
||||
```
|
||||
|
||||
- frame/event마다 schema, mapper, scope와 generation을 재검증한다.
|
||||
- sequence, duplicate, gap과 resume token은 backend application protocol이다.
|
||||
- bounded queue, high-water mark, overflow, idle/total deadline을 선언한다.
|
||||
- stream event는 registered reducer로 immutable snapshot을 만들거나 query
|
||||
invalidation hint만 발행한다.
|
||||
- partial event를 ordinary query success로 cache하지 않는다.
|
||||
- stream 종료/재connect를 TanStack Query retry로 처리하지 않는다.
|
||||
|
||||
GraphQL subscription은 현재 `NOT_SELECTED`이고, gRPC-Web server-stream은
|
||||
`DESIGNED_NOT_IMPLEMENTED`다.
|
||||
|
||||
gRPC-Web `ServerStreamPort`는 operation-bound outbound stream result일 수 있다.
|
||||
API adapter가 protobuf frame/message decode, semantic schema와 boundary mapper를
|
||||
끝낸 뒤 제품이 runtime-wide notification projection을 명시적으로 선택한
|
||||
branch에서만 mapped event를 VD-28 common coordinator/`FeatureEventInput`에
|
||||
전달한다. protobuf를 `REALTIME_EVENT_V1` JSON으로 감싸지 않고, 첫 event 전
|
||||
opening replay와 이후 resume 규칙은 VD-27이 계속 소유한다.
|
||||
|
||||
## 17. Backend/provider 계약
|
||||
|
||||
구현 owner, 권장 topology, 현재 reference endpoint/envelope, idempotency store,
|
||||
Cursor/ETag/revision과 protocol별 handoff checklist는
|
||||
[Backend API와 Server State contract](./backend-api-and-server-state-contract.md)가
|
||||
소유한다. 아래 표는 frontend 설계가 요구하는 경계 요약이다.
|
||||
|
||||
| 경계 | backend/provider가 제공할 계약 |
|
||||
| --- | --- |
|
||||
| 공통 | authorization, contract version/artifact compatibility, encoded transfer와 decoded payload의 bounded owner, stable error code, correlation/trace projection, idempotency와 rate-limit semantics |
|
||||
| REST | exact method/path/media/status/envelope, CSRF strategy, idempotency retention, cursor binding, ETag/If-None-Match 또는 revision, retry-safe status와 CORS/cache policy |
|
||||
| GraphQL | schema registry, persisted-operation manifest, allowlist/cost/depth enforcement, safe error extension vocabulary, operation retirement과 N/N-1 router rollout |
|
||||
| Connect | proto/descriptor/codegen source, exact Connect-Web runtime/encoding/method, EndStream/status/error, timeout/cancel/compression/CORS와 browser/server conformance |
|
||||
| gRPC-Web | proto/descriptor source, Buf/protoc compatibility policy, gRPC-Web proxy, exact service/method, message/frame ceiling, status/trailer/CORS exposure와 stream resume protocol |
|
||||
| Protobuf REST Gateway | selected gateway kind/version, HttpRule/ProtoJSON/OpenAPI artifact, path/query/body/status/error/header mapping, edge→upstream cancellation과 N/N-1 conformance |
|
||||
| Schema | authenticated source artifact, additive/breaking classification, deprecation window, fixtures and source digest |
|
||||
| Mapper | domain meaning, temporal/numeric/null/enum semantics와 stable identity |
|
||||
| Cache | revision/conflict/idempotency/invalidation semantics; frontend TTL은 authorization 대체가 아님 |
|
||||
|
||||
frontend가 제공하는 Zod schema, generated type과 cache invalidation은 backend
|
||||
authorization, validation, idempotency와 conflict resolution을 대체하지 않는다.
|
||||
|
||||
## 18. Security와 privacy
|
||||
|
||||
- API base/GraphQL/Connect/gRPC-Web endpoint는 HTTPS registry ID로 고정한다.
|
||||
- caller가 URL, header, GraphQL document, service/method와 metadata를 제출하지
|
||||
못한다.
|
||||
- auth owner가 credential을 붙이고 application/query/cache에는 token을 노출하지
|
||||
않는다.
|
||||
- credential attachment 뒤 URL/method/origin/body digest와 registry-owned header
|
||||
binding을 재검증한다. auth integration unavailable 상태에서 request를 보내지
|
||||
않는다.
|
||||
- cookie session의 unsafe request는 CSRF token/header 또는 same-site BFF 정책을
|
||||
operation profile과 provider conformance로 증명한다.
|
||||
- GET/GraphQL variables에 sensitive filter를 넣는 operation은 별도 review 없이
|
||||
만들지 않는다.
|
||||
- response byte/depth/node/string/collection/frame cap으로 resource exhaustion을
|
||||
막는다.
|
||||
- GraphQL cost/depth와 gRPC message cap은 server/proxy에서도 강제한다.
|
||||
- mapper와 cache는 prototype/accessor/class/native object를 받아들이지 않는다.
|
||||
- PII/business ID를 query key, diagnostics label, persisted cache physical key에
|
||||
직접 넣지 않는다. 필요한 identity는 opaque partition/token policy를 쓴다.
|
||||
- command identity token/logical mutation key는 runtime-local control data이며
|
||||
diagnostics, cross-context wire와 persistence에 넣지 않는다.
|
||||
- raw request/response, GraphQL variables/errors, protobuf bytes/metadata, ETag,
|
||||
cursor, idempotency key와 validation value를 관측 데이터에 넣지 않는다.
|
||||
|
||||
## 19. Observability
|
||||
|
||||
허용된 bounded aggregate:
|
||||
|
||||
- operation registry ID, protocol, semantics
|
||||
- outcome/error kind, HTTP status group 또는 gRPC status code allowlist
|
||||
- GraphQL full/partial/rejected outcome
|
||||
- attempt/deadline/duration/encoded-byte/result-item/frame bucket
|
||||
- schema/mapper profile ID와 compatibility outcome
|
||||
- query hit/miss/stale/refetch/eviction bucket
|
||||
- mutation optimistic/rollback/conflict/invalidation outcome
|
||||
- provider/browser/runtime version의 low-cardinality bucket
|
||||
|
||||
금지:
|
||||
|
||||
- URL/path parameter/search/body/header
|
||||
- GraphQL document, variables, response path와 raw error message
|
||||
- protobuf message/metadata/trailer raw value
|
||||
- resource/account/tenant ID, cursor, validator, digest와 cache value
|
||||
|
||||
하나의 logical operation은 terminal observation 하나를 만든다. attempt span은
|
||||
sampling된 내부 detail로만 남기며 terminal success/failure count를 중복시키지
|
||||
않는다.
|
||||
|
||||
## 20. Failure와 fallback
|
||||
|
||||
| 실패 | 기본 결과 |
|
||||
| --- | --- |
|
||||
| registry/schema/mapper 누락 | network 전 fail-closed, operation traffic disable |
|
||||
| incompatible contract artifact | product mount 또는 해당 capability admission 차단 |
|
||||
| response cap/decode/schema mismatch | body/reader cancel, cache write 금지, provider incompatibility |
|
||||
| mapper violation | cache write 금지, safe contract failure |
|
||||
| REST retry exhaustion | stale 허용 profile만 previous data 유지 |
|
||||
| GraphQL persisted operation missing | full document fallback 금지, coherent artifact rollback |
|
||||
| GraphQL partial data | default reject; explicit profile만 completeness와 함께 사용 |
|
||||
| Connect missing/duplicate EndStream 또는 oversize whole body | call cancel, cache write 금지, provider/runtime incompatible |
|
||||
| gRPC-Web proxy/trailer mismatch | reader cancel, provider unavailable/incompatible |
|
||||
| REST Gateway HttpRule/OpenAPI/runtime drift | affected route admission 차단, coherent gateway artifact rollback |
|
||||
| server stream gap/overflow | snapshot 폐기 또는 authoritative refetch |
|
||||
| account/generation mismatch | late result 폐기, old-scope cache write 금지 |
|
||||
| invalidation failure after commit | command 성공 유지, cache degraded + recovery refetch |
|
||||
|
||||
GraphQL, Connect 또는 gRPC-Web failure를 REST로 자동 전환하지 않는다. 사전에 등록된
|
||||
read-only shadow/fallback operation이 있고 동일 authorization/mapper/result
|
||||
contract를 conformance suite로 증명한 경우만 selector가 새 logical query를
|
||||
시작할 수 있다.
|
||||
|
||||
## 21. Rollout과 removal
|
||||
|
||||
```text
|
||||
ADR + registry schema accepted
|
||||
-> deterministic codec/schema/mapper fixture
|
||||
-> provider-neutral adapter and fake
|
||||
-> negative boundary/removal gate
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> product/provider/operation selection
|
||||
-> bootstrap composition behind TrafficAdmission=DISABLED
|
||||
-> COMPOSED
|
||||
-> shadow/read-only conformance
|
||||
-> browser/provider/operations evidence
|
||||
-> PromotionEvidence=COMPLETE
|
||||
-> CANARY
|
||||
-> ENABLED
|
||||
```
|
||||
|
||||
REST v2 local baseline은 installed reference operation에 연결됐다. 실제 provider
|
||||
traffic은 operation/profile 단위의 conformance와 canary를 거쳐야 하며,
|
||||
conditional/pagination은 backend 계약 없이 enabled하지 않는다.
|
||||
GraphQL/Connect/gRPC-Web/codegen/gateway dependency는 실제 selected operation이
|
||||
없으면 production inventory에 없어야 한다.
|
||||
|
||||
removal:
|
||||
|
||||
1. 신규 operation admission을 닫는다.
|
||||
2. query는 cancel하고 command/stream은 bounded drain 또는 explicit abort한다.
|
||||
3. 해당 invalidation listener, auth attachment와 provider를 close한다.
|
||||
4. current scope의 mapped memory cache를 clear한다.
|
||||
5. operation/schema/mapper/query profile과 generated artifact를 제거한다.
|
||||
6. dependency, config, proxy route, test fixture와 production module inventory가
|
||||
함께 제거됐음을 증명한다.
|
||||
7. backend persisted-operation/method retirement은 N/N-1 client window 뒤에 한다.
|
||||
|
||||
## 22. Test와 promotion evidence
|
||||
|
||||
### Deterministic
|
||||
|
||||
- operation registry closed union/reference/orphan/duplicate
|
||||
- request canonicalization과 query-key identity
|
||||
- timeout/total deadline/retry/idempotency/auth recovery
|
||||
- response byte/depth/node/collection cap
|
||||
- schema unknown-field, scalar, null/enum/numeric/date matrix
|
||||
- mapper success/failure/no raw value leakage
|
||||
- query stale/gc/refetch/pagination/mutation/rollback/generation fence
|
||||
|
||||
### Contract
|
||||
|
||||
- REST OpenAPI/envelope/status/media/cursor/conditional/idempotency
|
||||
- GraphQL schema + persisted manifest + variables/result/error/partial policy
|
||||
- proto descriptor + breaking check + gRPC status/trailer/frame fixture
|
||||
- Connect unary/stream JSON/binary/GET/EndStream/CORS/deadline fixture
|
||||
- HttpRule route manifest + ProtoJSON/OpenAPI/status/error/header mapping fixture
|
||||
- 같은 fixture를 fake, emulator/staging과 actual provider에 실행
|
||||
|
||||
### Browser/integration
|
||||
|
||||
- bootstrap → feature → transport → schema → mapper → Query → UI
|
||||
- AbortSignal/navigation/logout/account switch
|
||||
- CORS/cookie/CSRF/redirect/content-encoding
|
||||
- HTTP/2/proxy/CDN/Connect EndStream/gRPC-Web trailer behavior
|
||||
- offline/reconnect/focus와 stale-degraded UI
|
||||
|
||||
### Fault
|
||||
|
||||
- truncated/oversize/malformed response
|
||||
- slow credential/network/body/schema/mapper phase
|
||||
- 401 recovery, 429, retry exhaustion과 total deadline
|
||||
- GraphQL partial/error/persisted-operation drift
|
||||
- Connect missing/early/duplicate EndStream, whole-body overflow와 compression mismatch
|
||||
- gRPC missing/conflicting terminal status source, corrupt/compressed/oversize
|
||||
frame와 stream gap
|
||||
- REST Gateway route/OpenAPI/runtime rewrite drift와 abort propagation loss
|
||||
- late response, duplicate mutation, optimistic rollback과 invalidation failure
|
||||
|
||||
### Operations
|
||||
|
||||
- operation kill switch
|
||||
- contract artifact N/N-1 rollout과 rollback
|
||||
- provider incompatibility containment
|
||||
- cache scope reset와 stale-data decision
|
||||
- generated client/GraphQL/Connect/gRPC-Web/REST Gateway removal drill
|
||||
|
||||
fake와 generated compile success만으로 actual provider, browser나 operations
|
||||
evidence를 `COMPLETE`로 표시하지 않는다.
|
||||
|
||||
## 23. 설계 우선 work package
|
||||
|
||||
| package | 목표 |
|
||||
| --- | --- |
|
||||
| API-01 | REST v2 operation registry, total deadline, bounded decoder와 conditional/pagination contract |
|
||||
| API-02 | multi-protocol schema artifact/digest governance와 typed Mapper result |
|
||||
| API-03 | strict ServerStateProfile, query-key codec, pagination/revalidation와 mutation policy |
|
||||
| API-04 | persisted-operation-only GraphQL reference adapter와 conformance harness |
|
||||
| API-05 | gRPC-Web unary/server-stream reference adapter와 proxy harness |
|
||||
| API-06 | Connect-Web unary/server-stream reference adapter와 provider harness |
|
||||
| API-07 | Protobuf governance와 selected REST Gateway conformance |
|
||||
| API-08 | atomic composition, readiness, kill switch, runbook와 provider/browser evidence |
|
||||
|
||||
권장 순서는 API-01 → API-02 → API-03이다. API-04~07은 제품 선택과
|
||||
backend/provider 계약이 생긴 branch만 독립적으로 시작한다. GraphQL, Connect,
|
||||
gRPC-Web과 REST Gateway를 “미래 대비” 목적으로 모두 기본 bundle에 설치하지
|
||||
않는다.
|
||||
|
||||
## 24. 완료 기준
|
||||
|
||||
- [ ] 모든 installed operation은 protocol/schema/mapper/cache/error/deadline owner가 있다.
|
||||
- [ ] application/presentation public type에 DTO, GraphQL SDK와 generated protobuf가 없다.
|
||||
- [ ] untrusted byte부터 mapped projection까지 모든 ceiling과 trust transition이 닫혀 있다.
|
||||
- [ ] query key와 실제 request input이 동일 canonical source에서 파생된다.
|
||||
- [ ] transport retry와 Query retry가 중복되지 않는다.
|
||||
- [ ] command idempotency, optimistic patch와 conflict/invalidation owner가 명시돼 있다.
|
||||
- [ ] account/logout/release generation 뒤 late result가 cache에 들어가지 않는다.
|
||||
- [ ] actual REST/GraphQL/Connect/gRPC-Web/Gateway provider에 같은 semantic
|
||||
conformance fixture를 실행한다.
|
||||
- [ ] contract drift, kill switch, rollback과 optional dependency removal drill이 통과한다.
|
||||
- [ ] raw payload/URL/document/message/metadata/validator가 log와 cache에 없다.
|
||||
- [ ] `COMPOSED`와 production-ready/provider-conformant를 같은 의미로 쓰지 않는다.
|
||||
|
||||
## 25. 관련 문서
|
||||
|
||||
- [Frontend ports, adapters, and boundaries](./frontend-ports-adapters-and-boundaries.md)
|
||||
- [Client cache and browser storage](./client-cache-and-storage.md)
|
||||
- [VD-13 Client cache scope와 persistence](./decisions/VD-13-client-cache-scope-and-persistence.md)
|
||||
- [Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)
|
||||
- [VD-29 Connect-Web와 browser Protobuf runtime](./decisions/VD-29-connect-web-and-browser-protobuf-runtime.md)
|
||||
- [VD-30 Protobuf contract와 REST Gateway](./decisions/VD-30-protobuf-contract-and-rest-gateway.md)
|
||||
- [Contract compatibility](../contracts/compatibility.md)
|
||||
- [Frontend platform testing strategy](../testing/frontend-platform-testing-strategy.md)
|
||||
@@ -1,718 +0,0 @@
|
||||
# Backend API와 Server State handoff contract
|
||||
|
||||
> **정본 안내 (non-authoritative for runtime capability decisions)**
|
||||
>
|
||||
> Runtime Config/boot, Fetch HTTP client, Router, Query/Mutation, realtime 공통 경계, Web Worker,
|
||||
> Service Worker, offline command와 Background Sync의 구현 결정은
|
||||
> [프론트엔드 런타임 Capability 저장소 정합형 구현 결정 폐쇄 상세 설계](./2026-07-30-frontend-runtime-capability-repository-aligned-implementation-closed-deep-design.md)가 정본이다.
|
||||
> 본 문서의 해당 서술이 정본과 충돌하면 정본을 따른다.
|
||||
|
||||
|
||||
- 상태: frontend handoff design accepted, backend implementation/evidence pending
|
||||
- 기준일: 2026-07-28
|
||||
- 대상: Web API/BFF, application service, persistence, identity, GraphQL router,
|
||||
Connect/gRPC-Web gateway, Protobuf REST Gateway와 운영 owner
|
||||
- frontend 기준:
|
||||
[API contract, Schema, Mapper와 Server State](./api-contract-schema-mapper-and-server-state.md)
|
||||
- browser Protobuf/gateway 기준:
|
||||
[Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)
|
||||
- 파일 전송 backend 기준:
|
||||
[Server file capability infrastructure](./server-file-capability-infrastructure.md)
|
||||
- 복구 절차:
|
||||
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
|
||||
|
||||
## 1. 문서의 경계
|
||||
|
||||
이 문서는 이 frontend template이 실제 제품 backend와 연결될 때 backend가
|
||||
제공해야 하는 구조, wire contract, 상태 의미와 운영 증거를 정의한다. 특정
|
||||
언어·framework·cloud 제품을 강제하지 않는다. Spring, Nest/Fastify, Go,
|
||||
.NET 또는 다른 stack을 사용해도 아래 불변조건은 동일하다.
|
||||
|
||||
이 repository에는 backend source, database migration, identity provider,
|
||||
GraphQL schema/router, protobuf descriptor, Connect/gRPC-Web runtime/proxy,
|
||||
REST transcoder와 실제 provider evidence가 없다. 따라서 이 문서는 backend 구현
|
||||
완료 증거가 아니다.
|
||||
|
||||
파일 업로드·다운로드, object storage, presigned URL, multipart와 Image CDN은
|
||||
별도 server file 문서가 소유한다. 이 문서는 ordinary REST/GraphQL/Connect/
|
||||
gRPC-Web application API, Protobuf REST Gateway와 frontend Server State 계약만
|
||||
소유한다.
|
||||
|
||||
## 2. 권장 논리 구조
|
||||
|
||||
```text
|
||||
Browser
|
||||
-> CDN / reverse proxy / WAF
|
||||
-> Browser-facing Web API or BFF
|
||||
-> authentication + authorization
|
||||
-> exact operation registry
|
||||
-> request schema / byte / rate limit
|
||||
-> REST controller
|
||||
-> optional persisted GraphQL router
|
||||
-> optional Connect browser RPC gateway
|
||||
-> optional gRPC-Web gateway
|
||||
-> optional Protobuf REST transcoder
|
||||
-> application service
|
||||
-> command transaction
|
||||
-> query/read-model service
|
||||
-> idempotency coordinator
|
||||
-> revision/validator owner
|
||||
-> outbox/event owner
|
||||
-> primary database
|
||||
-> idempotency store
|
||||
-> read replica/read model
|
||||
-> event broker when selected
|
||||
```
|
||||
|
||||
Browser-facing contract owner와 내부 service contract owner를 분리한다.
|
||||
|
||||
- Browser API/BFF는 CORS, cookie/CSRF 또는 bearer, public DTO, envelope,
|
||||
body ceiling, status/media와 redaction을 소유한다.
|
||||
- Application service는 authorization 재검사, transaction, idempotency,
|
||||
conflict, revision과 domain invariant를 소유한다.
|
||||
- Persistence adapter는 SQL/NoSQL/Redis vendor type, row version과 cursor
|
||||
implementation을 외부 DTO에 노출하지 않는다.
|
||||
- GraphQL router, Connect/gRPC-Web gateway와 REST transcoder는 선택 adapter다.
|
||||
다른 protocol로 임의 fallback하거나 frontend에 내부 service address를
|
||||
노출하지 않는다.
|
||||
|
||||
작은 제품은 이 논리 모듈을 하나의 deployable로 구현할 수 있다. deployable을
|
||||
나누는 것보다 transaction/idempotency/authorization owner가 하나로 명확한지가
|
||||
우선이다.
|
||||
|
||||
## 3. 공통 contract artifact
|
||||
|
||||
Backend와 frontend release는 다음 bounded contract set을 공유한다.
|
||||
|
||||
```text
|
||||
ApiContractSetV1
|
||||
globalApiContractVersion
|
||||
restArtifactId + digest
|
||||
runtimeSchemaManifestId + digest
|
||||
mapperSemanticManifestId + digest
|
||||
errorVocabularyVersion
|
||||
minimumFrontendVersion
|
||||
minimumBackendVersion
|
||||
effectiveAt
|
||||
retirementEpoch | null
|
||||
|
||||
optional:
|
||||
graphqlSchemaId + digest
|
||||
persistedGraphqlManifestId + digest
|
||||
protobufDescriptorId + digest
|
||||
protobufSourceOrModuleId + digest
|
||||
protobufCodegenProfileId
|
||||
connectProviderProfileId
|
||||
grpcWebProviderProfileId
|
||||
protobufRestGatewayProfileId
|
||||
httpRuleArtifactId + digest
|
||||
protoJsonProfileId
|
||||
gatewayOpenApiArtifactId + digest
|
||||
```
|
||||
|
||||
최소 산출물:
|
||||
|
||||
- authenticated OpenAPI 또는 동등한 REST schema source
|
||||
- exact status/media/envelope fixture
|
||||
- stable error code vocabulary
|
||||
- request/response byte와 collection ceiling
|
||||
- scalar/date/null/enum 의미
|
||||
- N/N-1 compatibility 결과
|
||||
- backend build와 frontend release가 참조하는 immutable digest
|
||||
|
||||
runtime config의 version 문자열 일치만 compatibility 증거로 사용하지 않는다.
|
||||
artifact digest가 없는 동안에는 실제 staging conformance fixture와 수동 승인
|
||||
evidence가 필요하다.
|
||||
|
||||
## 4. 현재 reference REST 계약
|
||||
|
||||
현재 frontend에 실제 조립된 operation은 다음 세 개다.
|
||||
|
||||
| operation | request | success |
|
||||
| --- | --- | --- |
|
||||
| `LIST_REFERENCE_RESOURCES` | `GET /api/reference-resources?cursor&limit&tags` | `200 application/json` |
|
||||
| `GET_REFERENCE_RESOURCE` | `GET /api/reference-resources/{resourceId}` | `200 application/json` |
|
||||
| `CREATE_REFERENCE_RESOURCE` | `POST /api/reference-resources` | `200` 또는 `201 application/json` |
|
||||
|
||||
현재 list payload는 `ReferenceResource[]`다. `cursor` 입력이 존재하더라도
|
||||
`CursorPage` 출력 계약은 아직 아니다. backend가 같은 operation에서 배열을
|
||||
page object로 조용히 바꾸면 schema mismatch로 실패한다.
|
||||
|
||||
resource DTO:
|
||||
|
||||
```text
|
||||
ReferenceResourceDtoV1
|
||||
id: non-empty string, maximum 120 characters
|
||||
name: non-empty string, maximum 240 characters
|
||||
createdAt?: RFC 3339 date-time
|
||||
```
|
||||
|
||||
create command:
|
||||
|
||||
```text
|
||||
CreateReferenceResourceCommandV1
|
||||
name: trimmed string, 1..120
|
||||
note?: trimmed string, 0..500
|
||||
```
|
||||
|
||||
Backend는 frontend validation을 신뢰하지 않고 동일하거나 더 좁은 validation과
|
||||
authorization을 다시 수행한다.
|
||||
|
||||
### 4.1 JSON envelope
|
||||
|
||||
모든 현재 JSON success/failure는 다음 envelope를 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {},
|
||||
"meta": {
|
||||
"requestId": "server-request-id",
|
||||
"traceId": "server-trace-id",
|
||||
"correlationId": "client-correlation-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "STABLE_MACHINE_CODE",
|
||||
"category": "optional-safe-category",
|
||||
"message": "optional non-sensitive copy",
|
||||
"retryable": false,
|
||||
"details": {}
|
||||
},
|
||||
"meta": {
|
||||
"requestId": "server-request-id",
|
||||
"traceId": "server-trace-id",
|
||||
"correlationId": "client-correlation-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Envelope 최상위 unknown field는 현재 거절된다. ordinary resource DTO의 unknown
|
||||
field는 frontend schema에서 strip되지만 additive compatibility는 contract
|
||||
review와 fixture를 먼저 통과해야 한다.
|
||||
|
||||
`requestId`, `traceId`, `correlationId`는 각각 1..128 범위의 안전한 opaque
|
||||
identifier다. credential, user data, cursor, validator와 database key를
|
||||
identifier에 encode하지 않는다.
|
||||
|
||||
### 4.2 Status와 error
|
||||
|
||||
| HTTP status | 의미 |
|
||||
| --- | --- |
|
||||
| `400` | malformed request 또는 closed request contract 위반 |
|
||||
| `401` | 인증 없음/만료. 이미 적용된 command를 401로 반환하지 않음 |
|
||||
| `403` | authenticated principal에게 권한 없음 |
|
||||
| `404` | authorization 정책상 공개 가능한 not-found |
|
||||
| `409` | idempotency fingerprint, domain revision 또는 semantic conflict |
|
||||
| `412` | selected conditional write의 `If-Match` precondition 실패 |
|
||||
| `422` | field validation. bounded `details.issues[]`만 허용 |
|
||||
| `429` | rate limit. 유효한 `Retry-After`와 operation 정책 제공 |
|
||||
| `500/502/503/504` | server/provider failure. command effect certainty 별도 |
|
||||
|
||||
현재 frontend의 ordinary status mapper는 `412` 전용 처리를 아직 연결하지
|
||||
않았다. conditional mutation을 선택할 때 frontend failure vocabulary와
|
||||
transaction을 함께 승격해야 한다.
|
||||
|
||||
Backend `error.code`는 machine-readable stable code다. stack, SQL/vendor error,
|
||||
raw validation value, authorization reason과 내부 service address를 반환하지
|
||||
않는다.
|
||||
|
||||
## 5. 인증, CSRF와 CORS
|
||||
|
||||
현재 reference operation은 다음 profile로 조립돼 있다.
|
||||
|
||||
```text
|
||||
auth = external bearer
|
||||
Authorization: Bearer <credential>
|
||||
fetch credentials = omit
|
||||
CSRF profile = none
|
||||
redirect = error
|
||||
referrer policy = no-referrer
|
||||
```
|
||||
|
||||
Backend/BFF는 bearer의 issuer, audience, signature algorithm, time claims와
|
||||
revocation/session policy를 검증하고 operation별 authorization을 적용한다.
|
||||
401과 403을 구분하며 frontend cache를 authorization authority로 사용하지 않는다.
|
||||
|
||||
쿠키 session으로 전환할 경우 같은 profile로 간주하지 않는다. 별도
|
||||
`SAME_ORIGIN_COOKIE` profile에 다음을 함께 승인한다.
|
||||
|
||||
- `Secure`, `HttpOnly`, 명시적 `SameSite`와 host/path scope
|
||||
- unsafe method의 CSRF token/header와 Origin/Sec-Fetch-Site 검증
|
||||
- credentialed CORS에서 wildcard origin 금지
|
||||
- login/logout/session rotation과 cache generation 전환
|
||||
- session fixation, token rotation과 concurrent tab 동작
|
||||
|
||||
Cross-origin bearer provider 최소 CORS:
|
||||
|
||||
- exact allow-origin 목록과 bounded preflight cache
|
||||
- `Authorization`, `Content-Type`, `Idempotency-Key`,
|
||||
`X-Correlation-ID`, 향후 `If-None-Match`, `If-Match` 허용
|
||||
- 필요한 경우 `ETag`, `Retry-After`, request/trace header만 expose
|
||||
- redirect login page, HTML error body와 wildcard credential 금지
|
||||
|
||||
## 6. Command와 idempotency
|
||||
|
||||
`CREATE_REFERENCE_RESOURCE`는 keyed command다. frontend memory single-flight는
|
||||
backend idempotency를 대체하지 않는다.
|
||||
|
||||
idempotency identity:
|
||||
|
||||
```text
|
||||
principal/tenant
|
||||
+ semantic operation ID and contract version
|
||||
+ Idempotency-Key
|
||||
+ canonical request fingerprint
|
||||
```
|
||||
|
||||
권장 record:
|
||||
|
||||
```text
|
||||
IdempotencyRecord
|
||||
principalFingerprint
|
||||
operationId
|
||||
contractVersion
|
||||
idempotencyKeyHash
|
||||
requestFingerprint
|
||||
state = IN_PROGRESS | COMMITTED | FAILED_SAFE | EFFECT_UNKNOWN
|
||||
responseStatus
|
||||
responseEnvelopeReference
|
||||
resourceRevision | null
|
||||
leaseOwner + leaseExpiry
|
||||
retentionExpiry
|
||||
createdAt + completedAt
|
||||
```
|
||||
|
||||
불변조건:
|
||||
|
||||
- claim과 command transaction의 관계가 원자적이거나 crash reconciliation
|
||||
가능해야 한다.
|
||||
- 같은 key와 같은 fingerprint replay는 같은 authoritative receipt를 반환한다.
|
||||
- 같은 key와 다른 fingerprint는 `409 IDEMPOTENCY_KEY_REUSED`다.
|
||||
- concurrent replay는 하나만 실행하고 나머지는 같은 result를 기다리거나
|
||||
bounded `IN_PROGRESS` 결과를 받는다.
|
||||
- commit 뒤 response 유실은 새 resource를 만들지 않는다.
|
||||
- `EFFECT_UNKNOWN`은 새 key로 자동 재시도하지 않고 status/reconcile endpoint로
|
||||
확인한다.
|
||||
- retention은 frontend retry/recovery 최대 window보다 길고 quota/abuse limit이
|
||||
있다.
|
||||
- key 원문과 request body를 log/metric label에 넣지 않는다.
|
||||
|
||||
Database unique constraint 또는 durable compare-and-set이 최종 중복 방지
|
||||
authority여야 한다. process-local map/lock만 사용하지 않는다.
|
||||
|
||||
## 7. Cursor pagination과 snapshot
|
||||
|
||||
Backend가 pagination을 선택할 때 새 response schema/operation version으로 다음
|
||||
contract를 제공한다.
|
||||
|
||||
```text
|
||||
CursorPage<T>
|
||||
items: T[]
|
||||
nextCursor: opaque string | null
|
||||
hasMore: boolean
|
||||
snapshotToken: opaque string | null
|
||||
```
|
||||
|
||||
필수 불변조건:
|
||||
|
||||
- `hasMore === (nextCursor !== null)`
|
||||
- 동일 chain의 `snapshotToken`은 모든 page에서 동일
|
||||
- cursor는 principal/tenant, filter, sort, contract version과 snapshot에 binding
|
||||
- cursor는 opaque, 무결성 보호, 만료와 key rotation 정책 보유
|
||||
- offset이 아니라 stable keyset ordering 사용
|
||||
- total order의 마지막 tie-breaker는 immutable unique ID
|
||||
- deleted/inserted row가 duplicate/gap을 만드는 의미를 snapshot 정책으로 결정
|
||||
- empty page인데 `hasMore=true`인 sparse page 허용 여부를 operation profile에 고정
|
||||
- cursor 최대 encoded byte, page size와 total scan/cost ceiling을 server도 강제
|
||||
- invalid, expired, wrong-principal, wrong-filter cursor의 safe error code를 고정
|
||||
|
||||
권장 query ordering 예:
|
||||
|
||||
```text
|
||||
ORDER BY created_at DESC, resource_id DESC
|
||||
cursor payload = version + snapshot watermark + last(created_at, resource_id)
|
||||
+ filter digest + principal/tenant binding + expiry
|
||||
```
|
||||
|
||||
Cursor 원문은 log, trace, analytics와 frontend persistent storage에 넣지 않는다.
|
||||
|
||||
### 7.1 배열에서 page로의 migration
|
||||
|
||||
1. `ReferenceResourceListPagePayloadV2` schema와 새 operation/version을 추가한다.
|
||||
2. backend가 N/N-1 동안 기존 배열과 page contract를 동시에 제공한다.
|
||||
3. frontend가 cursor runtime을 새 bound query/infinite query에 연결한다.
|
||||
4. loop/snapshot/ceiling/abort conformance를 staging에서 검증한다.
|
||||
5. 새 operation을 canary한 뒤 기존 배열 operation을 retirement한다.
|
||||
|
||||
동일 media/status에서 payload shape만 바꾸는 in-place migration은 금지한다.
|
||||
|
||||
## 8. Conditional read와 revision/CAS
|
||||
|
||||
### 8.1 Read validator
|
||||
|
||||
Backend가 application-managed revalidation을 선택하면 exact mapped
|
||||
representation마다 ETag를 제공한다.
|
||||
|
||||
```text
|
||||
GET without validator
|
||||
-> 200 + JSON envelope + ETag
|
||||
|
||||
GET with If-None-Match
|
||||
-> representation unchanged: 304 + empty body
|
||||
-> changed: 200 + JSON envelope + new ETag
|
||||
```
|
||||
|
||||
불변조건:
|
||||
|
||||
- validator는 principal/tenant, authorization-visible representation,
|
||||
response schema/mapper semantics와 encoding variant에 binding
|
||||
- weak/strong 선택을 operation profile에 고정
|
||||
- user-private response를 shared CDN/public cache에 저장하지 않음
|
||||
- cross-origin이면 `ETag`를 expose하고 `If-None-Match`를 preflight 허용
|
||||
- 304에는 JSON success envelope를 넣지 않음
|
||||
- validator 원문을 log/metric/diagnostics에 넣지 않음
|
||||
- `Vary`와 `Cache-Control` owner를 명확히 하고 browser HTTP cache와
|
||||
TanStack/application revalidation이 서로 다른 value owner가 되지 않게 함
|
||||
|
||||
Frontend는 validator와 mapped cache value의 scope, query identity,
|
||||
representation version과 cache revision이 모두 일치할 때만 304를 success로
|
||||
받는다. cache value가 없으면 unconditional refetch 또는 safe failure로 닫는다.
|
||||
|
||||
### 8.2 Conditional command
|
||||
|
||||
수정/삭제 command가 선택되면 DTO에 opaque domain `revision`을 추가하고:
|
||||
|
||||
```text
|
||||
If-Match: "<revision validator>"
|
||||
```
|
||||
|
||||
를 요구한다. 일치하지 않으면 `412` 또는 승인된 `409` contract 하나만
|
||||
사용한다. frontend optimistic layer의 commit/rollback은 backend revision
|
||||
authority를 대체하지 않는다.
|
||||
|
||||
## 9. Optimistic mutation을 위한 backend 의미
|
||||
|
||||
Frontend ordered optimistic layer runtime은 구현돼 있지만 제품 operation에
|
||||
연결하려면 backend가 다음을 결정해야 한다.
|
||||
|
||||
- resource/list membership을 결정하는 canonical filter와 sort
|
||||
- command가 생성/수정/삭제하는 stable identity
|
||||
- server-assigned ID와 client correlation의 reconcile 방법
|
||||
- authoritative resource/list revision
|
||||
- conflict status와 stable error code
|
||||
- commit response가 complete resource인지 receipt인지
|
||||
- effect certainty와 idempotency status/reconcile endpoint
|
||||
- event/outbox가 있을 때 sequence/gap/snapshot reset 의미
|
||||
|
||||
Create가 server-assigned ID를 사용하는 경우 temporary UI ID를 backend ID로
|
||||
원자적으로 교체하고 관련 detail/list key를 reconcile하는 정책이 필요하다.
|
||||
이 의미 없이 generic optimistic append를 기본 활성화하지 않는다.
|
||||
|
||||
## 10. Database와 application service baseline
|
||||
|
||||
구현 예시는 다음 논리 table/constraint를 만족해야 한다.
|
||||
|
||||
```text
|
||||
reference_resource
|
||||
tenant_id
|
||||
resource_id
|
||||
display_name
|
||||
note
|
||||
revision
|
||||
created_at
|
||||
updated_at
|
||||
deleted_at | null
|
||||
unique(tenant_id, resource_id)
|
||||
|
||||
idempotency_record
|
||||
principal/tenant fingerprint
|
||||
operation + contract version
|
||||
key hash
|
||||
request fingerprint
|
||||
state + receipt
|
||||
lease/retention timestamps
|
||||
unique(principal/tenant, operation, contract version, key hash)
|
||||
|
||||
outbox_event when selected
|
||||
aggregate identity + revision
|
||||
event type/version
|
||||
sequence
|
||||
payload reference or bounded safe projection
|
||||
publication state
|
||||
```
|
||||
|
||||
Application service transaction은 authorization scope와 tenant predicate를
|
||||
모든 read/write에 적용하고, resource mutation과 revision/outbox 기록을 같은
|
||||
transaction boundary에 둔다. cache/replica lag를 고려해 command 직후 read
|
||||
consistency와 invalidation owner를 선언한다.
|
||||
|
||||
## 11. GraphQL 선택 시 추가 구조
|
||||
|
||||
GraphQL은 제품 operation이 REST보다 aggregation 이점을 실제로 가질 때만
|
||||
선택한다.
|
||||
|
||||
```text
|
||||
Browser
|
||||
-> persisted-operation endpoint
|
||||
-> manifest allowlist
|
||||
-> auth/CSRF/rate/cost/depth/alias enforcement
|
||||
-> GraphQL router
|
||||
-> application services/loaders
|
||||
```
|
||||
|
||||
Backend handoff:
|
||||
|
||||
- authenticated immutable schema artifact와 digest
|
||||
- named operation source와 persisted ID/hash manifest
|
||||
- variables/result runtime fixtures
|
||||
- selected GraphQL-over-HTTP revision과 exact media/status profile
|
||||
- partial data policy와 safe error extension vocabulary
|
||||
- field/row authorization, cost/depth/alias/list ceiling
|
||||
- N/N-1 router/frontend manifest rollout과 retirement
|
||||
|
||||
Production endpoint는 arbitrary document와 persisted miss 후 full-document
|
||||
fallback을 받지 않는다. normalized frontend entity cache는 별도 제품 선택이다.
|
||||
|
||||
## 12. gRPC-Web 선택 시 추가 구조
|
||||
|
||||
gRPC-Web은 browser-facing gateway/proxy가 실제 선택된 unary 또는 bounded
|
||||
server-stream operation에만 사용한다.
|
||||
|
||||
```text
|
||||
Browser
|
||||
-> same-origin BFF/Envoy/gRPC-Web gateway
|
||||
-> exact service/method allowlist
|
||||
-> frame/message/deadline/status/trailer enforcement
|
||||
-> internal gRPC application service
|
||||
```
|
||||
|
||||
Backend handoff:
|
||||
|
||||
- authenticated proto source와 immutable descriptor digest
|
||||
- Buf/protoc lint/breaking 및 deterministic generation evidence
|
||||
- exact service/method/rpc-kind allowlist
|
||||
- selected gRPC-Web runtime kind, client API와 binary/JSON/text/wire revision;
|
||||
official XHR와 Connect-Web Fetch profile을 분리
|
||||
- proxy CORS, content-type, terminal status/trailer behavior
|
||||
- Envoy를 선택하면 exact version/config digest, filter order, upstream HTTP/2,
|
||||
route/idle/max-stream timeout, timeout offset와 buffering/flush
|
||||
- message/frame/count/queue/idle/total budget
|
||||
- server-stream sequence, gap, resume와 snapshot reset protocol
|
||||
- actual browser/proxy conformance
|
||||
|
||||
client streaming과 bidirectional streaming은 common gRPC-Web browser contract로
|
||||
간주하지 않는다. upload는 REST transfer, duplex는 별도 protocol을 선택한다.
|
||||
|
||||
## 13. Connect-Web/Connect 선택 시 추가 구조
|
||||
|
||||
Connect는 Protobuf-first backend의 selected unary 또는 bounded server-stream
|
||||
operation에만 사용한다. Connect protocol과 Connect-Web의 gRPC-Web transport는
|
||||
서로 다른 provider row다.
|
||||
|
||||
```text
|
||||
Browser Connect-Web adapter
|
||||
-> same-origin BFF 또는 exact cross-origin Connect endpoint
|
||||
-> auth/CSRF/CORS + service/method allowlist
|
||||
-> Connect protocol handler
|
||||
-> application service
|
||||
```
|
||||
|
||||
Backend handoff:
|
||||
|
||||
- authenticated proto/Buf source, descriptor와 generated-service digest
|
||||
- exact Connect-Web/client runtime과 server/gateway version
|
||||
- protocol revision, JSON/binary encoding, POST 또는 approved GET
|
||||
- unary HTTP/error profile 또는 stream EndStream terminal profile
|
||||
- request/response/envelope/message/count/queue byte ceiling
|
||||
- unary/stream compression capability; stock browser stream은 identity-only
|
||||
- total/idle timeout, browser abort→server context→downstream cancellation 전파
|
||||
- exact CORS allow/expose/preflight와 auth/CSRF profile
|
||||
- actual Chromium/Firefox/WebKit와 selected proxy/server conformance
|
||||
|
||||
GET은 descriptor `NO_SIDE_EFFECTS`, non-sensitive bounded input, URL/cache key,
|
||||
`Vary`와 credential policy가 모두 승인된 unary에만 허용한다. browser
|
||||
client-streaming/bidi는 Connect protocol 자체 기능과 별개로 `PLATFORM_LIMITED`다.
|
||||
|
||||
## 14. Protobuf REST Gateway 선택 시 추가 구조
|
||||
|
||||
한 route는 `CURATED_BFF | GRPC_GATEWAY | ENVOY_TRANSCODER` 중 하나만 소유한다.
|
||||
현재 reference REST의 envelope와 `200|201`, 향후 `204/304/412` 의미를 유지하는
|
||||
기본 선택은 curated BFF다.
|
||||
|
||||
Direct gateway는 ProtoJSON/HttpRule/status/error 자체를 새 public contract로
|
||||
승인한 unary operation에서만 선택한다. Backend handoff:
|
||||
|
||||
- `.proto` annotation 또는 precedence가 고정된 service config의 immutable source
|
||||
- descriptor/Buf image, canonical HttpRule route manifest와 digest
|
||||
- pinned gateway/runtime/generator/plugin과 generated OpenAPI artifact
|
||||
- ProtoJSON name/default/enum/int64/bytes/null/presence/unknown-field profile
|
||||
- exact method/path/query/body/response-body/additional-binding와 path escaping
|
||||
- safe status/error/header mapping과 raw `google.rpc.Status` detail redaction
|
||||
- CORS/auth/CSRF, body/header/query ceiling와 rate limit
|
||||
- browser abort/deadline의 upstream gRPC/application work 전파
|
||||
- N/N-1 route/OpenAPI/runtime conformance와 coherent rollback
|
||||
|
||||
Gateway는 durable idempotency, pagination snapshot, ETag/HTTP conditional,
|
||||
product envelope, authorization와 file transfer semantics를 자동 구현하지 않는다.
|
||||
필요한 operation은 application service와 BFF가 계속 소유한다. generated
|
||||
REST streaming은 별도 framing/terminal/cache ADR 없이는 `NOT_SELECTED`다.
|
||||
|
||||
## 15. Invalidation과 realtime
|
||||
|
||||
현재 frontend cross-tab invalidation은 같은 browser origin 안의 opaque
|
||||
invalidate-only hint다. backend event delivery를 의미하지 않는다.
|
||||
|
||||
Backend-driven invalidation/realtime을 선택하면:
|
||||
|
||||
- transactional outbox 또는 동등한 durable publication
|
||||
- principal/tenant authorization을 통과한 event projection
|
||||
- event type/version, aggregate revision, sequence와 dedupe identity
|
||||
- reconnect cursor, gap detection과 snapshot reset
|
||||
- retention, replay ceiling과 slow-consumer policy
|
||||
|
||||
를 제공해야 한다. event payload를 authoritative resource snapshot으로 쓸지
|
||||
query invalidate hint로만 쓸지 operation별 reducer contract가 필요하다.
|
||||
|
||||
## 16. Rate limit, deadline와 retry
|
||||
|
||||
- backend deadline은 frontend total deadline보다 짧거나 cancellation을 전파할 수
|
||||
있어야 한다.
|
||||
- disconnect/cancel 뒤 불필요한 query 작업은 중단한다.
|
||||
- keyed command는 disconnect가 transaction rollback을 보장하지 않으므로
|
||||
idempotency receipt로 effect를 판정한다.
|
||||
- `Retry-After`는 selected status에서만 bounded delta/date 형식으로 제공한다.
|
||||
- retry-safe read와 keyed command를 구분한다.
|
||||
- proxy, BFF와 service retry가 겹쳐 retry amplification을 만들지 않게 한 owner만
|
||||
재시도한다.
|
||||
- rate limit key는 principal/tenant/operation과 abuse policy에 binding하며 raw
|
||||
credential/IP를 metric label에 넣지 않는다.
|
||||
|
||||
## 17. Observability와 privacy
|
||||
|
||||
허용되는 공통 dimension:
|
||||
|
||||
```text
|
||||
operation ID
|
||||
contract/profile version
|
||||
status group / safe error code
|
||||
attempt bucket
|
||||
duration bucket
|
||||
provider/runtime health
|
||||
traffic admission stage
|
||||
```
|
||||
|
||||
금지:
|
||||
|
||||
- Authorization, cookie, CSRF와 idempotency key
|
||||
- request/response body와 validation value
|
||||
- URL query, cursor, snapshot, ETag/revision
|
||||
- GraphQL variables/path/raw error/extensions
|
||||
- protobuf bytes, metadata와 trailer 원문
|
||||
- user ID/email/file name을 metric label이나 trace attribute로 사용
|
||||
|
||||
Request ID와 trace ID는 browser에 반환할 수 있지만 credential 역할을 하지 않으며
|
||||
추측 가능한 database primary key를 포함하지 않는다.
|
||||
|
||||
필수 SLO/alert 후보:
|
||||
|
||||
- operation availability와 latency
|
||||
- 401/403/409/412/422/429 및 5xx rate
|
||||
- schema/mapper/contract mismatch
|
||||
- idempotency in-progress age, collision과 unknown effect
|
||||
- cursor invalid/expired/loop-equivalent server detection
|
||||
- conditional hit/miss와 invalid 304
|
||||
- GraphQL persisted miss/cost reject
|
||||
- Connect missing/duplicate EndStream, whole-body/queue overflow와 compression mismatch
|
||||
- gRPC-Web missing terminal status, frame/idle/queue overflow
|
||||
- REST Gateway route/OpenAPI/runtime rewrite drift와 cancel propagation loss
|
||||
|
||||
## 18. 배포, compatibility와 rollback
|
||||
|
||||
권장 순서:
|
||||
|
||||
1. contract artifact와 compatibility diff를 생성한다.
|
||||
2. backend가 N/N-1 fixture를 통과한 상태로 먼저 배포한다.
|
||||
3. frontend operation은 traffic disabled 상태에서 staging conformance를 실행한다.
|
||||
4. read-only shadow/canary 뒤 query traffic을 올린다.
|
||||
5. keyed command는 idempotency/reconcile fault injection 뒤 별도 canary한다.
|
||||
6. pagination, conditional, optimistic, GraphQL, Connect, gRPC-Web과 REST
|
||||
Gateway는 각각 독립 gate로 승격한다.
|
||||
7. provider/browser/operations evidence가 완료된 operation만 enabled한다.
|
||||
|
||||
Rollback은 frontend/backend/contract artifact를 coherent set으로 되돌린다.
|
||||
unknown-effect command를 다른 protocol이나 새 idempotency key로 replay하지 않는다.
|
||||
Backend가 old contract를 제거하는 시점은 실제 frontend support window와 cache/CDN
|
||||
retention 뒤다.
|
||||
|
||||
## 19. Conformance와 fault-injection matrix
|
||||
|
||||
Backend 완료 판정에는 unit test 외에 actual staging provider evidence가 필요하다.
|
||||
|
||||
| 범위 | 필수 증거 |
|
||||
| --- | --- |
|
||||
| REST | exact path/query/body, media/status/envelope, max body, malformed/truncated JSON |
|
||||
| Auth | missing/expired credential, 401/403, rotation, cross-origin preflight |
|
||||
| Command | concurrent same-key replay, fingerprint mismatch, commit 뒤 response loss |
|
||||
| Cursor | filter/sort binding, expiry, snapshot stability, loop/gap/duplicate 방지 |
|
||||
| Conditional | 200→304, cache-missing 304 방지, representation change, 412 |
|
||||
| Schema | additive/breaking/null/enum/time/number fixtures와 N/N-1 |
|
||||
| GraphQL | persisted hit/miss/hash mismatch, partial, cost/depth, router rollout |
|
||||
| Connect | JSON/binary unary, GET restriction, EndStream, body/message cap, compression, cancel/deadline와 CORS |
|
||||
| gRPC-Web | proxy media/status/trailer, oversized frame, cancel, idle, gap/resume |
|
||||
| REST Gateway | HttpRule path/query/body, ProtoJSON, OpenAPI/status/error rewrite, abort propagation과 N/N-1 |
|
||||
| Operations | deadline/retry amplification, rate limit, kill switch, coherent rollback |
|
||||
|
||||
## 20. Backend handoff checklist
|
||||
|
||||
- [ ] Browser-facing API/BFF owner와 on-call이 정해졌다.
|
||||
- [ ] reference REST exact endpoint/envelope/status/media fixture가 있다.
|
||||
- [ ] bearer 또는 cookie+CSRF 중 하나의 실제 profile과 CORS evidence가 있다.
|
||||
- [ ] stable error vocabulary와 redaction contract가 있다.
|
||||
- [ ] keyed command idempotency store, TTL, receipt와 reconcile이 있다.
|
||||
- [ ] CursorPage를 선택했다면 opaque cursor/snapshot contract가 있다.
|
||||
- [ ] conditional을 선택했다면 ETag/304/412와 cache owner가 있다.
|
||||
- [ ] optimistic을 선택했다면 identity/membership/revision/conflict 의미가 있다.
|
||||
- [ ] OpenAPI/runtime schema/mapper semantic artifact와 digest가 release에 binding됐다.
|
||||
- [ ] GraphQL을 선택했다면 schema/persisted manifest/router evidence가 있다.
|
||||
- [ ] Connect를 선택했다면 descriptor/runtime/server/browser evidence가 있다.
|
||||
- [ ] gRPC-Web을 선택했다면 descriptor/proxy/browser evidence가 있다.
|
||||
- [ ] REST Gateway를 선택했다면 kind/HttpRule/ProtoJSON/OpenAPI와 runtime
|
||||
conformance evidence가 있다.
|
||||
- [ ] staging conformance, fault injection, canary, kill switch와 rollback drill이
|
||||
통과했다.
|
||||
|
||||
## 21. Frontend 완료 경계
|
||||
|
||||
Backend 구현과 별개로 현재 frontend 상태를 다음처럼 해석한다.
|
||||
|
||||
| 범위 | 현재 상태 | 남은 owner |
|
||||
| --- | --- | --- |
|
||||
| REST path/provider/auth/deadline/bounded JSON | `COMPOSED` | actual provider conformance는 backend/operations |
|
||||
| runtime schema와 mapper registry | `COMPOSED` | artifact digest/source provenance는 backend contract source + frontend/platform |
|
||||
| session generation과 query identity | `COMPOSED` | account identity projection은 identity integration + frontend |
|
||||
| Cursor runtime | `AVAILABLE_NOT_COMPOSED` | CursorPage backend 계약 후 frontend query binding |
|
||||
| conditional validator store | `AVAILABLE_NOT_COMPOSED` | ETag/304/412 backend 계약 후 frontend HTTP/cache transaction |
|
||||
| ordered optimistic layer | `AVAILABLE_NOT_COMPOSED` | product membership/revision 승인 후 frontend mutation definition |
|
||||
| GraphQL adapter | `DESIGNED_NOT_IMPLEMENTED` | product/backend 선택 뒤 frontend adapter/codegen |
|
||||
| Browser RPC V3 공통 계약/coordinator | `AVAILABLE_NOT_COMPOSED` | selected descriptor/generated client와 protocol transport 확정 뒤 frontend provider adapter |
|
||||
| Protobuf schema/codegen | `DESIGNED_NOT_IMPLEMENTED` | authenticated backend contract source 선택 뒤 pinned frontend generation |
|
||||
| Connect-Web adapter | `DESIGNED_NOT_IMPLEMENTED` | product/backend/server 선택 뒤 frontend adapter/codegen |
|
||||
| gRPC-Web adapter | `DESIGNED_NOT_IMPLEMENTED` | product/backend/proxy 선택 뒤 frontend adapter/codegen |
|
||||
| Protobuf REST Gateway | `NOT_SELECTED` | gateway kind와 public HTTP contract 승인 뒤 REST adapter binding |
|
||||
| persisted query/offline command | `NOT_SELECTED` | 별도 product ADR와 backend durability 계약 |
|
||||
|
||||
따라서 “backend만 구현하면 frontend가 아무 변경 없이 모든 capability를 자동
|
||||
사용한다”는 의미는 아니다. 현재 선택된 REST reference vertical의 공통 frontend
|
||||
기반은 완료됐지만, backend contract가 확정되면 Cursor/conditional/optimistic의
|
||||
마지막 composition과 schema/mapper 변경이 frontend에 남는다. GraphQL,
|
||||
Connect/gRPC-Web과 Protobuf REST Gateway는 제품이 선택되지 않았다. 공통 Browser
|
||||
RPC operation/profile registry, application port와 lifecycle coordinator는
|
||||
구현했지만, wire별 generated client/decoder/provider binding은 아직 구현하지
|
||||
않았다. 따라서 backend contract가 정해져도 frontend provider adapter와
|
||||
composition 작업은 명시적으로 남는다.
|
||||
@@ -1,385 +0,0 @@
|
||||
# Browser data capability completion ledger
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 다음 browser data capability의 **현재 구현 상태, 목표 상태, 남은
|
||||
공통 구현, 제품 조합 책임, backend/provider 계약과 promotion 조건**을 한곳에서
|
||||
관리하는 기준 문서다.
|
||||
|
||||
- File, Blob, 파일 선택기, preview와 다운로드
|
||||
- Local/Session Storage, IndexedDB, OPFS와 Cache Storage
|
||||
- TanStack Query memory cache와 탭 간 무효화
|
||||
- Presigned URL, multipart/resumable upload와 streaming download
|
||||
- Range resumable download와 background upload/download
|
||||
- Image CDN descriptor, 검증, delivery와 presentation
|
||||
|
||||
각 상세 문서는 메커니즘과 불변조건을 설명한다. 이 ledger는 상세 문서를
|
||||
대체하지 않으며, 서로 다른 문서의 "구현됨", "사용 가능", "설계됨" 표현이
|
||||
production readiness로 잘못 합쳐지는 것을 막는 상태 단일 기준이다.
|
||||
|
||||
이 문서가 정한 상태만으로 실제 제품의 `PRODUCTION_READY`를 주장할 수 없다.
|
||||
제품 owner, backend/provider conformance와 세 browser promotion evidence가 모두
|
||||
별도 gate를 통과해야 한다.
|
||||
|
||||
## 2. 상태 체계
|
||||
|
||||
### 2.1 Primary current status
|
||||
|
||||
각 capability는 다음 다섯 상태 중 정확히 하나를 갖는다.
|
||||
|
||||
| 상태 | 의미 | 허용되는 주장 |
|
||||
| --- | --- | --- |
|
||||
| `COMPOSED` | production bootstrap 또는 설치된 feature 호출 경로에 concrete runtime이 연결돼 있다. | 저장소의 현재 제품 경로에서 실행된다. |
|
||||
| `AVAILABLE_NOT_COMPOSED` | port, policy와 reference runtime이 있으나 기본 production graph에서는 제거돼 있다. | opt-in 조합 후보가 존재한다. |
|
||||
| `DESIGNED_NOT_IMPLEMENTED` | 불변조건과 계약은 승인됐지만 해당 runtime 또는 필수 orchestration이 없다. | 설계/계약 backlog가 닫혔고 구현 backlog는 열려 있다. |
|
||||
| `NOT_SELECTED` | 가치, 비용, 보안과 운영 owner가 승인되지 않아 의도적으로 선택하지 않았다. | 누락이 아니라 미선택이다. |
|
||||
| `PLATFORM_LIMITED` | 브라우저 공통 보장이 불가능하거나 지원 범위가 제한된다. | capability probe와 fallback 안에서만 제공할 수 있다. |
|
||||
|
||||
`AVAILABLE_NOT_COMPOSED`를 `COMPOSED`로 표시하거나,
|
||||
`DESIGNED_NOT_IMPLEMENTED`를 테스트 fixture만으로 구현 완료 처리하지 않는다.
|
||||
`NOT_SELECTED` capability를 인접 runtime의 "미완성"으로 계산하지 않는다.
|
||||
|
||||
### 2.2 독립적인 canonical readiness 축
|
||||
|
||||
Primary status와 다음 네 canonical 축을 섞지 않는다. VD-15와 이 ledger를
|
||||
참조하는 운영 runbook도 축 이름과 literal을 정확히 이 표에 맞춘다.
|
||||
|
||||
| canonical 축 | 값 | 의미 |
|
||||
| --- | --- | --- |
|
||||
| `Selection` | `NOT_SELECTED`, `SELECTED`, `REMOVING` | 특정 제품이 capability를 채택했는지 여부 |
|
||||
| `TrafficAdmission` | `DISABLED`, `SHADOW`, `CANARY`, `ENABLED` | 조합된 runtime의 신규 작업 admission |
|
||||
| `RuntimeHealth` | `UNKNOWN`, `AVAILABLE`, `DEGRADED`, `UNAVAILABLE`, `INCOMPATIBLE` | 현재 runtime/provider 관측 상태 |
|
||||
| `PromotionEvidence` | `MISSING`, `PARTIAL`, `COMPLETE`, `EXPIRED` | 필요한 contract/provider/browser/operations 증거의 합성 결과 |
|
||||
|
||||
`PromotionEvidence`의 입력은 다음 component gate다. 이 값들은 새로운 readiness
|
||||
축이 아니라 합성 근거이며 evidence record와 함께 보존한다.
|
||||
|
||||
| component gate | 값 | 의미 |
|
||||
| --- | --- | --- |
|
||||
| contract | `MISSING`, `DRAFT`, `ACCEPTED` | frontend와 provider가 맞출 wire/behavior 계약 상태 |
|
||||
| provider | `NOT_REQUIRED`, `PENDING`, `CONFORMANT` | 실제 BFF, object storage, CDN 또는 hosting 증거 |
|
||||
| browser | `MISSING`, `PARTIAL`, `PROMOTABLE` | 승인 browser/device matrix의 native 증거 |
|
||||
| operations | `MISSING`, `DOCUMENTED`, `DRILLED` | 관측, kill switch, recovery와 rollback 실행 증거 |
|
||||
|
||||
projection은 다음처럼 고정한다.
|
||||
|
||||
- 필수 component artifact가 없으면 `MISSING`이다.
|
||||
- 유효한 일부 증거만 있거나 component가 terminal gate 전이면 `PARTIAL`이다.
|
||||
- contract가 `ACCEPTED`, provider가 `NOT_REQUIRED` 또는 `CONFORMANT`, browser가
|
||||
`PROMOTABLE`, operations가 `DRILLED`이고 모든 required artifact가 유효할 때만
|
||||
`COMPLETE`다.
|
||||
- 한 번 유효했던 required artifact가 정책의 freshness/expiry를 넘으면 다른
|
||||
component 값과 무관하게 `EXPIRED`다.
|
||||
|
||||
예를 들어 Image CDN reference runtime은
|
||||
`AVAILABLE_NOT_COMPOSED / Selection=NOT_SELECTED /
|
||||
TrafficAdmission=DISABLED / RuntimeHealth=UNKNOWN /
|
||||
PromotionEvidence=PARTIAL`이고 그 근거가
|
||||
`contract=ACCEPTED / provider=PENDING / browser=PARTIAL /
|
||||
operations=DOCUMENTED`일 수 있다. 이 행을 `COMPOSED`나
|
||||
`PRODUCTION_READY`로 줄여 쓰지 않는다.
|
||||
|
||||
### 2.3 가능한 구현 경로
|
||||
|
||||
다음은 제품이 아직 선택하지 않았고 reference source도 없는 capability가 거칠 수
|
||||
있는 **일반적인 경로 예시**다. 다섯 primary status를 선형 maturity로 정의하지
|
||||
않으며 모든 capability가 이 경로를 밟는 것도 아니다. 이미 reference runtime이
|
||||
있는 capability는 `AVAILABLE_NOT_COMPOSED`에서 시작할 수 있고, cross-browser
|
||||
의미가 불가능한 capability는 구현량과 무관하게 `PLATFORM_LIMITED`다.
|
||||
|
||||
```text
|
||||
NOT_SELECTED
|
||||
-> decision + owner + data classification
|
||||
-> DESIGNED_NOT_IMPLEMENTED
|
||||
-> implementation + deterministic evidence + removal evidence
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> product policy + provider contract + bootstrap composition
|
||||
-> COMPOSED
|
||||
-> provider/browser/operations promotion gates
|
||||
-> product-local production approval
|
||||
```
|
||||
|
||||
`PLATFORM_LIMITED`는 위 흐름과 별도 제약이다. 지원 가능한 browser에서는
|
||||
지원 browser용 runtime 행을 별도 상태로 기록할 수 있지만, cross-browser 보장
|
||||
행의 primary status는 계속 `PLATFORM_LIMITED`다. 제품 계약은 지원 불가능한
|
||||
browser의 fallback을 동시에 선언해야 한다.
|
||||
|
||||
rollback은 상태를 거꾸로 가장하지 않는다. 신규 진입을 kill switch로 닫고,
|
||||
active operation을 drain 또는 abort하고, durable state를 정책대로 정리한 뒤
|
||||
composition과 production module을 제거한다.
|
||||
|
||||
## 3. 구현 책임 분류
|
||||
|
||||
남은 항목은 다음 네 분류 중 하나 이상을 갖는다.
|
||||
|
||||
| 분류 | owner | 설명 |
|
||||
| --- | --- | --- |
|
||||
| `COMMON_REQUIRED` | frontend platform | 제품 API 주소 없이도 구현할 수 있고 선택 capability의 안전성에 필수인 port, state machine, policy와 lifecycle |
|
||||
| `PRODUCT_COMPOSITION` | product/feature owner | dataset, account partition, UX, retention, quota priority, query/preset profile과 use-case facade |
|
||||
| `PROVIDER_CONTRACT` | backend/storage/CDN/infra owner | authorization, signing, server ledger, storage constraint, CDN preset와 conformance |
|
||||
| `OPTIONAL_CAPABILITY` | architecture + product approval | 필요성이 확인될 때 별도 threat model과 비용 승인을 거쳐 설치할 기능 |
|
||||
|
||||
`COMMON_REQUIRED`는 범용 mega-service를 뜻하지 않는다. 메커니즘은 공통이지만
|
||||
정책 값은 immutable composition snapshot으로 주입한다.
|
||||
|
||||
## 4. 현재 capability snapshot
|
||||
|
||||
### 4.1 File, Blob, picker와 download
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| File/Blob intake | `AVAILABLE_NOT_COMPOSED` | opaque file ref, transient vault, metadata normalization, byte/type/signature policy, bounded range read, closed-result stream | native chunk가 hard maximum을 넘지 않도록 재분할하는 ceiling과 제품 profile | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| native input picker | `AVAILABLE_NOT_COMPOSED` | keyboard/focus 가능한 input baseline, multiple, same-file reselection, dismissal outcome | 제품별 copy와 workflow | `PRODUCT_COMPOSITION` |
|
||||
| enhanced open picker | `AVAILABLE_NOT_COMPOSED` | user activation과 conditional enhancement | browser matrix와 native input fallback 유지 | `PRODUCT_COMPOSITION` |
|
||||
| directory selection | `NOT_SELECTED` | 없음 | bounded traversal, relative-path policy, symlink/entry ceiling | `OPTIONAL_CAPABILITY` |
|
||||
| persistent file handle | `NOT_SELECTED` | native handle은 transient vault 밖으로 나가지 않음 | permission recovery, handle registry, retention/logout | `OPTIONAL_CAPABILITY` |
|
||||
| drag/drop·paste·capture | `NOT_SELECTED` | 공통 file capture primitive 일부만 재사용 가능 | 별도 adapter와 접근 가능한 UX | `OPTIONAL_CAPABILITY` |
|
||||
| object URL preview lease | `AVAILABLE_NOT_COMPOSED` | receipt binding, active-content denylist, byte cap, lease/revoke | 제품이 preview를 선택할 때 safety probe와 함께 조합 | `PRODUCT_COMPOSITION` |
|
||||
| local preview decode-safety probe | `DESIGNED_NOT_IMPLEMENTED` | 현재 dimension/pixel/decoded-memory/animation preflight 없음 | object URL 발급 전 static header/decode budget 검증 | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| browser-managed download | `AVAILABLE_NOT_COMPOSED` | synchronous resolver/vault seam과 `BROWSER_HANDOFF` outcome을 saved와 구분 | concrete BFF issuer/strict response와 제품 open/share/save UX | `PRODUCT_COMPOSITION` + `PROVIDER_CONTRACT` |
|
||||
| picker streaming save | `AVAILABLE_NOT_COMPOSED` | bounded stream, backpressure, integrity, close/abort truth | capability/size 기반 strategy selector | `COMMON_REQUIRED` |
|
||||
| bounded Blob download | `AVAILABLE_NOT_COMPOSED` | small generated artifact hard cap | browser별 상한과 server-generation fallback | `PRODUCT_COMPOSITION` |
|
||||
| Range resumable download | `DESIGNED_NOT_IMPLEMENTED` | 현재 one-shot download와 명시적으로 분리 | Range/If-Range/206, validator, checkpoint, seek/truncate, final integrity | `COMMON_REQUIRED` + `PROVIDER_CONTRACT` |
|
||||
| app-managed background download | `NOT_SELECTED` | browser-managed handoff만 존재 | 지원 browser의 progressive enhancement로만 평가 | `OPTIONAL_CAPABILITY` |
|
||||
| cross-browser app-managed background download guarantee | `PLATFORM_LIMITED` | 장시간 worker/picker/file permission 유지가 공통 보장되지 않음 | browser-managed handoff 또는 explicit unsupported fallback | 플랫폼 제약 |
|
||||
|
||||
### 4.2 Query, Web Storage와 cross-context
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| TanStack Query memory cache | `COMPOSED` | concrete QueryClient, cancellation, stale UI, optimistic rollback, invalidate | session/account scope lifecycle, late-result fence, strict query policy/key codec | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| Local Storage registry | `COMPOSED` | 등록 key, closed codec/envelope, TTL, global hard cap, memory fallback | key별 cap, partition/logout, migration, explicit outcome, bounded sweep | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| Session Storage registry | `COMPOSED` | tab-scoped 등록 control record와 동일 codec | key별 cap과 explicit durability/outcome | `COMMON_REQUIRED` |
|
||||
| cross-tab invalidation | `COMPOSED` | invalidate-only, versioned envelope, duplicate/stale/gap 처리, BroadcastChannel→localStorage→local-only | account epoch, exact storage source, production coordinator browser E2E | `COMMON_REQUIRED` |
|
||||
| IndexedDB query persistence reference runtime | `DESIGNED_NOT_IMPLEMENTED` | persistence key는 disabled로 강제되고 persister source는 없음 | 승인 query만 dehydrate/hydrate하는 facade | 선택 시 `COMMON_REQUIRED` |
|
||||
| product query persistence | `NOT_SELECTED` | persist 대상 query, owner와 retention 승인이 없음 | reference runtime 구현 뒤 별도 opt-in | `OPTIONAL_CAPABILITY` |
|
||||
| durable cache namespace epoch | `DESIGNED_NOT_IMPLEMENTED` | release epoch만 존재 | persisted resurrection 방지 transaction ledger | persistence 선택 시 `COMMON_REQUIRED` |
|
||||
| offline mutation command queue | `NOT_SELECTED` | foreground optimistic mutation만 존재 | idempotent durable command/sync protocol | `OPTIONAL_CAPABILITY` + `PROVIDER_CONTRACT` |
|
||||
| SSR hydration | `NOT_SELECTED` | 현재 client SPA | request-scoped QueryClient와 precedence | SSR 선택 시 `PRODUCT_COMPOSITION` |
|
||||
|
||||
### 4.3 IndexedDB, OPFS와 Cache Storage
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| generic IndexedDB runtime | `AVAILABLE_NOT_COMPOSED` | transaction-complete, CAS, idempotency, logical budget, TTL, lifecycle authority, additive DDL, resumable codec migration, blocked/versionchange | feature dataset repository/schema/codec/query와 production composition | `PRODUCT_COMPOSITION` |
|
||||
| OPFS byte runtime | `AVAILABLE_NOT_COMPOSED` | DedicatedWorker SyncAccessHandle, async fallback, Web Locks, hash tree, IDB journal saga, budget/GC/reconcile | 제품 namespace/dataset policy와 production composition | `PRODUCT_COMPOSITION` |
|
||||
| OPFS real readiness preflight | `DESIGNED_NOT_IMPLEMENTED` | 없음; API property probe와 별도 native conformance test만 존재 | worker/lock/journal/small write-read-delete-cleanup을 한 readiness operation으로 검증 | `COMMON_REQUIRED` |
|
||||
| OPFS physical/journal forward migration | `DESIGNED_NOT_IMPLEMENTED` | 없음; 현재 v1 layout/journal과 reconciliation만 존재 | copy-on-write generation, checkpoint, publish authority와 N-1 rollback | `COMMON_REQUIRED` |
|
||||
| public static Cache release runtime | `AVAILABLE_NOT_COMPOSED` | same-origin public GET, exact Vary/URL/type/size/digest, stage/activate/previous rollback | 제품 release/hosting policy와 production composition | `PRODUCT_COMPOSITION` |
|
||||
| bounded Cache inspect/cleanup | `DESIGNED_NOT_IMPLEMENTED` | 없음; 현재 ownership 검사는 지키지만 cache-count scan은 unbounded | policy/epoch-bound cursor, count/deadline과 partial-success resume | `COMMON_REQUIRED` |
|
||||
| Cache control/prefix forward migration | `DESIGNED_NOT_IMPLEMENTED` | 없음; 현재 v1 control/prefix parser와 release primitive만 존재 | 새 schema candidate, verify/activate, N-1 retain과 bounded cleanup | `COMMON_REQUIRED` |
|
||||
| cross-store quota lifecycle | `DESIGNED_NOT_IMPLEMENTED` | store별 logical budget과 StorageManager signal은 존재 | write admission, pressure hysteresis, GC priority, one retry, scheduled maintenance | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| Service Worker offline fetch | `NOT_SELECTED` | Cache Storage runtime은 window에서도 독립 사용 가능 | registration, install/waiting/activation, client drain, navigation strategy | `OPTIONAL_CAPABILITY` |
|
||||
| private response cache | `NOT_SELECTED` | 현재 public cache가 명시적으로 거절 | 별도 partition/encryption 오해 방지/retention threat model | `OPTIONAL_CAPABILITY` + `PROVIDER_CONTRACT` |
|
||||
| sparse Range cache | `NOT_SELECTED` | `Range` request와 206 response를 거절 | validator-bound sparse segment merge | `OPTIONAL_CAPABILITY` + `PROVIDER_CONTRACT` |
|
||||
|
||||
### 4.4 Presigned transfer, upload와 Image CDN
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| presigned capability | `AVAILABLE_NOT_COMPOSED` | fixed endpoint provider, strict binding, in-memory single-use vault, safe data-plane fetch | explicit download wire version, browser-handoff provider, actual signer conformance | `COMMON_REQUIRED` + `PROVIDER_CONTRACT` |
|
||||
| multipart/resumable upload | `AVAILABLE_NOT_COMPOSED` | part hash/retry, IDB checkpoint, server reconcile, cross-tab cancel, complete/abort | pause, checkpoint inventory/retention sweep, unsupported lock decision와 실제 server/session provider | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` + `PROVIDER_CONTRACT` |
|
||||
| one-shot streaming download | `AVAILABLE_NOT_COMPOSED` | bounded whole-object stream, length/media/integrity, picker/Blob/handoff delivery | strategy selector와 Range capability 분리 | `COMMON_REQUIRED` |
|
||||
| top-level transfer composition | `DESIGNED_NOT_IMPLEMENTED` | 개별 factory와 dispose는 존재 | strict config, readiness, atomic account teardown, drain, kill switch | `COMMON_REQUIRED` |
|
||||
| Image CDN verification engine | `AVAILABLE_NOT_COMPOSED` | opaque asset/preset, signed descriptor verification, responsive candidate, static metadata/decode budget | 제품 preset/presentation policy 조합과 실제 provider/private delivery E2E | `PRODUCT_COMPOSITION` + `PROVIDER_CONTRACT` |
|
||||
| image descriptor HTTP provider | `DESIGNED_NOT_IMPLEMENTED` | caller가 decoded descriptor를 직접 제공 | fixed BFF endpoint, bounded schema, refresh single-flight, expiry/logout fence | `COMMON_REQUIRED` + `PROVIDER_CONTRACT` |
|
||||
| safe image presentation primitive | `DESIGNED_NOT_IMPLEMENTED` | descriptor 결과만 제공 | URL 재조립 없는 picture/source/img projection | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| app-managed background upload | `NOT_SELECTED` | checkpoint 기반 foreground resume만 존재 | worker lifetime/staging/permission 모델 별도 설계 | `OPTIONAL_CAPABILITY` |
|
||||
| cross-browser app-managed background upload guarantee | `PLATFORM_LIMITED` | page/worker lifetime과 local source permission이 공통 보장되지 않음 | foreground resume 또는 explicit unsupported fallback | 플랫폼 제약 |
|
||||
|
||||
## 5. 중요한 경계
|
||||
|
||||
### 5.1 같은 이름처럼 보이지만 다른 capability
|
||||
|
||||
- streaming download는 메모리 상한을 지키며 **이번 응답을 끝까지** 저장한다.
|
||||
Range resumable download는 새로운 요청에서 validator와 destination offset을
|
||||
검증해 **이전 partial state를 이어 간다**.
|
||||
- multipart resume는 upload session protocol이다. Background upload는 page
|
||||
lifecycle이 끝난 뒤에도 실행 주체가 살아 있다는 별도 보장이다.
|
||||
- Cache Storage release runtime은 public response를 검증·활성화한다. Service
|
||||
Worker는 navigation/fetch interception과 controlled-client lifecycle을 소유한다.
|
||||
- generic IndexedDB runtime은 query persistence가 아니다. Query persistence는
|
||||
query classification, dehydration, scope epoch와 restore precedence를 추가로
|
||||
요구한다.
|
||||
- browser-managed handoff는 브라우저에 전달했다는 결과다. application이
|
||||
저장 완료, 진행률 또는 background retry를 증명한 결과가 아니다.
|
||||
- Image CDN engine은 descriptor를 검증한다. BFF descriptor 발급, 실제 CDN,
|
||||
`<picture>` UX를 자동으로 제공하지 않는다.
|
||||
|
||||
### 5.2 정책과 도메인
|
||||
|
||||
byte ceiling, retry 상한, schema version, state transition과 fail-closed fallback은
|
||||
공통 메커니즘이다. 다음 값은 도메인 코드가 아니라 **제품 composition policy**다.
|
||||
|
||||
- 어떤 file purpose와 MIME/signature profile을 허용하는가
|
||||
- 어떤 query/dataset을 어느 account partition에 얼마나 오래 저장하는가
|
||||
- quota pressure에서 무엇을 먼저 제거하는가
|
||||
- 어떤 upload purpose와 CDN preset을 설치하는가
|
||||
- save/open/share와 conflict/recovery UX를 어떻게 보여 주는가
|
||||
|
||||
업무 entity와 권한 결과는 backend/domain이 소유한다. frontend policy는 이를
|
||||
추측하거나 대체하지 않는다.
|
||||
|
||||
## 6. External authority·backend·provider 계약
|
||||
|
||||
server/session/CDN 경계를 넘는 capability는 해당되는 external 계약 없이 실제
|
||||
제품에 조합하지 않는다. local-only/reconstructable dataset에 backend를
|
||||
일괄 요구하지 않는다.
|
||||
|
||||
| 경계 | 맞춰야 하는 owner/authority/provider 계약 |
|
||||
| --- | --- |
|
||||
| File upload | Web/BFF authorization, upload session API, file server 또는 object-storage data plane, quarantine/scanner/promotion |
|
||||
| Presigned URL | BFF signer, cloud object storage, CORS/CSP, method/header/length/checksum/expiry 강제 |
|
||||
| Multipart resume | server session ledger, idempotency, authoritative part status, completion receipt와 orphan janitor |
|
||||
| Range download | immutable object generation 또는 strong validator, exact Range/If-Range semantics, full-object digest |
|
||||
| account cache scope | frontend common runtime은 scope snapshot 검증, local generation/fence/teardown을 소유한다. product composition은 account/tenant 의미를 opaque partition policy에 mapping하고, auth/session owner는 sign-in/revoke/switch 사실을 제공한다. backend-issued epoch를 선택한 경우에만 그것이 wire 계약이다. |
|
||||
| offline mutation | idempotency key, entity revision/ETag, cursor/delta, conflict/merge protocol |
|
||||
| Image CDN | BFF descriptor endpoint, asset revision/preset registry, signing key rotation, CDN cache/CORS/CSP/no-store |
|
||||
| eviction recovery | server-authoritative projection의 재구성 또는 all-marker-loss 구분이 필요한 제품에만 re-sync cursor/opaque installation epoch 계약 |
|
||||
|
||||
브라우저 native `File`, `FileSystemHandle`, IndexedDB physical store, OPFS path,
|
||||
Cache name과 local checkpoint revision은 backend wire 계약이 아니다.
|
||||
|
||||
## 7. 설계 우선 work package
|
||||
|
||||
### WP-01. Scope-safe client cache
|
||||
|
||||
- session/account/release scope snapshot과 generation
|
||||
- old QueryClient cancel, fence, clear, dispose와 remount
|
||||
- late-result rejection
|
||||
- strict query registry/key codec와 per-query ceiling
|
||||
- Web Storage per-key policy, partition/logout/migration/outcome
|
||||
- production coordinator까지 연결한 multi-page browser evidence
|
||||
|
||||
Exit: account A의 cache, storage event와 늦은 async result가 account B runtime에
|
||||
관측되거나 기록될 수 없음을 deterministic fault와 native browser test로 증명한다.
|
||||
|
||||
### WP-02. Range resumable download
|
||||
|
||||
- 별도 `ResumableDownloadPort`
|
||||
- validator-bound checkpoint와 non-authorizing persistence
|
||||
- 200/206/412/416 state machine
|
||||
- seek/truncate 또는 OPFS staging destination
|
||||
- capability renewal와 final whole-object integrity
|
||||
- browser strategy selector와 fail-closed fallback
|
||||
|
||||
Exit: crash, capability expiry, object replacement, malformed Content-Range,
|
||||
destination mismatch와 integrity failure에서 corrupt saved outcome이 0건이다.
|
||||
|
||||
### WP-03. Origin storage lifecycle
|
||||
|
||||
- StorageManager signal + actual quota failure 기반 pressure controller
|
||||
- policy-owned eviction priority와 hysteresis
|
||||
- bounded maintenance cursor/deadline
|
||||
- IDB/OPFS/Cache forward migration과 N-1 rollback
|
||||
- OPFS native preflight와 clear/eviction recovery
|
||||
- local preview bounded header parser/decode probe, pixel/decoded-byte/animation ceiling과
|
||||
object URL 발급 전 fail-closed rejection
|
||||
|
||||
Exit: quota/migration/crash fault에서 unbounded scan, destructive auto-reset 또는
|
||||
cross-scope read 없이 read-only/online-only/recovery outcome으로 닫힌다. hostile,
|
||||
oversize 또는 animated preview fixture는 object URL 발급 전에 거절되고 decode
|
||||
resource와 lease가 남지 않는다.
|
||||
|
||||
### WP-04. Transfer operational composition
|
||||
|
||||
- strict config schema와 protocol/version registry
|
||||
- file/presigned/upload/download/image runtime atomic assembly
|
||||
- readiness, kill switch, active-operation drain과 idempotent close
|
||||
- logout/account switch fence
|
||||
- checkpoint inventory/retention owner와 safe observations
|
||||
- frontend provider contract harness
|
||||
|
||||
Exit: partially configured runtime이 시작되지 않고, teardown 뒤 capability나
|
||||
late refresh가 새 scope에서 재사용되지 않는다.
|
||||
|
||||
### WP-05. Image descriptor delivery
|
||||
|
||||
- fixed BFF provider와 bounded closed decoder
|
||||
- descriptor refresh single-flight와 expiry budget
|
||||
- logout/account/runtime-generation fence
|
||||
- static safe picture projection
|
||||
- actual private/public CDN conformance and browser evidence
|
||||
|
||||
Exit: caller-provided URL/transform이 DOM에 도달하지 않고, 만료·회전·logout·decode
|
||||
failure가 placeholder 또는 closed failure로 복구된다.
|
||||
|
||||
### WP-06. Optional capability decisions
|
||||
|
||||
directory/persistent handle, Query persistence, offline mutation, Service Worker,
|
||||
private/range cache와 app-managed background upload/download는 각각 독립 ADR,
|
||||
threat model, owner, budget과 removal plan을 승인한 뒤에만 시작한다.
|
||||
|
||||
## 8. 문서 우선 gate
|
||||
|
||||
runtime 구현을 시작하기 전에 해당 work package 문서에 다음이 모두 있어야 한다.
|
||||
|
||||
- current/target status와 out-of-scope
|
||||
- application port와 adapter/provider owner
|
||||
- immutable policy/config schema와 implementation ceiling
|
||||
- state machine, concurrency와 cancellation owner
|
||||
- durable record 분류, scope, TTL, purge와 migration
|
||||
- backend/provider wire version과 compatibility
|
||||
- browser capability matrix와 fallback
|
||||
- observability allowlist와 금지 값
|
||||
- rollout, kill switch, rollback과 removal
|
||||
- deterministic, contract, native browser, fault와 operational drill
|
||||
- 완료 조건과 promotion evidence 위치
|
||||
|
||||
문서가 없는 편의 API, fallback, persistence field 또는 retry owner를 구현 중에
|
||||
추가하지 않는다. 새 요구는 ledger와 해당 ADR을 먼저 변경한다.
|
||||
|
||||
## 9. 구현 및 promotion 순서
|
||||
|
||||
```text
|
||||
ledger/ADR accepted
|
||||
-> port + closed policy/schema
|
||||
-> deterministic fake/contract harness
|
||||
-> reference runtime + negative boundary gate
|
||||
-> fault/migration/removal evidence
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> product owner + 필요한 external provider/config 선택
|
||||
-> bootstrap composition behind kill switch
|
||||
-> COMPOSED + TrafficAdmission=DISABLED
|
||||
-> native Chromium/Firefox/WebKit + device drill
|
||||
-> provider/browser/operations promotion gates
|
||||
-> TrafficAdmission=CANARY/ENABLED
|
||||
-> project-local production promotion
|
||||
```
|
||||
|
||||
추천 구현 순서는 WP-01 → WP-02 → WP-03 → WP-04 → WP-05다. WP-02와 WP-03의
|
||||
seekable/staging 정책, WP-04와 WP-05의 lifecycle/config 계약은 설계 단계에서
|
||||
서로 검토하되 한 변경에서 모든 runtime을 동시에 조합하지 않는다.
|
||||
|
||||
## 10. 공통 완료 기준
|
||||
|
||||
- [ ] 모든 capability가 이 문서의 primary status 하나를 가진다.
|
||||
- [ ] `AVAILABLE_NOT_COMPOSED` source가 기본 production module inventory에 없다.
|
||||
- [ ] `COMPOSED` capability는 bootstrap부터 실제 consumer까지 호출 증거가 있다.
|
||||
- [ ] account/session 전환이 broadcast delivery나 브라우저 종료에 의존하지 않는다.
|
||||
- [ ] byte, record, queue, candidate, retry, deadline과 scan에 hard ceiling이 있다.
|
||||
- [ ] durable state는 schema/codec/scope/epoch/retention/migration을 함께 선언한다.
|
||||
- [ ] raw URL, query, signed header, file name/path, account ID, storage value,
|
||||
digest/ETag/receipt가 diagnostics나 telemetry에 노출되지 않는다.
|
||||
- [ ] backend/provider contract는 fake, emulator와 실제 provider에 재사용 가능한
|
||||
conformance suite를 가진다.
|
||||
- [ ] Chromium/Firefox/WebKit과 승인 device fallback 증거가 보존된다.
|
||||
- [ ] kill switch, N-1 rollback, recovery와 optional runtime removal drill이
|
||||
통과한다.
|
||||
- [ ] 외부 증거가 없는 항목을 `PRODUCTION_READY`로 표시하지 않는다.
|
||||
|
||||
## 11. 상세 문서
|
||||
|
||||
- [Browser file and origin storage](./browser-file-and-origin-storage.md)
|
||||
- [Client cache and storage](./client-cache-and-storage.md)
|
||||
- [Presigned transfer and Image CDN](./presigned-transfer-and-image-cdn.md)
|
||||
- [Server file capability infrastructure](./server-file-capability-infrastructure.md)
|
||||
- [VD-11 Browser file and origin-storage](./decisions/VD-11-browser-file-and-origin-storage.md)
|
||||
- [VD-12 Presigned transfer and Image CDN](./decisions/VD-12-presigned-transfer-and-image-cdn.md)
|
||||
- [VD-13 Client cache scope and persistence](./decisions/VD-13-client-cache-scope-and-persistence.md)
|
||||
- [VD-14 Resumable download and background download](./decisions/VD-14-resumable-download-and-background-transfer.md)
|
||||
- [VD-15 Origin storage lifecycle and migration](./decisions/VD-15-origin-storage-lifecycle-and-migration.md)
|
||||
- [VD-16 Browser transfer composition and image delivery](./decisions/VD-16-browser-transfer-composition-and-image-delivery.md)
|
||||
- [Browser file/storage recovery](../operations/browser-file-storage-recovery.md)
|
||||
- [Client cache/storage recovery](../operations/client-cache-and-storage-recovery.md)
|
||||
- [Browser transfer recovery](../operations/browser-transfer-recovery.md)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
# VD-01: TypeScript 7과 ESLint 10의 점진적 전환 도구
|
||||
|
||||
- 상태: Accepted
|
||||
- 결정일: 2026-07-26
|
||||
- 적용 브랜치: `feature-frontend-typescript-tooling-foundation`
|
||||
|
||||
## 배경
|
||||
|
||||
저장소는 TypeScript `7.0.2`와 ESLint `10.8.0`을 고정하고 있다. 첫 전환
|
||||
브랜치는 compiler를 변경하거나 production source를 일괄 변환하지 않고
|
||||
JS/JSX/TS/TSX가 같은 품질 게이트를 통과하게 해야 한다.
|
||||
|
||||
결정 시점의 package peer contract는 다음과 같다.
|
||||
|
||||
- `typescript-eslint@8.65.0`과 canary는 TypeScript `<6.1.0`을 요구한다.
|
||||
- `eslint-plugin-jsx-a11y@6.10.2`는 ESLint `<=9`를 요구한다.
|
||||
- `eslint-plugin-react-hooks@7.1.1`은 ESLint 10을 지원한다.
|
||||
- Babel 8 ESLint parser는 ESLint 10을 지원하고 Node `>=24.11.0`을 요구한다.
|
||||
|
||||
호환되지 않는 peer dependency를 강제 설치하면 lockfile 검증은 통과하더라도
|
||||
지원되지 않는 parser와 rule 조합을 플랫폼 계약으로 만들게 된다.
|
||||
|
||||
## 결정
|
||||
|
||||
1. TypeScript `7.0.2`와 ESLint `10.8.0`을 유지한다.
|
||||
2. TypeScript/TSX의 ESLint syntax parsing에는
|
||||
`@babel/eslint-parser`와 TypeScript/JSX syntax plugin을 사용한다.
|
||||
3. TypeScript의 이름 해석, unused 진단과 type semantics는 `tsc`가 소유한다.
|
||||
Babel parser가 TypeScript scope manager를 제공하지 않으므로 TS 파일의
|
||||
core `no-undef`와 `no-unused-vars`는 끄고 분리된 app/node/test TypeScript
|
||||
project를 필수 게이트로 실행한다.
|
||||
4. React Hook 규칙은 호환되는 `eslint-plugin-react-hooks`로 즉시 적용한다.
|
||||
5. JSX 접근성은 현재의 semantic component contract, Testing Library,
|
||||
axe 기반 cross-browser gate와 수동 검토 계약이 계속 담당한다. 호환되지 않는
|
||||
`eslint-plugin-jsx-a11y`는 설치하지 않는다.
|
||||
6. Babel 8의 지원 범위에 맞춰 Node engine 하한을 `24.11.0`으로 명시한다.
|
||||
7. production source의 대량 rename은 이 결정에 포함하지 않는다.
|
||||
|
||||
## 적용 후 상태 (2026-07-27)
|
||||
|
||||
후속 migration에서 production source, 비-fixture tests, Node scripts와 지원되는
|
||||
tool config를 모두 TS/TSX로 전환했다. `allowJs`는 껐고 runtime/source 영역의
|
||||
JavaScript 재유입은 architecture gate가 거절한다. Node scripts는 pinned Node
|
||||
24에서 `.ts`로 직접 실행되며 NodeNext, `verbatimModuleSyntax`와
|
||||
`erasableSyntaxOnly`로 별도 typecheck한다. `tests/fixtures/**`도 TS/TSX
|
||||
architecture/security/type negative input으로 전환했다. 이는 7번 결정의 범위를
|
||||
변경한 것이 아니라 그 기반 위에서 완료한 별도 후속 작업이다.
|
||||
|
||||
## 검증
|
||||
|
||||
- `check:types`는 app, Node scripts/config, tests project를 모두 검사한다.
|
||||
- TS invalid-call, invalid port, discriminated-union fixture는 실패해야 한다.
|
||||
- ESLint와 dependency-cruiser는 TS/TSX architecture fixture를 검사한다.
|
||||
- registry scanner는 TS registry의 required field, uniqueness와 reference를
|
||||
검증한다.
|
||||
- browser security gate는 TSX의 금지된 raw HTML fixture를 거절한다.
|
||||
|
||||
## 후속 검토와 제거
|
||||
|
||||
`typescript-eslint`가 TypeScript 7을, JSX 접근성 plugin이 ESLint 10을 공식
|
||||
지원하면 별도 dependency 브랜치에서 peer metadata와 전체 negative fixture를
|
||||
재검증한다. 교체할 때는 Babel parser package와 TS 전용 ESLint override를
|
||||
함께 제거한다. compiler downgrade나 `--force` 설치는 이 ADR의 rollback
|
||||
방법이 아니다.
|
||||
@@ -1,58 +0,0 @@
|
||||
# VD-03: React Router Data Mode와 서버 상태 소유권
|
||||
|
||||
- 상태: Accepted
|
||||
- 결정일: 2026-07-26
|
||||
- 적용 브랜치: `feature-frontend-routing-release-recovery-runtime`
|
||||
|
||||
## 배경
|
||||
|
||||
기존 라우터는 `BrowserRouter`와 수동 JSX route 목록을 사용했다. 직렬화 가능한
|
||||
route registry에 params/search schema, loading/error surface, access, title,
|
||||
navigation과 chunk ID가 있었지만 실행 route tree와 독립적이어서 선언과 행동이
|
||||
어긋날 수 있었다.
|
||||
|
||||
이 저장소는 client-only SPA이며 서버 상태는 application input과 TanStack Query가
|
||||
소유한다. Framework Mode의 loader/action 중심 데이터 소유권이나 SSR을 도입하지
|
||||
않으면서 route object, 오류 경계와 navigation lifecycle은 중앙에서 조립할
|
||||
필요가 있다.
|
||||
|
||||
## 결정
|
||||
|
||||
1. 고정된 React Router `7.18.1`의 `createBrowserRouter`와 `RouterProvider`를
|
||||
사용하는 Data Mode를 기본값으로 채택한다.
|
||||
2. 직렬화 가능한 route contract와 React component/codec runtime map을 분리한다.
|
||||
3. 모든 executable route object와 navigation은 registry에서 생성한다. JSX에서
|
||||
route 목록을 다시 열거하지 않는다.
|
||||
4. params/search는 route 경계의 Zod codec으로 parse하고 같은 codec으로 canonical
|
||||
URL을 생성한다.
|
||||
5. loader/action은 같은 서버 데이터를 직접 다시 요청하지 않는다. 필요하면
|
||||
application input 또는 query adapter 한 경로를 호출한다.
|
||||
6. 서버 상태, retry, cache와 mutation lifecycle은 application input과 TanStack
|
||||
Query가 계속 소유한다.
|
||||
7. lazy chunk rejection만 release recovery input으로 보내며 일반 render error는
|
||||
route/feature boundary가 소유한다.
|
||||
8. Framework Mode, SSR, static generation과 router version upgrade는 별도
|
||||
dependency/architecture 브랜치에서 결정한다.
|
||||
|
||||
## 검증
|
||||
|
||||
- route contract/runtime map의 누락과 orphan은 TypeScript negative fixture와
|
||||
registry gate가 모두 거절한다.
|
||||
- duplicate ID/path, unknown codec/surface/chunk와 참조 불일치를 negative registry
|
||||
fixture로 검증한다.
|
||||
- params/search parse/build round-trip, canonical redirect, 최대 redirect hop,
|
||||
access rejection, title/focus와 boundary reset을 unit/component test로 검증한다.
|
||||
- Vite dynamic entry와 release route chunk map, runtime config JSON Schema를
|
||||
build/release 검증기가 확인한다.
|
||||
- chunk failure는 no-store manifest refetch 후 build/release 쌍마다 한 번만
|
||||
reload하며 offline, malformed manifest와 storage 실패는 fail-closed한다.
|
||||
|
||||
## 결과와 rollback
|
||||
|
||||
Data Router는 navigation lifecycle의 조립 경계이며 서버 데이터 계층이 아니다.
|
||||
이 구분을 지키면 React Router를 교체해도 application input과 output port는
|
||||
유지된다.
|
||||
|
||||
rollback은 RP-04 merge를 되돌려 이전 수동 router와 generic route failure
|
||||
surface로 복구한다. URL shape와 application API는 유지하고, 이미 배포된 asset
|
||||
cache의 purge는 저장소 rollback 범위에 포함하지 않는다.
|
||||
@@ -1,55 +0,0 @@
|
||||
# VD-04: Native form controller와 local facade
|
||||
|
||||
- 상태: Accepted
|
||||
- 결정일: 2026-07-26
|
||||
- 적용 브랜치: `feature-frontend-form-page-platform`
|
||||
|
||||
## 배경
|
||||
|
||||
플랫폼에는 Zod가 이미 설치돼 있지만 form state, field error, dirty navigation과
|
||||
page template 계약은 없었다. React Hook Form과 resolver를 바로 추가하면
|
||||
dependency와 lockfile이 바뀌고, 현재 reference form에 필요하지 않은 복합 비동기
|
||||
field orchestration까지 플랫폼 기본값으로 고정하게 된다.
|
||||
|
||||
## 결정
|
||||
|
||||
1. RP-06은 React native form event와 controlled value를 사용하는 local
|
||||
`useAppForm` facade를 기본 엔진으로 채택한다.
|
||||
2. Zod presentation schema, application command mapper와 domain invariant는 서로
|
||||
다른 소유물로 유지한다.
|
||||
3. page와 feature는 `useAppForm`, `Form`, `FormField`, `ErrorSummary`,
|
||||
`mapValidationFailureToFields`, `useDirtyNavigationGuard`만 사용한다.
|
||||
4. 422 details는 승인된 `path`와 `code`만 HTTP 경계에서 투영한다. backend
|
||||
message와 알 수 없는 field는 field에 전달하지 않고 안전한 form-level
|
||||
error로 이동한다.
|
||||
5. 409 conflict는 validation으로 바꾸지 않으며 입력과 dirty 상태를 보존한다.
|
||||
6. pending submit은 동일 controller에서 한 번만 실행하고 success/reset 이후
|
||||
dirty 상태를 해제한다.
|
||||
7. `StandardPage`, `CollectionPage`, `DetailPage`, `FormPage`, `StatusPage`는
|
||||
layout과 state slot만 소유하며 application/query/HTTP를 import하지 않는다.
|
||||
|
||||
## React Hook Form 도입 조건
|
||||
|
||||
다음 중 하나가 실제 제품 요구로 확인되면 local facade 내부 adapter로
|
||||
React Hook Form과 Zod resolver를 평가한다.
|
||||
|
||||
- 동적 field array와 중첩 object를 함께 다루는 복합 form
|
||||
- field 단위 비동기 validation 취소와 의존 validation
|
||||
- 수백 개 field의 render isolation이 측정 가능한 병목인 경우
|
||||
- uncontrolled input 또는 vendor extension이 필요한 경우
|
||||
|
||||
도입하더라도 이 문서의 public API와 component/application tests를 유지해야
|
||||
한다. vendor package를 feature/page에서 직접 import하는 것은 허용하지 않는다.
|
||||
|
||||
## 검증과 rollback
|
||||
|
||||
- client validation, transform/default, 422 allowlist, conflict, duplicate submit,
|
||||
reset, dirty guard와 focus를 component test로 검증한다.
|
||||
- template 최소/전체 slot과 async/status variation을 component test로 검증한다.
|
||||
- architecture gate가 template의 application/HTTP/query vendor import를
|
||||
거절한다.
|
||||
- secret-like input이 URL, storage, diagnostics에 복제되지 않는지 검증한다.
|
||||
|
||||
rollback 시 reference page는 이전 직접 form/layout으로 돌아갈 수 있다.
|
||||
application input과 outbound gateway 계약은 유지되며, form facade와 template
|
||||
commit은 독립적으로 되돌릴 수 있다.
|
||||
@@ -1,57 +0,0 @@
|
||||
# VD-05: Semantic icon facade와 native-first interaction
|
||||
|
||||
- 상태: Accepted
|
||||
- 결정일: 2026-07-26
|
||||
- 적용 브랜치: `feature-frontend-design-system-platform`
|
||||
- 재검토: native 계약으로 충족할 수 없는 widget 요구가 확인될 때
|
||||
|
||||
## 배경
|
||||
|
||||
앱 셸과 공통 UI는 문자 glyph, raw button/select와 페이지별 focus 처리를
|
||||
사용했다. 아이콘 공급자와 복합 interaction을 제품 코드에 직접 노출하면 번들,
|
||||
접근성, vendor type과 교체 비용이 모든 feature로 전파된다. 반대로 실제 요구가
|
||||
없는 두 개의 headless vendor를 기본 설치하면 skeleton 소비자가 제거해야 할
|
||||
의존성과 중복 interaction 모델이 생긴다.
|
||||
|
||||
## 결정
|
||||
|
||||
1. 아이콘 공급자는 lockfile 최소 게시 유예를 통과한 `lucide-react@1.25.0`으로
|
||||
고정한다.
|
||||
2. `lucide-react`의 static named import는
|
||||
`design-system/icons/vendors/lucide.tsx` 한 파일에서만 허용한다.
|
||||
3. public API는 `MenuIcon`, `CloseIcon`, `WarningIcon` 같은 의미 이름만
|
||||
노출한다. vendor component type, icon name, stroke API와 dynamic icon
|
||||
registry는 노출하지 않는다.
|
||||
4. 장식 아이콘은 accessibility tree에서 제외한다. 정보를 단독 전달하는
|
||||
아이콘은 `label`, icon-only action은 필수 `accessibleName`을 사용한다.
|
||||
5. 현재 복합 control은 native `dialog`, form control, `details`와 local
|
||||
TypeScript state model로 구현한다. Menu는 roving focus/typeahead/Escape,
|
||||
Tabs는 manual/automatic activation, Drawer는 modal/background
|
||||
비활성화/focus restore 계약을 가진다.
|
||||
6. React Aria와 Radix는 기본 dependency로 추가하지 않는다. native platform이
|
||||
collision, nested overlay, virtualized collection 또는 복합 select 요구를
|
||||
충족하지 못한다는 재현 가능한 요구가 생길 때 prototype과 ADR로 다시
|
||||
평가한다.
|
||||
7. Storybook과 pinned visual baseline은 VD-08/RP-10에서 도입한다. RP-07의
|
||||
runtime gallery와 browser interaction test는 해당 workshop을 대체한다고
|
||||
주장하지 않는다.
|
||||
|
||||
## 경계와 검증
|
||||
|
||||
- 제품 코드는 `presentation/design-system/index`만 import한다.
|
||||
- design-system 검사기는 direct icon/headless import, deep import, raw palette,
|
||||
undefined token과 tooltip-only required information fixture를 거절한다.
|
||||
- type negative fixture는 accessible name 없는 `IconButton`을 거절한다.
|
||||
- component test는 decorative icon, form control, Menu, Tabs, Drawer와 Toast를
|
||||
검증한다.
|
||||
- Chromium/Firefox E2E는 compact Drawer의 native modal 상태, Escape, focus
|
||||
restore, gallery keyboard interaction과 axe를 검증한다.
|
||||
- 로컬 WebKit 실행은 host `libevent-2.1.so.7` 부재로 환경 검증이 남아 있으며
|
||||
공급자 선택이나 product behavior의 PASS로 숨기지 않는다.
|
||||
|
||||
## Rollback
|
||||
|
||||
기존 `presentation/components/ui/*` 경로는 canonical TypeScript primitive를
|
||||
재수출하므로 소비 코드를 즉시 되돌릴 수 있다. Lucide 제거 시 vendor facade와
|
||||
semantic icon 구현만 교체하고 제품 API는 유지한다. headless vendor를 나중에
|
||||
도입해도 public props와 interaction test를 유지한다.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user