Compare commits
149
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2420fcee9f | ||
|
|
2632850322 | ||
|
|
ad8f322b43 | ||
|
|
ab4d822956 | ||
|
|
344a163d84 | ||
|
|
60c8c82097 | ||
|
|
b3119952d5 | ||
|
|
7211dd1a92 | ||
|
|
3036b8d788 | ||
|
|
fd73bc88a1 | ||
|
|
e5770dfbc8 | ||
|
|
a7fe069a6d | ||
|
|
ab67eb912c | ||
|
|
e9b866184a | ||
|
|
84d72c4f60 | ||
|
|
6b9dc3f0a2 | ||
|
|
f9d20e8946 | ||
|
|
014f21b9e1 | ||
|
|
16e5b9f4ec | ||
|
|
0eb3c86839 | ||
|
|
c87e0a2338 | ||
|
|
b3aa304975 | ||
|
|
c03b0c77b8 | ||
|
|
1801414592 | ||
|
|
7345500ef3 | ||
|
|
fb478f951b | ||
|
|
7289ce97bb | ||
|
|
348420618d | ||
|
|
89a73c13c6 | ||
|
|
21f8425f1e | ||
|
|
7093d84ab5 | ||
|
|
d2c289c650 | ||
|
|
5cffe30200 | ||
|
|
197b2c7e72 | ||
|
|
c5e8735041 | ||
|
|
ab8c6c14db | ||
|
|
3754269118 | ||
|
|
760071156d | ||
|
|
03986da3d6 | ||
|
|
31dca00857 | ||
|
|
11c2713139 | ||
|
|
4b62bf3b1f | ||
|
|
6784eb1ce6 | ||
|
|
24c01aedf2 | ||
|
|
4566f2d7a8 | ||
|
|
c362ec6100 | ||
|
|
83409bef7a | ||
|
|
5e2b1a5586 | ||
|
|
f1498feee5 | ||
|
|
5fe355483e | ||
|
|
44caa477e3 | ||
|
|
fff5e6f59e | ||
|
|
eb86708076 | ||
|
|
68c5dbdaa3 | ||
|
|
4090c8681d | ||
|
|
b75c9d0956 | ||
|
|
3e2406349a | ||
|
|
aaaf0ac343 | ||
|
|
9244f5c15d | ||
|
|
d23a18f659 | ||
|
|
25a6b63d27 | ||
|
|
d84b57bb3f | ||
|
|
cf45bcc7dc | ||
|
|
b2f577ef49 | ||
|
|
2483c4032f | ||
|
|
d2574a3be7 | ||
|
|
9950cb9d6b | ||
|
|
1f2cba79e9 | ||
|
|
f69edb633d | ||
|
|
a5825c18b5 | ||
|
|
419d9d006d | ||
|
|
74281b0277 | ||
|
|
1a40522ff8 | ||
|
|
ef17bca445 | ||
|
|
813f9e16cd | ||
|
|
a889cb5c00 | ||
|
|
65f8528ccc | ||
|
|
e2a0e695dd | ||
|
|
e4f9f81a3f | ||
|
|
172497591f | ||
|
|
6085af51b6 | ||
|
|
f19be639a3 | ||
|
|
5c997f3a7e | ||
|
|
83d47e7185 | ||
|
|
3ab04a236d | ||
|
|
b11aa94f1c | ||
|
|
073fda87eb | ||
|
|
783e9b2cf1 | ||
|
|
7ff9728a5c | ||
|
|
98649585e6 | ||
|
|
54d9bf9120 | ||
|
|
e2c1d076f4 | ||
|
|
9d91001a31 | ||
|
|
0c071aaabc | ||
|
|
35cc5c868a | ||
|
|
2cab4974b7 | ||
|
|
c9c832c365 | ||
|
|
d9c2d8bc5e | ||
|
|
7724a8720c | ||
|
|
3b641906b8 | ||
|
|
7424ed4594 | ||
|
|
1d01a522de | ||
|
|
9313018ef5 | ||
|
|
66c047cec8 | ||
|
|
a6fc536d8a | ||
|
|
6c40c291d9 | ||
|
|
7381be1477 | ||
|
|
a0e0be6522 | ||
|
|
639e1a49c9 | ||
|
|
ac555a85e8 | ||
|
|
9f0599a389 | ||
|
|
78766accdf | ||
|
|
12329ec9c0 | ||
|
|
9e5fbd1384 | ||
|
|
93ce86eef4 | ||
|
|
cd1ef5cda2 | ||
|
|
7d1ccccbcd | ||
|
|
0355b644a0 | ||
|
|
79e9aa8328 | ||
|
|
72669949bc | ||
|
|
47cbf4ddca | ||
|
|
3a7c5deca0 | ||
|
|
6c2780b7a7 | ||
|
|
c5c8b9423c | ||
|
|
9c6906fc6f | ||
|
|
5933265975 | ||
|
|
887f5e6eb1 | ||
|
|
2b6fa42620 | ||
|
|
4283e40bb2 | ||
|
|
512aa4a1e9 | ||
|
|
ef1d5cc548 | ||
|
|
c9164c1a03 | ||
|
|
3bc74e0195 | ||
|
|
bdee07a93b | ||
|
|
627df884dd | ||
|
|
01316763e3 | ||
|
|
adb8613cb9 | ||
|
|
27dda3e0e3 | ||
|
|
708680b28e | ||
|
|
8342fb14dc | ||
|
|
16753f53af | ||
|
|
82f94423e5 | ||
|
|
01ed1e9300 | ||
|
|
5479101c8c | ||
|
|
034b702e8e | ||
|
|
05e3d50ba0 | ||
|
|
954ca8a1fd | ||
|
|
325a2a0843 | ||
|
|
c2d03165b3 |
@@ -156,6 +156,41 @@
|
|||||||
"path": "^(src/(presentation|bootstrap)|react|react-dom|@tanstack)"
|
"path": "^(src/(presentation|bootstrap)|react|react-dom|@tanstack)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "contracts-do-not-know-application",
|
||||||
|
"comment": "§4. `src/contracts` is the lower of the two packages: application reads contracts, never the other way round. Before this rule the shared Result carrier and the compatibility predicate lived in application and were imported back down by contracts, so neither package owned the shared vocabulary and the coupling was invisible to every gate.",
|
||||||
|
"severity": "error",
|
||||||
|
"from": {
|
||||||
|
"path": "^src/contracts"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"path": "^src/(application|features)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "generic-presentation-does-not-compose-the-product",
|
||||||
|
"comment": "§4 / §9. Which features are installed is a product decision that belongs to bootstrap. Generic presentation reads the installed registries directly today; the paths below are the exact set that does so, frozen so the coupling cannot spread while the assembly is lifted into bootstrap.",
|
||||||
|
"severity": "error",
|
||||||
|
"from": {
|
||||||
|
"path": "^src/presentation/",
|
||||||
|
"pathNot": "^src/presentation/(layouts/app-shell\\.tsx|pages/(not-found-page|home-page)\\.tsx|routes/(route-contract|route-codecs|app-router|navigation-policy)\\.(ts|tsx)|i18n/catalog\\.ts|examples/platform-overview-page\\.tsx)$"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"path": "^src/features/installed-"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "adapters-do-not-know-other-concrete-adapters",
|
||||||
|
"comment": "docs/architecture/layers.md §4: a concrete adapter never depends on another concrete adapter. Only the adapter kernel is shared — `src/adapters/platform` (clock, abort primitive, capacity guard) and the browser-data result helpers. `query-cache` still reads two collaborator types from `cross-context-invalidation`; that edge is named here rather than left silent, and closes when those types are lifted to a port.",
|
||||||
|
"severity": "error",
|
||||||
|
"from": {
|
||||||
|
"path": "^src/adapters/([^/]+)/"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"path": "^src/adapters/([^/]+)/",
|
||||||
|
"pathNot": "^src/adapters/($1/|platform/|browser-file-storage/result\\.ts$|cross-context-invalidation/index\\.ts$)"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "no-circular-dependencies",
|
"name": "no-circular-dependencies",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Build-time inputs (§6.1). These are compiled into the bundle by Vite, so
|
||||||
|
# everything here is public by definition. Never put a secret in this file or in
|
||||||
|
# any `.env*` file: a frontend has no confidential storage, and a value that
|
||||||
|
# reaches the browser has been published.
|
||||||
|
#
|
||||||
|
# Runtime configuration — API endpoints, auth mode, telemetry, capability
|
||||||
|
# switches — is NOT here. It lives in `config/runtime/<profile>.json` and is
|
||||||
|
# materialized into `dist/config.json` at build time, so it can be changed
|
||||||
|
# without rebuilding. See docs/architecture/layers.md.
|
||||||
|
#
|
||||||
|
# Copy to `.env.local` (git-ignored) to override locally.
|
||||||
|
|
||||||
|
# Identifies the build in release manifests and the runtime document.
|
||||||
|
# CI supplies the real value; a developer build falls back to "local-build".
|
||||||
|
VITE_BUILD_ID=local-build
|
||||||
|
|
||||||
|
# Source revision the bundle was produced from.
|
||||||
|
VITE_COMMIT_SHA=local
|
||||||
|
|
||||||
|
# Sub-path the app is served under. Must start and end with "/".
|
||||||
|
# Feeds the router, the Service Worker scope and Vite's asset base together.
|
||||||
|
VITE_ROUTER_BASE_PATH=/
|
||||||
|
|
||||||
|
# Where the browser fetches the runtime document from at boot.
|
||||||
|
VITE_RUNTIME_CONFIG_URL=/config.json
|
||||||
@@ -126,6 +126,9 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
dist_sha256: ${{ steps.candidate.outputs.dist_sha256 }}
|
dist_sha256: ${{ steps.candidate.outputs.dist_sha256 }}
|
||||||
archive_sha256: ${{ steps.candidate.outputs.archive_sha256 }}
|
archive_sha256: ${{ steps.candidate.outputs.archive_sha256 }}
|
||||||
|
env:
|
||||||
|
APP_PROFILE: "${{ vars.APP_PROFILE }}"
|
||||||
|
RELEASE_TARGET: "${{ vars.RELEASE_TARGET }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
@@ -187,6 +190,7 @@ jobs:
|
|||||||
scripts/lib/secret-scan.ts \
|
scripts/lib/secret-scan.ts \
|
||||||
scripts/lib/supply-chain.ts \
|
scripts/lib/supply-chain.ts \
|
||||||
scripts/lib/validated-json-artifact.ts \
|
scripts/lib/validated-json-artifact.ts \
|
||||||
|
scripts/lib/vite-route-chunks.ts \
|
||||||
src/contracts/release-artifacts.ts \
|
src/contracts/release-artifacts.ts \
|
||||||
src/features/installed-contract-contributions.ts \
|
src/features/installed-contract-contributions.ts \
|
||||||
src/features/installed-feature-contracts.ts \
|
src/features/installed-feature-contracts.ts \
|
||||||
|
|||||||
+12
@@ -6,6 +6,7 @@ dist/
|
|||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
coverage/
|
coverage/
|
||||||
|
.worktrees/
|
||||||
!tests/fixtures/coverage/
|
!tests/fixtures/coverage/
|
||||||
!tests/fixtures/coverage/below-threshold.json
|
!tests/fixtures/coverage/below-threshold.json
|
||||||
artifacts/**/*.json
|
artifacts/**/*.json
|
||||||
@@ -18,3 +19,14 @@ artifacts/storybook/
|
|||||||
artifacts/tests/storybook/
|
artifacts/tests/storybook/
|
||||||
artifacts/tests/visual/
|
artifacts/tests/visual/
|
||||||
!artifacts/**/.gitkeep
|
!artifacts/**/.gitkeep
|
||||||
|
|
||||||
|
# Local environment overrides. `.env.example` is the tracked template; every
|
||||||
|
# other `.env*` file is a developer's own machine and never enters the repo.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Git worktrees created inside the repository. A worktree is a checkout, not
|
||||||
|
# source: committing one would nest a second working copy inside this one.
|
||||||
|
.worktrees/
|
||||||
|
.playwright-mcp/
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import { ApplicationProvider } from "../src/presentation/providers/application-p
|
|||||||
import { SessionProvider } from "../src/presentation/providers/session-provider.tsx";
|
import { SessionProvider } from "../src/presentation/providers/session-provider.tsx";
|
||||||
import { ThemeProvider } from "../src/presentation/providers/theme-provider.tsx";
|
import { ThemeProvider } from "../src/presentation/providers/theme-provider.tsx";
|
||||||
import "../src/presentation/styles/theme.css";
|
import "../src/presentation/styles/theme.css";
|
||||||
|
import { resolveProductFeatures } from "../src/contracts/product-features.ts";
|
||||||
|
import {
|
||||||
|
COMPILED_PRODUCT_FEATURE_IDS,
|
||||||
|
INSTALLED_PRODUCT_FEATURE_IDS,
|
||||||
|
} from "../src/features/installed-product-manifest.ts";
|
||||||
|
|
||||||
const preferences = new Map<string, unknown>();
|
const preferences = new Map<string, unknown>();
|
||||||
const application = createApplication({
|
const application = createApplication({
|
||||||
@@ -45,6 +50,16 @@ const application = createApplication({
|
|||||||
routeChunks: {},
|
routeChunks: {},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
// Storybook renders components, not a product: every declared feature is
|
||||||
|
// shown as active so a story is never blank because of a deployment switch.
|
||||||
|
productFeatures: {
|
||||||
|
getSnapshot: () =>
|
||||||
|
resolveProductFeatures(
|
||||||
|
COMPILED_PRODUCT_FEATURE_IDS,
|
||||||
|
INSTALLED_PRODUCT_FEATURE_IDS,
|
||||||
|
),
|
||||||
|
isActive: () => true,
|
||||||
|
},
|
||||||
runtimeCapabilities: {
|
runtimeCapabilities: {
|
||||||
getSnapshot: () =>
|
getSnapshot: () =>
|
||||||
Object.freeze(
|
Object.freeze(
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Task 10 report
|
||||||
|
|
||||||
|
## Mapping
|
||||||
|
|
||||||
|
- Source Studio provider/runtime shell and header → application-input-created, provider-scoped gateway; React Router navigation; native Public link; persisted `pageshow` generation reset.
|
||||||
|
- Source dashboard → exact workspace heading, totals, workflow sections, row labels, links, loading and error copy.
|
||||||
|
- Source document list → exact search/filter/list/empty surfaces plus cursor pagination, retry, and abort of obsolete requests.
|
||||||
|
- Source new-document form → exact type cards/copy, session gateway creation, announcement, and editor redirect.
|
||||||
|
- Source Studio not-found → in-shell 404 surface; Studio routes remain public with no auth UI.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx` failed both suites at missing Studio presentation imports (exit 1).
|
||||||
|
- GREEN: the same command passed 2 files / 8 tests.
|
||||||
|
- Focused regression: both Studio suites plus `tests/features/tech-log/runtime-composition.test.ts` passed 3 files / 10 tests.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- Added the 12 Task 10 Studio provider/runtime/shell/component/page files under `src/features/tech-log/presentation/studio/`.
|
||||||
|
- Added `studio-shell-smoke.test.tsx` and `studio-screens-smoke.test.tsx`.
|
||||||
|
|
||||||
|
## SHA
|
||||||
|
|
||||||
|
- Base: `2b6fa42620136c3edb1506f907ce79c2251d1316`.
|
||||||
|
- Implementation: the commit containing this report, titled `feat: port TechLog Studio shell and indexes` (final SHA recorded in the Task 10 handoff).
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- Task 11 editor screens and Task 12 dirty-leave/save/validation dialogs remain intentionally deferred.
|
||||||
|
- Broad architecture, type, lint, build, security, and visual gates remain deferred to Task 14 by user direction.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Task 11 report
|
||||||
|
|
||||||
|
## Mapping
|
||||||
|
|
||||||
|
- Source common, Case, Reference, and Question fields → exact target labels, controls, order, loaded values, conditional resolution fields, and CSS classes.
|
||||||
|
- Source ordered text/rule/option/relation editors → presentation-owned add, remove, reorder, local IDs, limits, and accessibility names.
|
||||||
|
- Source document editor/status rail → source tabs, keyboard focus, working-copy status, dirty indicator, version/kind rail, and deferred workflow controls.
|
||||||
|
- Source instant preview → Content Format v1 `projectWorkingCopy` plus the shared `PublicRecordRenderer`; no gateway preview mutation or parser/renderer duplication.
|
||||||
|
- Source edit page → registered route input adaptation for the document ID.
|
||||||
|
- Task 10 provider seam → smallest generic provider-owned editor session (`saved`, `draft`, `status`) retained across editor tabs.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-editor-smoke.test.tsx` failed at the exact missing `document-editor-screen.tsx` import before test collection.
|
||||||
|
- First GREEN: the same focused command passed 1 file / 5 tests.
|
||||||
|
- Focused regression: editor smoke plus `content-format.test.ts` and `public-render.test.tsx` passed 3 files / 41 tests.
|
||||||
|
- Scope check: `git diff --check` passed.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- Added the 12 Task 11 component/page files under `src/features/tech-log/presentation/studio/`.
|
||||||
|
- Extended `studio-provider.tsx` and `use-studio.ts` only with presentation-owned editor session state.
|
||||||
|
- Added `tests/features/tech-log/studio-editor-smoke.test.tsx`.
|
||||||
|
|
||||||
|
## SHA
|
||||||
|
|
||||||
|
- Base: `887f5e6eb1a5ccdf4feab0b27fae1c5823233190`.
|
||||||
|
- Implementation: the commit containing this report, titled `feat: port TechLog Studio editors` (final SHA recorded in the Task 11 handoff).
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- Save/conflict resolution, guarded navigation, validation, server preview, publish, and unpublish workflows remain deferred to Tasks 12/13. The source Save control is present but disabled until Task 12 supplies the workflow.
|
||||||
|
- Broad app/test types, lint, build, architecture, security, visual, and full-suite gates remain deferred to integrated Task 14 by user direction.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Task 12 report
|
||||||
|
|
||||||
|
## Mapping
|
||||||
|
|
||||||
|
- Source editor save workflow → pending/success announcements, fresh per-command idempotency keys, retry after request failure, and revision-conflict state without replacing the local draft.
|
||||||
|
- Source guarded Studio links/provider/dialog → all internal Studio anchors use the guarded `<a>` DOM; dirty navigation offers stay, discard, and save-then-navigate with modal focus/trigger restoration and native `beforeunload` protection.
|
||||||
|
- Source validation report/screen → saved-version validation gates, current/stale freshness copy, error-before-warning issue order, exact JSON-pointer editor anchors, retry/not-found surfaces, and abortable route reads.
|
||||||
|
- Source Public Preview screen → missing/current/stale/expired states, exact next-action labels, idempotent preview creation, retry/not-found surfaces, and the shared typed `PublicRecordRenderer`.
|
||||||
|
- Route pages → existing route-input codecs supply the validation/preview document ID; no publish/history/snapshot behavior was pulled forward.
|
||||||
|
- Task 10/11 seam → provider gained only dirty-navigation/time state, editor gained the save callback, and existing Studio links were switched to the newly available source guarded-link component.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
- RED: `corepack pnpm exec vitest run tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx` exited 1 before collection at the intentionally missing `guarded-studio-link.tsx` and `public-preview-screen.tsx` imports (2 failed files, 0 tests).
|
||||||
|
- GREEN: the two workflow suites plus `tests/features/tech-log/mock-studio-gateway.test.ts` passed 3 files / 22 tests.
|
||||||
|
- Focused seam regression: those three files plus the existing Studio shell, screen, and editor suites passed 6 files / 35 tests.
|
||||||
|
- Scope check: `git diff --check` passed.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- Added guarded link, unsaved dialog, beforeunload hook, validation report/screen, Public Preview screen, and validation/preview route pages under `src/features/tech-log/presentation/studio/`.
|
||||||
|
- Extended the Task 10 provider/context and Task 11 editor/status rail; updated existing Studio internal link consumers to use the source guard.
|
||||||
|
- Added `studio-save-navigation.test.tsx` and `studio-validation-preview.test.tsx`.
|
||||||
|
|
||||||
|
## SHA
|
||||||
|
|
||||||
|
- Base: `59332659752ee17c095471d06e7f0fc8b00c89b4`.
|
||||||
|
- Implementation: the commit containing this report, titled `feat: port TechLog Studio validation workflow` (final SHA recorded in the Task 12 handoff).
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- Publish, republish, unpublish, publication history, warning acknowledgement, and immutable publication snapshots remain deferred to Task 13.
|
||||||
|
- Broad browser-security, app/test types, lint, build, architecture, visual, and full-suite gates remain deferred to integrated Task 14 by user direction.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Task 13 report: publication flow and atomic install
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
`DONE_WITH_CONCERNS`
|
||||||
|
|
||||||
|
Base SHA: `9c6906fc6f76115346360d050dd8c211fee1b5a9`
|
||||||
|
|
||||||
|
Delivery commit: the commit containing this report, with subject `feat: complete TechLog Studio publication flow`.
|
||||||
|
|
||||||
|
## Publication mapping
|
||||||
|
|
||||||
|
- Added the source-faithful publish screen, warning acknowledgements, publication history/filter, unpublish dialog, immutable event snapshot preview, and their three route pages.
|
||||||
|
- Publish blocks invalid or stale validation, requires every warning acknowledgement, creates a fresh idempotency key per command/retry, preserves gateway command ordering, and exposes pending/error/retry states.
|
||||||
|
- Unpublish preserves the reason/confirmation contract. Historical preview reads the event-owned immutable snapshot and renders it through the shared `PublicRecordRenderer`; a missing/unknown event stays inside Studio.
|
||||||
|
- Added `tests/features/tech-log/studio-publication-flow.test.tsx` first. The initial red was the exact missing `publication-list.tsx` module/screen; the implemented suite is green at 7 tests.
|
||||||
|
|
||||||
|
## Atomic install and removals
|
||||||
|
|
||||||
|
- Installed exactly the 27 governed TechLog route definitions, codecs, runtime imports, module identities, message catalogs, schemas, and release-manifest chunk IDs. `PublicShell` and `StudioShell` are the grouped layout elements.
|
||||||
|
- Added governed Vite chunk naming for the 27 route module identities and changed the performance probe to `TECH_LOG_HOME`.
|
||||||
|
- Kept the reference contract/adapters and API/schema/invalidation/platform fixture tests, while removing its presentation runtime/pages and page-level tests.
|
||||||
|
- Removed the four sample presentation pages, starter home/not-found pages, their page-level component/E2E screens, and all four `/examples/*` E2E specs authorized by the brief.
|
||||||
|
- Added `tests/e2e/tech-log-studio-workflow.spec.ts`; it was deliberately not run and is deferred to Task 14.
|
||||||
|
- Updated the removal fixture to retain TechLog after reference removal and to exclude tests whose only contract is the removed reference runtime or the canonical (non-reduced) CI authority.
|
||||||
|
- Split `DocumentEditorController` into a type-only module to remove the editor/status-rail cycle exposed by the installed route graph.
|
||||||
|
|
||||||
|
## Verification evidence
|
||||||
|
|
||||||
|
- Publication + validation-preview + mock gateway: PASS, 3 files / 23 tests.
|
||||||
|
- Router + runtime application + retained reference contract: PASS, 3 files / 16 tests.
|
||||||
|
- Final route contract + navigation policy: PASS, 2 files / 10 tests.
|
||||||
|
- Registry structure: PASS, 11 registries.
|
||||||
|
- Release manifest inventory: PASS, exactly 27 derived chunk IDs.
|
||||||
|
- `git diff --check`: PASS.
|
||||||
|
- `test:sample-removal` was run once. Its isolated home smoke passed 9/9 and registry/CI reduced-contract checks passed, but its internally broad type/architecture/unit/coverage/build loop failed. Task-owned findings were fixed afterward: ES-target-incompatible `toSorted`, stale `APP_HOME`, direct adapter import, editor/status-rail cycle, reference-dependent fixture residue, canonical-CI-only tests in a reduced fixture, and missing governed build chunk names. Per fast-mode direction, that several-minute broad loop was not rerun; Task 14 must confirm the fixes through its integrated gates.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- Publication/UI/runtime: `src/features/tech-log/presentation/**`, including the five publication components, three pages, route runtime, and controller boundary.
|
||||||
|
- Contracts/install: `src/contracts/{routes,route-runtime-contract}.ts`, `src/features/installed-feature-*.{ts,tsx}`, TechLog route contract, platform codecs/runtime, router, layout reference, registry governance, Vite config, release manifest, performance/removal scripts.
|
||||||
|
- Tests: new publication flow and Studio workflow specs; updated route/router/runtime/reference/navigation expectations; authorized sample/reference presentation test deletions.
|
||||||
|
|
||||||
|
## Task 14 deferred concerns
|
||||||
|
|
||||||
|
- Run the complete integrated review/gates, including the sample-removal loop with the post-fix code, production build/manifest verification, types, lint, architecture, security, and Playwright workflow.
|
||||||
|
- Confirm the governed Vite chunk names in the generated production manifest and assess any unrelated environment/timing failures from the broad isolated fixture.
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
# Task 14 report: integrated TechLog parity and release verification
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
`DONE_WITH_CONCERNS`. The production serving correction, 130-case
|
||||||
|
source-to-target comparison, full recursive product-tree evidence, direct HTTP
|
||||||
|
contract, target visuals, focused browser suites, and static gates pass. The
|
||||||
|
automated Chromium accessibility suite passes 29/29, but the 27 signed human
|
||||||
|
keyboard/focus/screen-reader records remain `PENDING`; `FE-GATE-009` is not
|
||||||
|
claimed as passing. The other concern is the repository's pre-existing
|
||||||
|
restricted-runner `test:all` baseline: 19 provider-environment cases remain
|
||||||
|
red. The isolation and exact counts below prove that no TechLog test is among
|
||||||
|
those failures.
|
||||||
|
|
||||||
|
The work remains on `feature/techlog-ui-migration`. It was not merged, pushed,
|
||||||
|
finished with GitFlow, or deleted. The immutable code candidate is
|
||||||
|
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`
|
||||||
|
(`fix: complete TechLog migration evidence`). This report and the durable
|
||||||
|
parity JSON are deliberately recorded afterward in
|
||||||
|
`docs: record TechLog migration evidence`, so the evidence can name the exact
|
||||||
|
candidate it verifies.
|
||||||
|
|
||||||
|
## Evidence files
|
||||||
|
|
||||||
|
Added or replaced product evidence:
|
||||||
|
|
||||||
|
- `tests/visual/tech-log.visual.spec.ts` and 129 target-only PNG snapshots. The
|
||||||
|
suite has 130 cases because canonical and state coverage for the 1440-pixel
|
||||||
|
`/studio/publications` screen deliberately share the same reviewed image.
|
||||||
|
- `scripts/lib/tech-log-production-server.ts`, the generated self-contained
|
||||||
|
`dist/server.mjs` build artifact, its serving contract/generator, and 39-case
|
||||||
|
direct HTTP regression coverage.
|
||||||
|
- `tests/e2e/tech-log-public-discovery.spec.ts`,
|
||||||
|
`tests/e2e/tech-log-accessibility.spec.ts`, and
|
||||||
|
`tests/e2e/tech-log-responsive.spec.ts`.
|
||||||
|
- `tests/support/browser/tech-log-fixtures.ts`, the checked Node 24 parity
|
||||||
|
runner, and durable
|
||||||
|
`docs/operations/evidence/tech-log-source-parity.json` evidence.
|
||||||
|
- Focused regressions in `tests/unit/vite-route-chunks.test.ts`,
|
||||||
|
`tests/unit/design-system-source.test.ts`, the bounded-body reader tests, the
|
||||||
|
router component suite, and TechLog feature suites.
|
||||||
|
- Governed registry, dependency, release, CI, and operations evidence in
|
||||||
|
`config/contracts`, `config/security`, `config/ci`, the generated Gitea
|
||||||
|
workflow, `README.md`, and
|
||||||
|
`docs/operations/techlog-ui-migration-baseline.md`.
|
||||||
|
|
||||||
|
Removed starter-only evidence:
|
||||||
|
|
||||||
|
- `tests/e2e/compact-smoke.spec.ts`,
|
||||||
|
`tests/e2e/design-system-interactions.spec.ts`, `tests/e2e/i18n.spec.ts`, and
|
||||||
|
`tests/e2e/theme.spec.ts`.
|
||||||
|
- `tests/visual/platform.visual.spec.ts` and all five platform visual PNGs.
|
||||||
|
|
||||||
|
The retained `app-shell`, registry-wide accessibility, and responsive suites
|
||||||
|
were rewritten around TechLog. No stale starter browser or visual snapshot is
|
||||||
|
left referenced.
|
||||||
|
|
||||||
|
## Source-to-target visual method and result
|
||||||
|
|
||||||
|
The supplied source at `/home/donghyeon/workspace/techlog-studio-frontend` was
|
||||||
|
never written. It was copied to `/tmp/techlog-source-parity.I0CBK7`; build and
|
||||||
|
Vinext runtime caches were created only in that temporary copy. The source
|
||||||
|
production server on `4375` and target `dist/server.mjs` production artifact on
|
||||||
|
`4174` were opened by one Playwright Chromium instance with two fresh contexts
|
||||||
|
and the following identical controls:
|
||||||
|
|
||||||
|
- device scale factor 1, light color scheme, `ko-KR`, `Asia/Seoul`, reduced
|
||||||
|
motion, service workers blocked, 1000-pixel viewport height, and full-page
|
||||||
|
screenshots;
|
||||||
|
- fixed clock `2026-08-14T01:00:00.000Z`, deterministic in-memory data,
|
||||||
|
`document.fonts.ready`, matching Pretendard/IBM Plex Mono font-face state,
|
||||||
|
and zero-duration animation, transition, and caret styles;
|
||||||
|
- no masks and no tolerance: exact RGBA pixel comparison, normalized recursive
|
||||||
|
product-subtree tags, ordered child nodes, complete classes, attributes,
|
||||||
|
text and ARIA relationships, layout diagnostics, response metadata, boot
|
||||||
|
lifecycle, and console/page/request failure collection.
|
||||||
|
|
||||||
|
The only attributes normalized by name are diagnostics-confirmed framework
|
||||||
|
outputs: React Router `data-discover`; Next Image `data-nimg`, `decoding`, and
|
||||||
|
`srcset`; and Next SSR `selected` for a controlled select. Generated React IDs
|
||||||
|
and CSS-module hashes are normalized by value; there is no broad attribute
|
||||||
|
omission.
|
||||||
|
|
||||||
|
The final external comparison command was:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TZ=Asia/Seoul corepack pnpm verify:tech-log-source-parity
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: **130/130 passed**, 0 failed, `totalDifferentPixels=0`, every recursive
|
||||||
|
DOM/class/attribute/text/ARIA tree equal, all HTTP metadata equal, and 0
|
||||||
|
unexplained source/target errors. Source
|
||||||
|
screenshots were temporary comparison inputs; none was copied into target
|
||||||
|
snapshots. The no-update target visual run also passed 130/130 with
|
||||||
|
`maxDiffPixels=0` and `maxDiffPixelRatio=0`.
|
||||||
|
|
||||||
|
The durable evidence identifies source-tree digest
|
||||||
|
`6724c2f898eefc62d2fc0ee695bccc3ae61a69c5153ed43c69f2cf99ee45bca5`,
|
||||||
|
candidate `3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, build-manifest digest
|
||||||
|
`3579650faa482f566553c00d8b4a05a05b4f7a1ab93b83e48676289bbcf02984`,
|
||||||
|
Vite-manifest digest
|
||||||
|
`cfd583ed7b6c27dce7f47389447608636b6b9df3e28df00c6416674da2c7c46d`,
|
||||||
|
case-inventory digest
|
||||||
|
`5689bcdb5d5637205cdeb92b7c57f72d99f0b039818b8f90afdde526bbafe0ac`,
|
||||||
|
and evidence-payload digest
|
||||||
|
`f046047beded19ce468be607dcf99c6b74d4e07323457ec7145c56d2f67d2c79`.
|
||||||
|
|
||||||
|
The comparison found and corrected actual integration defects rather than
|
||||||
|
accepting drift: the TechLog Tailwind bootstrap is loaded exactly once in the
|
||||||
|
same cascade order as source; the starter theme import is removed; CSS-module
|
||||||
|
class mapping, shell navigation/focus, publication labels and states, router
|
||||||
|
404 handling, and generated Vite route-chunk lookup now follow source. All five
|
||||||
|
source/target CSS pairs pass `cmp -s`; their hashes are recorded in the
|
||||||
|
operations baseline.
|
||||||
|
|
||||||
|
Source production returns missing dynamic slugs and an unmatched Public path as
|
||||||
|
HTTP 404, `text/plain;charset=UTF-8`, with the exact nine-byte body `Not Found`.
|
||||||
|
The target production boundary now returns that exact shell-free response;
|
||||||
|
known Public paths and all known Studio paths remain SPA-served, while an
|
||||||
|
unknown Studio path preserves the source's HTML Studio shell with HTTP 404.
|
||||||
|
This is an observed source-production contract and satisfies the planned
|
||||||
|
prohibition on a generic runtime error; it is not a redesign.
|
||||||
|
|
||||||
|
## Route, viewport, and state inventory
|
||||||
|
|
||||||
|
The 27 canonical contract routes were each compared at 360 and 1440 pixels:
|
||||||
|
|
||||||
|
- Public: `/`, `/explore`, `/explore/:kind`, `/cases/:slug`,
|
||||||
|
`/references/:slug`, `/questions/:slug`, `/topics/:slug`, `/projects`,
|
||||||
|
`/projects/:slug`, the `records`, `decisions`, and `activity` project views,
|
||||||
|
`/releases`, `/releases/:version`, `/profile`, `/search`, and `*`.
|
||||||
|
- Studio: `/studio`, `/studio/documents`, `/studio/documents/new`, the `edit`,
|
||||||
|
`validation`, `preview`, and `publish` document views,
|
||||||
|
`/studio/publications`, publication-event preview, and `/studio/*`.
|
||||||
|
|
||||||
|
All known Public fixtures were exercised: two cases, two references, two open
|
||||||
|
questions, three topics, both projects and all three nested views, and release
|
||||||
|
`0.1.0`. Ten unknown Public dynamic shapes were also compared at both widths.
|
||||||
|
|
||||||
|
The 130-case matrix is 54 canonical-route captures, 18 additional known Public
|
||||||
|
fixture captures, 12 home breakpoints (1180, 1179, 1050, 1024, 980, 900, 820,
|
||||||
|
768, 767, 420, 390, and 375), 19 Studio states, 20 unknown-Public captures, and
|
||||||
|
7 interactions. Studio states cover the
|
||||||
|
dashboard, list/new, Case/Reference/Question/conflict editors, valid/invalid
|
||||||
|
validation, current/missing/expired previews, ready/blocked publish,
|
||||||
|
publications/snapshot, missing document/publication, and unknown Studio route.
|
||||||
|
Interactions cover Public search, Studio mobile menu, immediate preview,
|
||||||
|
dirty-leave dialog, newly created current preview, unpublish confirmation, and
|
||||||
|
warning acknowledgement through publish-ready state.
|
||||||
|
|
||||||
|
## Browser, responsive, and accessibility outcomes
|
||||||
|
|
||||||
|
- Required four-spec Chromium command: **48/48 passed**. It covers Public
|
||||||
|
discovery, the full Studio workflow, responsive behavior, and accessibility.
|
||||||
|
- Responsive plus accessibility focused command: **26/26 passed**.
|
||||||
|
- Direct production HTTP contract: **39/39 passed**, including exact raw Public
|
||||||
|
404s, the in-shell Studio 404, known SPA routes, and boot documents.
|
||||||
|
- Exact target visual command: **130/130 passed** in 2.5 minutes with no masks
|
||||||
|
and zero pixel tolerance.
|
||||||
|
- Automated Chromium `@a11y`: **29/29 passed**. The 27 human review records are
|
||||||
|
intentionally pending, so `corepack pnpm review:a11y-manual` exits 1 and
|
||||||
|
lists missing status, candidate release ID, reviewer/signature/attestation,
|
||||||
|
reviewed time, M1-M7, and screen-reader evidence for every route.
|
||||||
|
- Keyboard/focus checks cover Public search dismissal/restoration, Studio mobile
|
||||||
|
navigation, dirty-leave and unpublish dialogs, labels, heading/landmark order,
|
||||||
|
and focus-visible behavior. Axe reports no violations in the required route
|
||||||
|
and state inventory. Overflow assertions pass at the compact and transition
|
||||||
|
widths, and no unexpected console, page, or request error remains.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||||
|
corepack pnpm exec playwright test tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||||
|
corepack pnpm exec playwright test tests/e2e/tech-log-http-contract.spec.ts --project=chromium
|
||||||
|
corepack pnpm exec playwright test tests/e2e/accessibility.spec.ts --project=chromium --grep @a11y
|
||||||
|
corepack pnpm exec playwright test tests/visual/tech-log.visual.spec.ts --config=playwright.visual.config.ts --project=chromium
|
||||||
|
corepack pnpm test:visual
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registry and supply-chain governance
|
||||||
|
|
||||||
|
The initial no-baseline registry artifact reported 23 migration-owned breaking
|
||||||
|
IDs. Each now has owner `tech-log-frontend`, a TechLog contract-version reason,
|
||||||
|
atomic route/runtime/manifest installation, same-release compatibility, and
|
||||||
|
rollback to `05e3d50ba01f01c27f257d2e9040c2bc413ea053`:
|
||||||
|
|
||||||
|
- Contract: `$contract:{allowedValues,breakingFields,fieldTypes,requiredFields}`.
|
||||||
|
- Removed route rows: `APP_HOME`, `EXAMPLES_AUTH`, `EXAMPLES_PLATFORM`,
|
||||||
|
`EXAMPLES_STATES`, `EXAMPLES_UI`, `REFERENCE_RESOURCE_DETAIL`,
|
||||||
|
`REFERENCE_RESOURCE_FORM`, `REFERENCE_RESOURCE_LIST`, and
|
||||||
|
`REFERENCE_RESOURCE_STATUS`.
|
||||||
|
- Runtime removals: the same nine route IDs.
|
||||||
|
- Runtime change: `NOT_FOUND:moduleId:field-changed`.
|
||||||
|
|
||||||
|
The governed update used exactly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
REGISTRY_BASELINE_OWNER=tech-log-frontend REGISTRY_BASELINE_REASON="Install approved TechLog Public and Studio route contract" node scripts/update-registry-baseline.ts artifacts/quality/registries.json
|
||||||
|
corepack pnpm check:registries
|
||||||
|
```
|
||||||
|
|
||||||
|
Final result: 11 registries pass, compatibility `none`, no unacknowledged
|
||||||
|
change. Approved snapshot digest:
|
||||||
|
`428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b`.
|
||||||
|
|
||||||
|
Six direct dependency additions have evidence owner `tech-log-frontend`,
|
||||||
|
reviewer `frontend-platform-security`, product-specific reason, and atomic
|
||||||
|
rollback: `@fontsource/ibm-plex-mono@5.3.0`, `pretendard@1.3.9`,
|
||||||
|
`remark-directive@4.0.0`, `remark-gfm@4.0.1`, `remark-parse@11.0.0`, and
|
||||||
|
`unified@11.0.5`. The dependency policy recognizes the font packages' OFL-1.1
|
||||||
|
license. Supply-chain generation covered 641 packages. The pre-existing
|
||||||
|
dependency baseline was not promoted; the denied promotion was unnecessary for
|
||||||
|
the regular verification path, which passes with the committed evidence.
|
||||||
|
|
||||||
|
## Sample removal and fresh verification
|
||||||
|
|
||||||
|
The final staged-candidate command passed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm test:sample-removal
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: **PASS (13 checks, no fixture IDs)**. Its internal evidence included
|
||||||
|
types; reduced architecture (382 modules/1,170 dependencies, 12 graph checks,
|
||||||
|
9 forbidden fixtures); 11 registry checks; runtime schema 3 files/40 tests;
|
||||||
|
unit 118/1,250; component 18/124; integration 8/74; recipes 2/17; coverage at
|
||||||
|
77.40% statements, 73.26% branches, 83.88% functions, and 80.02% lines; risk
|
||||||
|
coverage 381/381 with 76 thresholds; source evidence 202 files/129 baselines;
|
||||||
|
artifact/CI checks; router smoke 9/9; and production build.
|
||||||
|
|
||||||
|
Fresh completion commands and results:
|
||||||
|
|
||||||
|
| Command | Result |
|
||||||
|
| --- | --- |
|
||||||
|
| `corepack pnpm exec vitest run tests/features/tech-log` | 21 files, 170 tests passed |
|
||||||
|
| required four-spec Chromium command above | 48 tests passed |
|
||||||
|
| `corepack pnpm exec playwright test tests/e2e/tech-log-http-contract.spec.ts --project=chromium` | 39 tests passed |
|
||||||
|
| Chromium `@a11y` command above | 29 tests passed |
|
||||||
|
| `corepack pnpm test:visual` | 130 tests passed |
|
||||||
|
| `corepack pnpm check:types` | app/node/test/recipes/web-worker/service-worker passed |
|
||||||
|
| `corepack pnpm lint` | passed with 0 warnings |
|
||||||
|
| `corepack pnpm check:architecture` | 391 modules, 1,212 dependencies, 12 graph checks, 9 forbidden fixtures passed |
|
||||||
|
| `corepack pnpm check:design-system` | 48 tokens and vendor boundaries passed |
|
||||||
|
| `corepack pnpm check:i18n` | 194 keys across 4 locales passed |
|
||||||
|
| `corepack pnpm check:registries` | 11 registries passed; compatibility `none` |
|
||||||
|
| `corepack pnpm check:browser-security` | injection rejected; Public source maps absent |
|
||||||
|
| `corepack pnpm build` | 2,351 modules transformed; build and manifest completed |
|
||||||
|
| `git diff --check` | passed |
|
||||||
|
|
||||||
|
The fresh staged-candidate `corepack pnpm test:all` passed runtime schema 3/40,
|
||||||
|
then its unit phase passed 122 files/1,773 tests and failed 19 tests in only
|
||||||
|
`ci-artifact-contract`. The failures are the documented provider/cgroup,
|
||||||
|
RLIMIT/EMFILE, restrictive-umask, `/tmp`, timing, and identity environment
|
||||||
|
cases; no TechLog test failed. A pre-staging run had also exposed 39
|
||||||
|
release-inventory `APP_HOME` failures because the new serving files were not
|
||||||
|
yet visible to `git ls-files`; staging the complete candidate corrected that
|
||||||
|
test precondition, and all 39 disappeared. Its one aggregate guardian timeout
|
||||||
|
passed 1/1 in isolation and 21/21 in the fresh staged aggregate.
|
||||||
|
|
||||||
|
The earlier exact baseline-isolation command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts tests/unit/ci-workflow-generation.test.ts tests/unit/http-scenario-evidence.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
ran 3 files/529 tests: 510 passed; all 407 CI-workflow and all 14 HTTP-scenario
|
||||||
|
tests passed, leaving the same 19 environment-only CI-artifact cases. Direct
|
||||||
|
runs of the aggregate's remaining phases passed: component 18/124, integration
|
||||||
|
11/82 under the required child-process scope, reference feature 4/13, and
|
||||||
|
recipes 2/17. This environment-only baseline is also recorded in the
|
||||||
|
operations baseline.
|
||||||
|
|
||||||
|
## Fixes, branch audit, and handoff
|
||||||
|
|
||||||
|
Root-cause-driven fixes added regressions for generated Vite manifest chunk
|
||||||
|
resolution, source-compatible raw 404 responses, palette-source detection, and
|
||||||
|
abort rejection. Presentation integration corrections preserve source DOM,
|
||||||
|
ARIA, copy, assets, CSS, workflow state, and focus behavior; no design was
|
||||||
|
introduced. The branch-wide audit of
|
||||||
|
`05e3d50ba01f01c27f257d2e9040c2bc413ea053..HEAD` found no migrated
|
||||||
|
presentation import of adapters, Next.js, Vinext, or Cloudflare. The 27 route
|
||||||
|
chunks are present in the release manifest and derive from actual Vite output,
|
||||||
|
not hard-coded generated filenames.
|
||||||
|
|
||||||
|
The immutable code candidate
|
||||||
|
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a` contains the production 404
|
||||||
|
boundary, parity runner, tests, snapshots, and 27 pending human-review records.
|
||||||
|
Only after that commit existed was the target rebuilt cleanly and the final
|
||||||
|
130-case parity, visual, HTTP, accessibility, and static verification rerun.
|
||||||
|
This report and its JSON are committed separately as
|
||||||
|
`docs: record TechLog migration evidence`; later human accessibility evidence
|
||||||
|
must cite the candidate SHA, not the evidence-only commit.
|
||||||
|
|
||||||
|
The independent `final-review.md` remains the immutable review input with its
|
||||||
|
historical `CHANGES_REQUESTED` verdict. This candidate addresses its production
|
||||||
|
404 issue with a real build artifact and 39 direct HTTP tests; expands the
|
||||||
|
source matrix from 112 to 130 and makes recursive tree equality part of pass;
|
||||||
|
and replaces the missing `tsx` invocation with a checked Node runner and
|
||||||
|
durable provenance. Its accessibility inventory issue is structurally fixed
|
||||||
|
and automated Chromium coverage is green, but the reviewer-dependent 27 human
|
||||||
|
records deliberately remain pending. No review verdict was rewritten or
|
||||||
|
self-approved.
|
||||||
|
|
||||||
|
Manual completion requires a human to check out candidate
|
||||||
|
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, review every route according to
|
||||||
|
`docs/accessibility/manual-checklist.md`, fill each record's exact candidate
|
||||||
|
Release ID, reviewer, signature, attestation, reviewed time, M1-M7, and screen
|
||||||
|
reader result, commit that evidence separately, then rerun
|
||||||
|
`corepack pnpm review:a11y-manual`. Until then `FE-GATE-009` remains pending.
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
# 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
|
||||||
@@ -4,9 +4,9 @@ Initialized from `clean-architecture-frontend-template` revision
|
|||||||
`4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is
|
`4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is
|
||||||
recorded in `template.lock.json`.
|
recorded in `template.lock.json`.
|
||||||
|
|
||||||
A React/Vite reference implementation where architecture boundaries,
|
A React/Vite TechLog application where architecture boundaries, integration
|
||||||
integration behavior, release coherence, accessibility, performance, and
|
behavior, release coherence, accessibility, performance, and operations are
|
||||||
operations are executable contracts rather than conventions.
|
executable contracts rather than conventions.
|
||||||
|
|
||||||
## Start locally
|
## Start locally
|
||||||
|
|
||||||
@@ -28,26 +28,107 @@ corepack pnpm dev
|
|||||||
Runtime-public settings live in `public/config.json` and are validated before
|
Runtime-public settings live in `public/config.json` and are validated before
|
||||||
the product tree mounts. Client secrets are forbidden.
|
the product tree mounts. Client secrets are forbidden.
|
||||||
|
|
||||||
## Included starter experience
|
## TechLog experience
|
||||||
|
|
||||||
The default build mounts a domain-neutral application shell with a header,
|
The default build mounts the source-faithful TechLog Public and Studio
|
||||||
responsive sidebar, route focus management, session integration status, and a
|
experience. Public routes provide discovery, search, documents, topics,
|
||||||
persistent `system` / `light` / `dark` theme selector.
|
projects, releases, and profile content. `/studio` provides the session-scoped
|
||||||
|
mock authoring workflow: create, edit, validate, preview, publish, unpublish,
|
||||||
|
and immutable publication history.
|
||||||
|
|
||||||
| Route | Purpose |
|
The 27 canonical route definitions are divided into `PUBLIC` and `STUDIO`
|
||||||
| --- | --- |
|
nested layouts. Studio authentication remains deliberately deferred; its mock
|
||||||
| `/` | implementation readiness and starter links |
|
gateway state lasts for one Studio shell session and resets on a full document
|
||||||
| `/examples/ui` | buttons, fields, cards, alerts, badges, modal, and tokens |
|
load. The exact route, dependency, stylesheet, asset, and parity inventory is
|
||||||
| `/examples/states` | loading, refresh, empty, error, auth, forbidden, and not-found states |
|
recorded in
|
||||||
| `/examples/auth` | reactive external-auth integration seam |
|
[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md).
|
||||||
| `/examples/reference-resources` | removable, session-required reference feature |
|
|
||||||
|
|
||||||
`AUTH_MODE=demo` is credential-free and accepted only in local/development
|
`corepack pnpm build` emits a self-contained `dist/server.mjs` production
|
||||||
environments. Deployments use `AUTH_MODE=external` and provide the opaque auth
|
boundary. `corepack pnpm preview --host 127.0.0.1 --port 4174` serves known
|
||||||
owner described in
|
Public and Studio routes as SPA documents, preserves the in-shell Studio 404,
|
||||||
[`docs/architecture/starter-experience.md`](docs/architecture/starter-experience.md).
|
and returns source-exact raw `404 text/plain` responses for missing Public
|
||||||
The client route policy is user experience only; server authorization remains
|
content. With the read-only source temp-copy server on `4375`, run:
|
||||||
authoritative.
|
|
||||||
|
```bash
|
||||||
|
TECH_LOG_SOURCE_URL=http://127.0.0.1:4375 \
|
||||||
|
TECH_LOG_TARGET_URL=http://127.0.0.1:4174 \
|
||||||
|
corepack pnpm verify:tech-log-source-parity
|
||||||
|
```
|
||||||
|
|
||||||
|
### Studio backend source
|
||||||
|
|
||||||
|
`TECH_LOG_STUDIO_SOURCE` (`MOCK` | `HTTP`) selects which `StudioGateway`
|
||||||
|
adapter the composition root wires up. It defaults to `MOCK` — the
|
||||||
|
session-scoped in-memory Studio described above — so the existing Studio
|
||||||
|
workflow and its test suites are unaffected unless the switch is deliberately
|
||||||
|
turned on. Setting it to `HTTP` wires the HTTP `StudioGateway` instead, which
|
||||||
|
calls the canonical `@tech-log/studio-contract` operations against
|
||||||
|
`API_BASE_URL`. With no backend reachable at that URL, Studio still boots and
|
||||||
|
its shell renders; the specific panels that need the backend show an inline
|
||||||
|
"failed to load" state rather than a blank screen or an unhandled exception.
|
||||||
|
|
||||||
|
The switch is a field on the versioned runtime config document
|
||||||
|
(`RuntimeConfigV2`), not a build-time flag:
|
||||||
|
|
||||||
|
- `config/runtime/{local,development,staging,production}.json` are the
|
||||||
|
deployment profiles `corepack pnpm build` (via
|
||||||
|
`scripts/generate-runtime-config.ts`) materializes into `dist/config.json`
|
||||||
|
for a real build.
|
||||||
|
- `corepack pnpm dev` does not run that step. Plain `vite` serves
|
||||||
|
`public/config.json` (and `public/release-manifest.json`) verbatim as dev
|
||||||
|
fixtures — editing `config/runtime/local.json` alone has no effect on
|
||||||
|
`pnpm dev`. To exercise `HTTP` mode under `pnpm dev`, set
|
||||||
|
`TECH_LOG_STUDIO_SOURCE` in `public/config.json` directly.
|
||||||
|
|
||||||
|
### 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
|
## Architecture
|
||||||
|
|
||||||
@@ -61,11 +142,13 @@ contracts own cross-cutting registries
|
|||||||
```
|
```
|
||||||
|
|
||||||
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||||
`docs/architecture/starter-experience.md`. The removable vertical slice is
|
`docs/architecture/starter-experience.md`. TechLog is one feature boundary
|
||||||
under `src/features/reference-feature`; its domain, application input, HTTP
|
under `src/features/tech-log`. Its immutable Public catalog and session-scoped
|
||||||
adapter, contracts, route runtime, and presentation are installed through the
|
Studio mock gateway are injected through the application feature input;
|
||||||
feature contribution files in `src/features`. The generic starter routes
|
Public and Studio presentation code shares the typed content renderer without
|
||||||
continue to typecheck, test, and build after that contribution is removed.
|
importing concrete adapters. The retained reference feature remains a
|
||||||
|
non-product platform contract fixture and can be removed without changing the
|
||||||
|
TechLog route set.
|
||||||
|
|
||||||
### Platform capability review
|
### Platform capability review
|
||||||
|
|
||||||
@@ -117,6 +200,9 @@ corepack pnpm verify:release
|
|||||||
corepack pnpm check:registries
|
corepack pnpm check:registries
|
||||||
corepack pnpm drill:runbooks
|
corepack pnpm drill:runbooks
|
||||||
corepack pnpm check:ci
|
corepack pnpm check:ci
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log
|
||||||
|
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||||
|
corepack pnpm test:visual
|
||||||
```
|
```
|
||||||
|
|
||||||
`check:types`는 source, Node scripts/config와 tests를 분리된 TypeScript
|
`check:types`는 source, Node scripts/config와 tests를 분리된 TypeScript
|
||||||
@@ -144,7 +230,20 @@ corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
|||||||
Two gates intentionally need external evidence:
|
Two gates intentionally need external evidence:
|
||||||
|
|
||||||
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
|
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
|
||||||
for all six registered routes.
|
for all 27 registered routes:
|
||||||
|
`NOT_FOUND`, `TECH_LOG_CASE`, `TECH_LOG_EXPLORE`, `TECH_LOG_EXPLORE_KIND`,
|
||||||
|
`TECH_LOG_HOME`, `TECH_LOG_PROFILE`, `TECH_LOG_PROJECT`,
|
||||||
|
`TECH_LOG_PROJECTS`, `TECH_LOG_PROJECT_ACTIVITY`,
|
||||||
|
`TECH_LOG_PROJECT_DECISIONS`, `TECH_LOG_PROJECT_RECORDS`,
|
||||||
|
`TECH_LOG_QUESTION`, `TECH_LOG_REFERENCE`, `TECH_LOG_RELEASE`,
|
||||||
|
`TECH_LOG_RELEASES`, `TECH_LOG_SEARCH`, `TECH_LOG_STUDIO_DOCUMENTS`,
|
||||||
|
`TECH_LOG_STUDIO_DOCUMENT_EDIT`, `TECH_LOG_STUDIO_DOCUMENT_NEW`,
|
||||||
|
`TECH_LOG_STUDIO_DOCUMENT_PREVIEW`, `TECH_LOG_STUDIO_DOCUMENT_PUBLISH`,
|
||||||
|
`TECH_LOG_STUDIO_DOCUMENT_VALIDATION`, `TECH_LOG_STUDIO_HOME`,
|
||||||
|
`TECH_LOG_STUDIO_NOT_FOUND`, `TECH_LOG_STUDIO_PUBLICATIONS`,
|
||||||
|
`TECH_LOG_STUDIO_PUBLICATION_PREVIEW`, `TECH_LOG_TOPIC`.
|
||||||
|
`verify:documentation` derives that list from the route registry and fails if
|
||||||
|
this paragraph falls behind it.
|
||||||
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
||||||
eligible-sample threshold and 28 days of production data exist.
|
eligible-sample threshold and 28 days of production data exist.
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# APP_HOME accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: APP_HOME
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Automated axe, keyboard-focus, and reduced-motion evidence is available; human review pending.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# EXAMPLES_AUTH accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: EXAMPLES_AUTH
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Review session state announcements and unavailable external-integration behavior.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# EXAMPLES_PLATFORM accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: EXAMPLES_PLATFORM
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Review the horizontally scrollable registry tables for keyboard reachability of the scroll container, table caption and header association announced per row, capability status badges carrying their meaning in text rather than colour alone, and the release identity region announcing its update through aria-live without interrupting a reader mid row.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# EXAMPLES_STATES accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: EXAMPLES_STATES
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Review loading, refresh, empty, error, authentication, forbidden, and not-found announcements.
|
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# REFERENCE_RESOURCE_STATUS accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: REFERENCE_RESOURCE_STATUS
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Human review pending.
|
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_FORM accessibility review
|
# TECH_LOG_CASE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_FORM
|
Route ID: TECH_LOG_CASE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -15,4 +15,4 @@ M5 Error association: pending
|
|||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
# EXAMPLES_UI accessibility review
|
# TECH_LOG_EXPLORE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: EXAMPLES_UI
|
Route ID: TECH_LOG_EXPLORE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -15,4 +15,4 @@ M5 Error association: pending
|
|||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Review form primitives, Menu/Tabs keyboard behavior, Toast announcements, Tooltip supplemental copy, text-field error association and modal focus containment/restoration.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_EXPLORE_KIND accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_EXPLORE_KIND
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_LIST accessibility review
|
# TECH_LOG_HOME accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_LIST
|
Route ID: TECH_LOG_HOME
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_DETAIL accessibility review
|
# TECH_LOG_PROFILE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_DETAIL
|
Route ID: TECH_LOG_PROFILE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECTS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECTS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT_ACTIVITY accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT_ACTIVITY
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT_DECISIONS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT_DECISIONS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT_RECORDS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT_RECORDS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_QUESTION accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_QUESTION
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_REFERENCE accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_REFERENCE
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_RELEASE accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_RELEASE
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_RELEASES accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_RELEASES
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_SEARCH accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_SEARCH
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_ASSETS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_ASSETS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_DOCUMENTS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_DOCUMENTS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_DOCUMENT_EDIT accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_DOCUMENT_EDIT
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_DOCUMENT_NEW accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_DOCUMENT_NEW
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_DOCUMENT_PREVIEW accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_DOCUMENT_PREVIEW
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_DOCUMENT_PUBLISH accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_DOCUMENT_PUBLISH
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_DOCUMENT_VALIDATION accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_DOCUMENT_VALIDATION
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_HOME accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_HOME
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_NOT_FOUND accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_NOT_FOUND
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_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.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_PUBLICATIONS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_PUBLICATIONS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_PUBLICATION_PREVIEW accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_STUDIO_PUBLICATION_PREVIEW
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_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.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_TOPIC accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_TOPIC
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
+284
-42
@@ -174,6 +174,11 @@
|
|||||||
"script": "test:reference-feature",
|
"script": "test:reference-feature",
|
||||||
"expect": "pass"
|
"expect": "pass"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "test-tech-log",
|
||||||
|
"script": "test:tech-log",
|
||||||
|
"expect": "pass"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "test-recipes",
|
"id": "test-recipes",
|
||||||
"script": "test:recipes",
|
"script": "test:recipes",
|
||||||
@@ -323,6 +328,16 @@
|
|||||||
"expectedExitCode": 1,
|
"expectedExitCode": 1,
|
||||||
"expectedDiagnosticId": "duplicates routeId=DUPLICATE"
|
"expectedDiagnosticId": "duplicates routeId=DUPLICATE"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "check-tech-log-contract",
|
||||||
|
"script": "check:tech-log-contract",
|
||||||
|
"expect": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "check-dev-release-manifest",
|
||||||
|
"script": "check:dev-release-manifest",
|
||||||
|
"expect": "pass"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "build",
|
"id": "build",
|
||||||
"script": "build",
|
"script": "build",
|
||||||
@@ -472,6 +487,11 @@
|
|||||||
"id": "check-ci",
|
"id": "check-ci",
|
||||||
"script": "check:ci",
|
"script": "check:ci",
|
||||||
"expect": "pass"
|
"expect": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "check-release-admission",
|
||||||
|
"script": "check:release-admission",
|
||||||
|
"expect": "pass"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"artifactSchemas": [
|
"artifactSchemas": [
|
||||||
@@ -732,6 +752,12 @@
|
|||||||
"id": "sarif-secret-scan",
|
"id": "sarif-secret-scan",
|
||||||
"kind": "sarif",
|
"kind": "sarif",
|
||||||
"maxBytes": 67108864
|
"maxBytes": 67108864
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "json-deployment-admission",
|
||||||
|
"kind": "json",
|
||||||
|
"maxBytes": 67108864,
|
||||||
|
"executableSchemaId": "deployment-admission"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
@@ -885,6 +911,15 @@
|
|||||||
"test-reference-feature"
|
"test-reference-feature"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-tech-log-xml",
|
||||||
|
"path": "artifacts/tests/tech-log.xml",
|
||||||
|
"schemaId": "junit",
|
||||||
|
"production": "command-generated",
|
||||||
|
"producerCommandIds": [
|
||||||
|
"test-tech-log"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-optional-recipes-xml",
|
"id": "artifact-artifacts-tests-optional-recipes-xml",
|
||||||
"path": "artifacts/tests/optional-recipes.xml",
|
"path": "artifacts/tests/optional-recipes.xml",
|
||||||
@@ -1006,58 +1041,196 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
|
||||||
"path": "artifacts/tests/a11y-manual/APP_HOME.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_HOME.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_UI.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_CASE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
},
|
||||||
"review-a11y-manual"
|
{
|
||||||
]
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-ASSETS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_ASSETS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-TAXONOMY-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_TAXONOMY.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PROJECT-EDIT-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PROJECT_EDIT.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-RELEASES-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_RELEASES.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-RELEASE-EDIT-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_RELEASE_EDIT.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
||||||
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
|
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-report-json",
|
"id": "artifact-artifacts-tests-a11y-manual-report-json",
|
||||||
@@ -1602,6 +1775,21 @@
|
|||||||
"producerCommandIds": [
|
"producerCommandIds": [
|
||||||
"check-ci"
|
"check-ci"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-release-deployment-admission-json",
|
||||||
|
"path": "artifacts/release/deployment-admission.json",
|
||||||
|
"schemaId": "json-deployment-admission",
|
||||||
|
"production": "command-generated",
|
||||||
|
"producerCommandIds": [
|
||||||
|
"check-release-admission"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-quality-gates-FE-GATE-027-txt",
|
||||||
|
"path": "artifacts/quality/gates/FE-GATE-027.txt",
|
||||||
|
"schemaId": "text",
|
||||||
|
"production": "runner-generated"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"gates": [
|
"gates": [
|
||||||
@@ -1707,6 +1895,7 @@
|
|||||||
"test-integration",
|
"test-integration",
|
||||||
"test-http-scenario-evidence",
|
"test-http-scenario-evidence",
|
||||||
"test-reference-feature",
|
"test-reference-feature",
|
||||||
|
"test-tech-log",
|
||||||
"test-recipes"
|
"test-recipes"
|
||||||
],
|
],
|
||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-007-txt",
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-007-txt",
|
||||||
@@ -1716,6 +1905,7 @@
|
|||||||
"artifact-artifacts-quality-http-scenario-evidence-json",
|
"artifact-artifacts-quality-http-scenario-evidence-json",
|
||||||
"artifact-artifacts-quality-http-scenario-evidence-fixture-json",
|
"artifact-artifacts-quality-http-scenario-evidence-fixture-json",
|
||||||
"artifact-artifacts-tests-reference-feature-xml",
|
"artifact-artifacts-tests-reference-feature-xml",
|
||||||
|
"artifact-artifacts-tests-tech-log-xml",
|
||||||
"artifact-artifacts-tests-optional-recipes-xml"
|
"artifact-artifacts-tests-optional-recipes-xml"
|
||||||
],
|
],
|
||||||
"retentionClassId": "merge-cycle"
|
"retentionClassId": "merge-cycle"
|
||||||
@@ -1757,11 +1947,37 @@
|
|||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
|
||||||
"evidenceArtifactIds": [
|
"evidenceArtifactIds": [
|
||||||
"artifact-artifacts-tests-a11y-json",
|
"artifact-artifacts-tests-a11y-json",
|
||||||
"artifact-artifacts-tests-a11y-manual-APP-HOME-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-ASSETS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-TAXONOMY-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PROJECT-EDIT-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-RELEASES-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-RELEASE-EDIT-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
"artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-report-json"
|
"artifact-artifacts-tests-a11y-manual-report-json"
|
||||||
],
|
],
|
||||||
@@ -1788,6 +2004,8 @@
|
|||||||
"check-registries-baseline-fixture",
|
"check-registries-baseline-fixture",
|
||||||
"check-registries-fixture",
|
"check-registries-fixture",
|
||||||
"check-routes-fixture",
|
"check-routes-fixture",
|
||||||
|
"check-tech-log-contract",
|
||||||
|
"check-dev-release-manifest",
|
||||||
"check-ci"
|
"check-ci"
|
||||||
],
|
],
|
||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
|
||||||
@@ -2049,6 +2267,18 @@
|
|||||||
"artifact-artifacts-performance-lab-json"
|
"artifact-artifacts-performance-lab-json"
|
||||||
],
|
],
|
||||||
"retentionClassId": "release-coherence"
|
"retentionClassId": "release-coherence"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FE-GATE-027",
|
||||||
|
"name": "release-admission",
|
||||||
|
"commandIds": [
|
||||||
|
"check-release-admission"
|
||||||
|
],
|
||||||
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-027-txt",
|
||||||
|
"evidenceArtifactIds": [
|
||||||
|
"artifact-artifacts-release-deployment-admission-json"
|
||||||
|
],
|
||||||
|
"retentionClassId": "release-coherence"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stages": [
|
"stages": [
|
||||||
@@ -2083,7 +2313,8 @@
|
|||||||
"FE-GATE-014",
|
"FE-GATE-014",
|
||||||
"FE-GATE-015",
|
"FE-GATE-015",
|
||||||
"FE-GATE-019",
|
"FE-GATE-019",
|
||||||
"FE-GATE-026"
|
"FE-GATE-026",
|
||||||
|
"FE-GATE-027"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2236,10 +2467,20 @@
|
|||||||
"condition": "release",
|
"condition": "release",
|
||||||
"timeoutMinutes": 45,
|
"timeoutMinutes": 45,
|
||||||
"gateIds": [
|
"gateIds": [
|
||||||
"FE-GATE-015"
|
"FE-GATE-015",
|
||||||
|
"FE-GATE-027"
|
||||||
],
|
],
|
||||||
"browserGateIds": [],
|
"browserGateIds": [],
|
||||||
"environment": [],
|
"environment": [
|
||||||
|
{
|
||||||
|
"name": "APP_PROFILE",
|
||||||
|
"value": "${{ vars.APP_PROFILE }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RELEASE_TARGET",
|
||||||
|
"value": "${{ vars.RELEASE_TARGET }}"
|
||||||
|
}
|
||||||
|
],
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"kind": "checkout"
|
"kind": "checkout"
|
||||||
@@ -2301,6 +2542,7 @@
|
|||||||
"scripts/lib/secret-scan.ts",
|
"scripts/lib/secret-scan.ts",
|
||||||
"scripts/lib/supply-chain.ts",
|
"scripts/lib/supply-chain.ts",
|
||||||
"scripts/lib/validated-json-artifact.ts",
|
"scripts/lib/validated-json-artifact.ts",
|
||||||
|
"scripts/lib/vite-route-chunks.ts",
|
||||||
"src/contracts/release-artifacts.ts",
|
"src/contracts/release-artifacts.ts",
|
||||||
"src/features/installed-contract-contributions.ts",
|
"src/features/installed-contract-contributions.ts",
|
||||||
"src/features/installed-feature-contracts.ts",
|
"src/features/installed-feature-contracts.ts",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8",
|
"snapshotDigest": "428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b",
|
||||||
"owner": "frontend-platform",
|
"owner": "tech-log-frontend",
|
||||||
"reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
|
"reason": "Install approved TechLog Public and Studio route contract",
|
||||||
"approvedAt": "2026-08-01T15:15:45.537Z"
|
"approvedAt": "2026-08-15T16:32:32.042Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
"registryId": "FE-REG-ROUTE",
|
"registryId": "FE-REG-ROUTE",
|
||||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 10,
|
"rowCount": 27,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"fieldTypes": {
|
"fieldTypes": {
|
||||||
"routeId": "string",
|
"routeId": "string",
|
||||||
"path": "string",
|
"path": "string",
|
||||||
|
"layoutGroup": "string",
|
||||||
"paramsSchema": "string|null",
|
"paramsSchema": "string|null",
|
||||||
"searchSchema": "string|null",
|
"searchSchema": "string|null",
|
||||||
"access": "string",
|
"access": "string",
|
||||||
@@ -43,14 +45,29 @@
|
|||||||
"public",
|
"public",
|
||||||
"session-required"
|
"session-required"
|
||||||
],
|
],
|
||||||
|
"layoutGroup": [
|
||||||
|
"PUBLIC",
|
||||||
|
"STUDIO"
|
||||||
|
],
|
||||||
"paramsSchema": [
|
"paramsSchema": [
|
||||||
null,
|
null,
|
||||||
"NotFoundSplat",
|
"NotFoundSplat",
|
||||||
"ReferenceResourceParams"
|
"ReferenceResourceParams",
|
||||||
|
"TechLogExploreKindParams",
|
||||||
|
"TechLogSlugParams",
|
||||||
|
"TechLogVersionParams",
|
||||||
|
"TechLogDocumentIdParams",
|
||||||
|
"TechLogPublicationEventIdParams",
|
||||||
|
"TechLogStudioSplat"
|
||||||
],
|
],
|
||||||
"searchSchema": [
|
"searchSchema": [
|
||||||
null,
|
null,
|
||||||
"ReferenceResourceListQuery"
|
"ReferenceResourceListQuery",
|
||||||
|
"TechLogHomeSearch",
|
||||||
|
"TechLogExploreSearch",
|
||||||
|
"TechLogExploreKindSearch",
|
||||||
|
"TechLogSearchQuery",
|
||||||
|
"TechLogCaseStateSearch"
|
||||||
],
|
],
|
||||||
"loadingSurface": [
|
"loadingSurface": [
|
||||||
"app-shell",
|
"app-shell",
|
||||||
@@ -88,6 +105,7 @@
|
|||||||
"breakingFields": [
|
"breakingFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -95,75 +113,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"rows": {
|
"rows": {
|
||||||
"APP_HOME": {
|
|
||||||
"access": "public",
|
|
||||||
"chunkId": "route-home",
|
|
||||||
"errorSurface": "route-boundary",
|
|
||||||
"loadingSurface": "app-shell",
|
|
||||||
"navigationLabel": "시작",
|
|
||||||
"navigationOrder": 10,
|
|
||||||
"paramsSchema": null,
|
|
||||||
"path": "/",
|
|
||||||
"routeId": "APP_HOME",
|
|
||||||
"searchSchema": null,
|
|
||||||
"title": "시작"
|
|
||||||
},
|
|
||||||
"EXAMPLES_AUTH": {
|
|
||||||
"access": "public",
|
|
||||||
"chunkId": "route-examples-auth",
|
|
||||||
"errorSurface": "route-boundary",
|
|
||||||
"loadingSurface": "example-page",
|
|
||||||
"navigationLabel": "인증 연동",
|
|
||||||
"navigationOrder": 40,
|
|
||||||
"paramsSchema": null,
|
|
||||||
"path": "/examples/auth",
|
|
||||||
"routeId": "EXAMPLES_AUTH",
|
|
||||||
"searchSchema": null,
|
|
||||||
"title": "인증 연동"
|
|
||||||
},
|
|
||||||
"EXAMPLES_PLATFORM": {
|
|
||||||
"access": "public",
|
|
||||||
"chunkId": "route-examples-platform",
|
|
||||||
"errorSurface": "route-boundary",
|
|
||||||
"loadingSurface": "example-page",
|
|
||||||
"navigationLabel": "플랫폼 구성",
|
|
||||||
"navigationOrder": 15,
|
|
||||||
"paramsSchema": null,
|
|
||||||
"path": "/examples/platform",
|
|
||||||
"routeId": "EXAMPLES_PLATFORM",
|
|
||||||
"searchSchema": null,
|
|
||||||
"title": "플랫폼 구성"
|
|
||||||
},
|
|
||||||
"EXAMPLES_STATES": {
|
|
||||||
"access": "public",
|
|
||||||
"chunkId": "route-examples-states",
|
|
||||||
"errorSurface": "route-boundary",
|
|
||||||
"loadingSurface": "example-page",
|
|
||||||
"navigationLabel": "화면 상태",
|
|
||||||
"navigationOrder": 30,
|
|
||||||
"paramsSchema": null,
|
|
||||||
"path": "/examples/states",
|
|
||||||
"routeId": "EXAMPLES_STATES",
|
|
||||||
"searchSchema": null,
|
|
||||||
"title": "화면 상태"
|
|
||||||
},
|
|
||||||
"EXAMPLES_UI": {
|
|
||||||
"access": "public",
|
|
||||||
"chunkId": "route-examples-ui",
|
|
||||||
"errorSurface": "route-boundary",
|
|
||||||
"loadingSurface": "example-page",
|
|
||||||
"navigationLabel": "UI 구성요소",
|
|
||||||
"navigationOrder": 20,
|
|
||||||
"paramsSchema": null,
|
|
||||||
"path": "/examples/ui",
|
|
||||||
"routeId": "EXAMPLES_UI",
|
|
||||||
"searchSchema": null,
|
|
||||||
"title": "UI 구성요소"
|
|
||||||
},
|
|
||||||
"NOT_FOUND": {
|
"NOT_FOUND": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"chunkId": "route-not-found",
|
"chunkId": "route-not-found",
|
||||||
"errorSurface": "not-found",
|
"errorSurface": "not-found",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
"loadingSurface": "none",
|
"loadingSurface": "none",
|
||||||
"navigationLabel": null,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
@@ -171,59 +125,371 @@
|
|||||||
"path": "*",
|
"path": "*",
|
||||||
"routeId": "NOT_FOUND",
|
"routeId": "NOT_FOUND",
|
||||||
"searchSchema": null,
|
"searchSchema": null,
|
||||||
"title": "페이지를 찾을 수 없음"
|
"title": "페이지를 찾을 수 없습니다."
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_DETAIL": {
|
"TECH_LOG_CASE": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resource-detail",
|
"chunkId": "route-tech-log-case",
|
||||||
"errorSurface": "feature-boundary",
|
"errorSurface": "feature-boundary",
|
||||||
"loadingSurface": "reference-resource-detail",
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
"navigationLabel": null,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
"paramsSchema": "ReferenceResourceParams",
|
"paramsSchema": "TechLogSlugParams",
|
||||||
"path": "/examples/reference-resources/:resourceId",
|
"path": "/cases/:slug",
|
||||||
"routeId": "REFERENCE_RESOURCE_DETAIL",
|
"routeId": "TECH_LOG_CASE",
|
||||||
"searchSchema": null,
|
"searchSchema": "TechLogCaseStateSearch",
|
||||||
"title": "Reference detail"
|
"title": "Case"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_FORM": {
|
"TECH_LOG_EXPLORE": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resource-form",
|
"chunkId": "route-tech-log-explore",
|
||||||
"errorSurface": "feature-boundary",
|
"errorSurface": "feature-boundary",
|
||||||
"loadingSurface": "reference-resource-form",
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": "탐색",
|
||||||
|
"navigationOrder": 10,
|
||||||
|
"paramsSchema": null,
|
||||||
|
"path": "/explore",
|
||||||
|
"routeId": "TECH_LOG_EXPLORE",
|
||||||
|
"searchSchema": "TechLogExploreSearch",
|
||||||
|
"title": "탐색"
|
||||||
|
},
|
||||||
|
"TECH_LOG_EXPLORE_KIND": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-explore-kind",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogExploreKindParams",
|
||||||
|
"path": "/explore/:kind",
|
||||||
|
"routeId": "TECH_LOG_EXPLORE_KIND",
|
||||||
|
"searchSchema": "TechLogExploreKindSearch",
|
||||||
|
"title": "유형별 탐색"
|
||||||
|
},
|
||||||
|
"TECH_LOG_HOME": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-home",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
"navigationLabel": null,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
"paramsSchema": null,
|
"paramsSchema": null,
|
||||||
"path": "/examples/reference-resources/new",
|
"path": "/",
|
||||||
"routeId": "REFERENCE_RESOURCE_FORM",
|
"routeId": "TECH_LOG_HOME",
|
||||||
"searchSchema": null,
|
"searchSchema": "TechLogHomeSearch",
|
||||||
"title": "Reference form"
|
"title": "TechLog"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_LIST": {
|
"TECH_LOG_PROFILE": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resources",
|
"chunkId": "route-tech-log-profile",
|
||||||
"errorSurface": "feature-boundary",
|
"errorSurface": "feature-boundary",
|
||||||
"loadingSurface": "reference-resource-list",
|
"layoutGroup": "PUBLIC",
|
||||||
"navigationLabel": "Reference feature",
|
"loadingSurface": "app-shell",
|
||||||
"navigationOrder": 50,
|
"navigationLabel": "프로필",
|
||||||
|
"navigationOrder": 40,
|
||||||
"paramsSchema": null,
|
"paramsSchema": null,
|
||||||
"path": "/examples/reference-resources",
|
"path": "/profile",
|
||||||
"routeId": "REFERENCE_RESOURCE_LIST",
|
"routeId": "TECH_LOG_PROFILE",
|
||||||
"searchSchema": "ReferenceResourceListQuery",
|
"searchSchema": null,
|
||||||
"title": "Reference feature"
|
"title": "프로필"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_STATUS": {
|
"TECH_LOG_PROJECT": {
|
||||||
"access": "session-required",
|
"access": "public",
|
||||||
"chunkId": "route-reference-resource-status",
|
"chunkId": "route-tech-log-project",
|
||||||
"errorSurface": "feature-boundary",
|
"errorSurface": "feature-boundary",
|
||||||
"loadingSurface": "reference-resource-status",
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogSlugParams",
|
||||||
|
"path": "/projects/:slug",
|
||||||
|
"routeId": "TECH_LOG_PROJECT",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "프로젝트"
|
||||||
|
},
|
||||||
|
"TECH_LOG_PROJECT_ACTIVITY": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-project-activity",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogSlugParams",
|
||||||
|
"path": "/projects/:slug/activity",
|
||||||
|
"routeId": "TECH_LOG_PROJECT_ACTIVITY",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "프로젝트 활동"
|
||||||
|
},
|
||||||
|
"TECH_LOG_PROJECT_DECISIONS": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-project-decisions",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogSlugParams",
|
||||||
|
"path": "/projects/:slug/decisions",
|
||||||
|
"routeId": "TECH_LOG_PROJECT_DECISIONS",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "프로젝트 결정"
|
||||||
|
},
|
||||||
|
"TECH_LOG_PROJECT_RECORDS": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-project-records",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogSlugParams",
|
||||||
|
"path": "/projects/:slug/records",
|
||||||
|
"routeId": "TECH_LOG_PROJECT_RECORDS",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "프로젝트 기록"
|
||||||
|
},
|
||||||
|
"TECH_LOG_PROJECTS": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-projects",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": "프로젝트",
|
||||||
|
"navigationOrder": 20,
|
||||||
|
"paramsSchema": null,
|
||||||
|
"path": "/projects",
|
||||||
|
"routeId": "TECH_LOG_PROJECTS",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "프로젝트"
|
||||||
|
},
|
||||||
|
"TECH_LOG_QUESTION": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-question",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogSlugParams",
|
||||||
|
"path": "/questions/:slug",
|
||||||
|
"routeId": "TECH_LOG_QUESTION",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "Open Question"
|
||||||
|
},
|
||||||
|
"TECH_LOG_REFERENCE": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-reference",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogSlugParams",
|
||||||
|
"path": "/references/:slug",
|
||||||
|
"routeId": "TECH_LOG_REFERENCE",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "Reference"
|
||||||
|
},
|
||||||
|
"TECH_LOG_RELEASE": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-release",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogVersionParams",
|
||||||
|
"path": "/releases/:version",
|
||||||
|
"routeId": "TECH_LOG_RELEASE",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "변경 기록"
|
||||||
|
},
|
||||||
|
"TECH_LOG_RELEASES": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-releases",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": "변경 기록",
|
||||||
|
"navigationOrder": 30,
|
||||||
|
"paramsSchema": null,
|
||||||
|
"path": "/releases",
|
||||||
|
"routeId": "TECH_LOG_RELEASES",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "변경 기록"
|
||||||
|
},
|
||||||
|
"TECH_LOG_SEARCH": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-search",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
"navigationLabel": null,
|
"navigationLabel": null,
|
||||||
"navigationOrder": null,
|
"navigationOrder": null,
|
||||||
"paramsSchema": null,
|
"paramsSchema": null,
|
||||||
"path": "/examples/reference-resources/status",
|
"path": "/search",
|
||||||
"routeId": "REFERENCE_RESOURCE_STATUS",
|
"routeId": "TECH_LOG_SEARCH",
|
||||||
|
"searchSchema": "TechLogSearchQuery",
|
||||||
|
"title": "검색"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_EDIT": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-document-edit",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogDocumentIdParams",
|
||||||
|
"path": "/studio/documents/:id/edit",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT",
|
||||||
"searchSchema": null,
|
"searchSchema": null,
|
||||||
"title": "Reference status"
|
"title": "문서 편집"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_NEW": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-document-new",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": "새 문서",
|
||||||
|
"navigationOrder": 30,
|
||||||
|
"paramsSchema": null,
|
||||||
|
"path": "/studio/documents/new",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "새 문서"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_PREVIEW": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-document-preview",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogDocumentIdParams",
|
||||||
|
"path": "/studio/documents/:id/preview",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "Public Preview"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_PUBLISH": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-document-publish",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogDocumentIdParams",
|
||||||
|
"path": "/studio/documents/:id/publish",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "게시"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_VALIDATION": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-document-validation",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogDocumentIdParams",
|
||||||
|
"path": "/studio/documents/:id/validation",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "문서 검증"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENTS": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-documents",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": "작업본",
|
||||||
|
"navigationOrder": 10,
|
||||||
|
"paramsSchema": null,
|
||||||
|
"path": "/studio/documents",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENTS",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "작업본"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_HOME": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-home",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": null,
|
||||||
|
"path": "/studio",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_HOME",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "TechLog Studio"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_NOT_FOUND": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-not-found",
|
||||||
|
"errorSurface": "not-found",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "none",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogStudioSplat",
|
||||||
|
"path": "/studio/*",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_NOT_FOUND",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "Studio 화면을 찾을 수 없습니다"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_PUBLICATION_PREVIEW": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-publication-preview",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogPublicationEventIdParams",
|
||||||
|
"path": "/studio/publications/:publicationEventId/preview",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "게시 Snapshot"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_PUBLICATIONS": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-studio-publications",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "STUDIO",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": "게시 기록",
|
||||||
|
"navigationOrder": 20,
|
||||||
|
"paramsSchema": null,
|
||||||
|
"path": "/studio/publications",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_PUBLICATIONS",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "게시 기록"
|
||||||
|
},
|
||||||
|
"TECH_LOG_TOPIC": {
|
||||||
|
"access": "public",
|
||||||
|
"chunkId": "route-tech-log-topic",
|
||||||
|
"errorSurface": "feature-boundary",
|
||||||
|
"layoutGroup": "PUBLIC",
|
||||||
|
"loadingSurface": "app-shell",
|
||||||
|
"navigationLabel": null,
|
||||||
|
"navigationOrder": null,
|
||||||
|
"paramsSchema": "TechLogSlugParams",
|
||||||
|
"path": "/topics/:slug",
|
||||||
|
"routeId": "TECH_LOG_TOPIC",
|
||||||
|
"searchSchema": null,
|
||||||
|
"title": "Topic"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -231,7 +497,7 @@
|
|||||||
"registryId": "FE-REG-ROUTE-RUNTIME",
|
"registryId": "FE-REG-ROUTE-RUNTIME",
|
||||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 10,
|
"rowCount": 27,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
@@ -276,64 +542,166 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"rows": {
|
"rows": {
|
||||||
"APP_HOME": {
|
|
||||||
"moduleId": "home-page",
|
|
||||||
"paramsCodec": "none",
|
|
||||||
"routeId": "APP_HOME",
|
|
||||||
"searchCodec": "none"
|
|
||||||
},
|
|
||||||
"EXAMPLES_AUTH": {
|
|
||||||
"moduleId": "auth-example-page",
|
|
||||||
"paramsCodec": "none",
|
|
||||||
"routeId": "EXAMPLES_AUTH",
|
|
||||||
"searchCodec": "none"
|
|
||||||
},
|
|
||||||
"EXAMPLES_PLATFORM": {
|
|
||||||
"moduleId": "platform-overview-page",
|
|
||||||
"paramsCodec": "none",
|
|
||||||
"routeId": "EXAMPLES_PLATFORM",
|
|
||||||
"searchCodec": "none"
|
|
||||||
},
|
|
||||||
"EXAMPLES_STATES": {
|
|
||||||
"moduleId": "state-gallery-page",
|
|
||||||
"paramsCodec": "none",
|
|
||||||
"routeId": "EXAMPLES_STATES",
|
|
||||||
"searchCodec": "none"
|
|
||||||
},
|
|
||||||
"EXAMPLES_UI": {
|
|
||||||
"moduleId": "ui-gallery-page",
|
|
||||||
"paramsCodec": "none",
|
|
||||||
"routeId": "EXAMPLES_UI",
|
|
||||||
"searchCodec": "none"
|
|
||||||
},
|
|
||||||
"NOT_FOUND": {
|
"NOT_FOUND": {
|
||||||
"moduleId": "not-found-page",
|
"moduleId": "route-not-found",
|
||||||
"paramsCodec": "NotFoundSplat",
|
"paramsCodec": "NotFoundSplat",
|
||||||
"routeId": "NOT_FOUND",
|
"routeId": "NOT_FOUND",
|
||||||
"searchCodec": "none"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_DETAIL": {
|
"TECH_LOG_CASE": {
|
||||||
"moduleId": "reference-resource-detail-page",
|
"moduleId": "route-tech-log-case",
|
||||||
"paramsCodec": "ReferenceResourceParams",
|
"paramsCodec": "TechLogSlugParams",
|
||||||
"routeId": "REFERENCE_RESOURCE_DETAIL",
|
"routeId": "TECH_LOG_CASE",
|
||||||
|
"searchCodec": "TechLogCaseStateSearch"
|
||||||
|
},
|
||||||
|
"TECH_LOG_EXPLORE": {
|
||||||
|
"moduleId": "route-tech-log-explore",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_EXPLORE",
|
||||||
|
"searchCodec": "TechLogExploreSearch"
|
||||||
|
},
|
||||||
|
"TECH_LOG_EXPLORE_KIND": {
|
||||||
|
"moduleId": "route-tech-log-explore-kind",
|
||||||
|
"paramsCodec": "TechLogExploreKindParams",
|
||||||
|
"routeId": "TECH_LOG_EXPLORE_KIND",
|
||||||
|
"searchCodec": "TechLogExploreKindSearch"
|
||||||
|
},
|
||||||
|
"TECH_LOG_HOME": {
|
||||||
|
"moduleId": "route-tech-log-home",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_HOME",
|
||||||
|
"searchCodec": "TechLogHomeSearch"
|
||||||
|
},
|
||||||
|
"TECH_LOG_PROFILE": {
|
||||||
|
"moduleId": "route-tech-log-profile",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_PROFILE",
|
||||||
"searchCodec": "none"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_FORM": {
|
"TECH_LOG_PROJECT": {
|
||||||
"moduleId": "reference-resource-form-page",
|
"moduleId": "route-tech-log-project",
|
||||||
"paramsCodec": "none",
|
"paramsCodec": "TechLogSlugParams",
|
||||||
"routeId": "REFERENCE_RESOURCE_FORM",
|
"routeId": "TECH_LOG_PROJECT",
|
||||||
"searchCodec": "none"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_LIST": {
|
"TECH_LOG_PROJECT_ACTIVITY": {
|
||||||
"moduleId": "reference-resource-page",
|
"moduleId": "route-tech-log-project-activity",
|
||||||
"paramsCodec": "none",
|
"paramsCodec": "TechLogSlugParams",
|
||||||
"routeId": "REFERENCE_RESOURCE_LIST",
|
"routeId": "TECH_LOG_PROJECT_ACTIVITY",
|
||||||
"searchCodec": "ReferenceResourceListQuery"
|
"searchCodec": "none"
|
||||||
},
|
},
|
||||||
"REFERENCE_RESOURCE_STATUS": {
|
"TECH_LOG_PROJECT_DECISIONS": {
|
||||||
"moduleId": "reference-resource-status-page",
|
"moduleId": "route-tech-log-project-decisions",
|
||||||
|
"paramsCodec": "TechLogSlugParams",
|
||||||
|
"routeId": "TECH_LOG_PROJECT_DECISIONS",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_PROJECT_RECORDS": {
|
||||||
|
"moduleId": "route-tech-log-project-records",
|
||||||
|
"paramsCodec": "TechLogSlugParams",
|
||||||
|
"routeId": "TECH_LOG_PROJECT_RECORDS",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_PROJECTS": {
|
||||||
|
"moduleId": "route-tech-log-projects",
|
||||||
"paramsCodec": "none",
|
"paramsCodec": "none",
|
||||||
"routeId": "REFERENCE_RESOURCE_STATUS",
|
"routeId": "TECH_LOG_PROJECTS",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_QUESTION": {
|
||||||
|
"moduleId": "route-tech-log-question",
|
||||||
|
"paramsCodec": "TechLogSlugParams",
|
||||||
|
"routeId": "TECH_LOG_QUESTION",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_REFERENCE": {
|
||||||
|
"moduleId": "route-tech-log-reference",
|
||||||
|
"paramsCodec": "TechLogSlugParams",
|
||||||
|
"routeId": "TECH_LOG_REFERENCE",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_RELEASE": {
|
||||||
|
"moduleId": "route-tech-log-release",
|
||||||
|
"paramsCodec": "TechLogVersionParams",
|
||||||
|
"routeId": "TECH_LOG_RELEASE",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_RELEASES": {
|
||||||
|
"moduleId": "route-tech-log-releases",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_RELEASES",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_SEARCH": {
|
||||||
|
"moduleId": "route-tech-log-search",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_SEARCH",
|
||||||
|
"searchCodec": "TechLogSearchQuery"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_EDIT": {
|
||||||
|
"moduleId": "route-tech-log-studio-document-edit",
|
||||||
|
"paramsCodec": "TechLogDocumentIdParams",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_NEW": {
|
||||||
|
"moduleId": "route-tech-log-studio-document-new",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_PREVIEW": {
|
||||||
|
"moduleId": "route-tech-log-studio-document-preview",
|
||||||
|
"paramsCodec": "TechLogDocumentIdParams",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_PUBLISH": {
|
||||||
|
"moduleId": "route-tech-log-studio-document-publish",
|
||||||
|
"paramsCodec": "TechLogDocumentIdParams",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENT_VALIDATION": {
|
||||||
|
"moduleId": "route-tech-log-studio-document-validation",
|
||||||
|
"paramsCodec": "TechLogDocumentIdParams",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_DOCUMENTS": {
|
||||||
|
"moduleId": "route-tech-log-studio-documents",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_DOCUMENTS",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_HOME": {
|
||||||
|
"moduleId": "route-tech-log-studio-home",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_HOME",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_NOT_FOUND": {
|
||||||
|
"moduleId": "route-tech-log-studio-not-found",
|
||||||
|
"paramsCodec": "TechLogStudioSplat",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_NOT_FOUND",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_PUBLICATION_PREVIEW": {
|
||||||
|
"moduleId": "route-tech-log-studio-publication-preview",
|
||||||
|
"paramsCodec": "TechLogPublicationEventIdParams",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_STUDIO_PUBLICATIONS": {
|
||||||
|
"moduleId": "route-tech-log-studio-publications",
|
||||||
|
"paramsCodec": "none",
|
||||||
|
"routeId": "TECH_LOG_STUDIO_PUBLICATIONS",
|
||||||
|
"searchCodec": "none"
|
||||||
|
},
|
||||||
|
"TECH_LOG_TOPIC": {
|
||||||
|
"moduleId": "route-tech-log-topic",
|
||||||
|
"paramsCodec": "TechLogSlugParams",
|
||||||
|
"routeId": "TECH_LOG_TOPIC",
|
||||||
"searchCodec": "none"
|
"searchCodec": "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,7 +898,7 @@
|
|||||||
"registryId": "FE-REG-SCHEMA",
|
"registryId": "FE-REG-SCHEMA",
|
||||||
"owner": "feature-frontend-contract-schema-registry",
|
"owner": "feature-frontend-contract-schema-registry",
|
||||||
"source": "src/features/installed-feature-contracts.ts",
|
"source": "src/features/installed-feature-contracts.ts",
|
||||||
"rowCount": 8,
|
"rowCount": 19,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"schemaId",
|
"schemaId",
|
||||||
@@ -633,6 +1001,105 @@
|
|||||||
"schemaId": "ReferenceResourcePayload",
|
"schemaId": "ReferenceResourcePayload",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogCaseStateSearch": {
|
||||||
|
"boundary": "route-search",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogCaseStateSearch",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogDocumentIdParams": {
|
||||||
|
"boundary": "route-params",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogDocumentIdParams",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogExploreKindParams": {
|
||||||
|
"boundary": "route-params",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogExploreKindParams",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogExploreKindSearch": {
|
||||||
|
"boundary": "route-search",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogExploreKindSearch",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogExploreSearch": {
|
||||||
|
"boundary": "route-search",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogExploreSearch",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogHomeSearch": {
|
||||||
|
"boundary": "route-search",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogHomeSearch",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogPublicationEventIdParams": {
|
||||||
|
"boundary": "route-params",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogPublicationEventIdParams",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogSearchQuery": {
|
||||||
|
"boundary": "route-search",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogSearchQuery",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogSlugParams": {
|
||||||
|
"boundary": "route-params",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogSlugParams",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogStudioSplat": {
|
||||||
|
"boundary": "route-params",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogStudioSplat",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
|
},
|
||||||
|
"TechLogVersionParams": {
|
||||||
|
"boundary": "route-params",
|
||||||
|
"direction": "REQUEST",
|
||||||
|
"owner": "feature-tech-log",
|
||||||
|
"runtime": "zod",
|
||||||
|
"schemaId": "TechLogVersionParams",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -766,7 +1233,7 @@
|
|||||||
"registryId": "FE-REG-STORAGE",
|
"registryId": "FE-REG-STORAGE",
|
||||||
"owner": "feature-frontend-storage-registry-contract",
|
"owner": "feature-frontend-storage-registry-contract",
|
||||||
"source": "src/contracts/storage-keys.ts",
|
"source": "src/contracts/storage-keys.ts",
|
||||||
"rowCount": 4,
|
"rowCount": 5,
|
||||||
"contract": {
|
"contract": {
|
||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"logicalName",
|
"logicalName",
|
||||||
@@ -847,6 +1314,19 @@
|
|||||||
"ttl": null,
|
"ttl": null,
|
||||||
"valueCodec": "none"
|
"valueCodec": "none"
|
||||||
},
|
},
|
||||||
|
"CACHE_INVALIDATION_PULSE": {
|
||||||
|
"backend": "localStorage",
|
||||||
|
"classification": "opaque-cache",
|
||||||
|
"logicalName": "CACHE_INVALIDATION_PULSE",
|
||||||
|
"migration": "discard",
|
||||||
|
"name": "pulse",
|
||||||
|
"physicalKey": "ca-frontend:cache-invalidation:v1:pulse",
|
||||||
|
"quotaFallback": "no-persist",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"scope": "cache-invalidation",
|
||||||
|
"ttl": null,
|
||||||
|
"valueCodec": "opaque-string-v1"
|
||||||
|
},
|
||||||
"CHUNK_RELOAD_GUARD": {
|
"CHUNK_RELOAD_GUARD": {
|
||||||
"backend": "sessionStorage",
|
"backend": "sessionStorage",
|
||||||
"classification": "opaque-cache",
|
"classification": "opaque-cache",
|
||||||
|
|||||||
@@ -1,6 +1,190 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"changes": [
|
"changes": [
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:$contract:allowedValues:contract-field-changed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:$contract:breakingFields:contract-field-changed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:$contract:fieldTypes:contract-field-changed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:$contract:requiredFields:contract-field-changed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:APP_HOME:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:EXAMPLES_AUTH:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:EXAMPLES_PLATFORM:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:EXAMPLES_STATES:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:EXAMPLES_UI:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_DETAIL:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_FORM:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_LIST:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_STATUS:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:APP_HOME:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_AUTH:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_PLATFORM:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_STATES:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_UI:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:NOT_FOUND:moduleId:field-changed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_DETAIL:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_FORM:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_LIST:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_STATUS:*:removed",
|
||||||
|
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||||
|
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||||
|
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||||
|
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||||
|
"owner": "tech-log-frontend"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"changeId": "FE-REG-ENV:API_CONTRACT_VERSION:*:removed",
|
"changeId": "FE-REG-ENV:API_CONTRACT_VERSION:*:removed",
|
||||||
"versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.",
|
"versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.",
|
||||||
@@ -96,6 +280,86 @@
|
|||||||
"compatibilityWindow": "Existing valid envelopes continue to decode; invalid values fail closed.",
|
"compatibilityWindow": "Existing valid envelopes continue to decode; invalid values fail closed.",
|
||||||
"rollback": "Remove the required codec field and runtime codec dispatch together.",
|
"rollback": "Remove the required codec field and runtime codec dispatch together.",
|
||||||
"owner": "frontend-platform"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"requiredFields": [
|
"requiredFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"fieldTypes": {
|
"fieldTypes": {
|
||||||
"routeId": "string",
|
"routeId": "string",
|
||||||
"path": "string",
|
"path": "string",
|
||||||
|
"layoutGroup": "string",
|
||||||
"paramsSchema": "string|null",
|
"paramsSchema": "string|null",
|
||||||
"searchSchema": "string|null",
|
"searchSchema": "string|null",
|
||||||
"access": "string",
|
"access": "string",
|
||||||
@@ -41,8 +43,27 @@
|
|||||||
"uniqueFields": ["routeId", "path", "chunkId"],
|
"uniqueFields": ["routeId", "path", "chunkId"],
|
||||||
"allowedValues": {
|
"allowedValues": {
|
||||||
"access": ["public", "session-required"],
|
"access": ["public", "session-required"],
|
||||||
"paramsSchema": [null, "NotFoundSplat", "ReferenceResourceParams"],
|
"layoutGroup": ["PUBLIC", "STUDIO"],
|
||||||
"searchSchema": [null, "ReferenceResourceListQuery"],
|
"paramsSchema": [
|
||||||
|
null,
|
||||||
|
"NotFoundSplat",
|
||||||
|
"ReferenceResourceParams",
|
||||||
|
"TechLogExploreKindParams",
|
||||||
|
"TechLogSlugParams",
|
||||||
|
"TechLogVersionParams",
|
||||||
|
"TechLogDocumentIdParams",
|
||||||
|
"TechLogPublicationEventIdParams",
|
||||||
|
"TechLogStudioSplat"
|
||||||
|
],
|
||||||
|
"searchSchema": [
|
||||||
|
null,
|
||||||
|
"ReferenceResourceListQuery",
|
||||||
|
"TechLogHomeSearch",
|
||||||
|
"TechLogExploreSearch",
|
||||||
|
"TechLogExploreKindSearch",
|
||||||
|
"TechLogSearchQuery",
|
||||||
|
"TechLogCaseStateSearch"
|
||||||
|
],
|
||||||
"loadingSurface": [
|
"loadingSurface": [
|
||||||
"app-shell",
|
"app-shell",
|
||||||
"example-page",
|
"example-page",
|
||||||
@@ -84,6 +105,7 @@
|
|||||||
"breakingFields": [
|
"breakingFields": [
|
||||||
"routeId",
|
"routeId",
|
||||||
"path",
|
"path",
|
||||||
|
"layoutGroup",
|
||||||
"paramsSchema",
|
"paramsSchema",
|
||||||
"searchSchema",
|
"searchSchema",
|
||||||
"access",
|
"access",
|
||||||
@@ -234,7 +256,7 @@
|
|||||||
"consumerIdentityField": "schemaId",
|
"consumerIdentityField": "schemaId",
|
||||||
"consumerDirectories": [
|
"consumerDirectories": [
|
||||||
"src/presentation/routes",
|
"src/presentation/routes",
|
||||||
"src/features/reference-feature/presentation",
|
"src/features/tech-log/presentation",
|
||||||
"src/features/reference-feature/contracts"
|
"src/features/reference-feature/contracts"
|
||||||
],
|
],
|
||||||
"breakingFields": ["schemaId", "boundary", "runtime"]
|
"breakingFields": ["schemaId", "boundary", "runtime"]
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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,4 +1,47 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"changes": []
|
"changes": [
|
||||||
|
{
|
||||||
|
"changeId": "add:@fontsource/ibm-plex-mono@5.3.0",
|
||||||
|
"owner": "tech-log-frontend",
|
||||||
|
"reviewer": "frontend-platform-security",
|
||||||
|
"reason": "Preserve the source application's IBM Plex Mono typography and bundled font assets without a runtime font request.",
|
||||||
|
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "add:pretendard@1.3.9",
|
||||||
|
"owner": "tech-log-frontend",
|
||||||
|
"reviewer": "frontend-platform-security",
|
||||||
|
"reason": "Preserve the source application's Pretendard Variable typography using its pinned bundled font asset.",
|
||||||
|
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "add:remark-directive@4.0.0",
|
||||||
|
"owner": "tech-log-frontend",
|
||||||
|
"reviewer": "frontend-platform-security",
|
||||||
|
"reason": "Parse the source TechLog document directive syntax through the migrated deterministic content pipeline.",
|
||||||
|
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "add:remark-gfm@4.0.1",
|
||||||
|
"owner": "tech-log-frontend",
|
||||||
|
"reviewer": "frontend-platform-security",
|
||||||
|
"reason": "Preserve source TechLog GitHub-flavored Markdown tables, task lists, and autolink parsing.",
|
||||||
|
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "add:remark-parse@11.0.0",
|
||||||
|
"owner": "tech-log-frontend",
|
||||||
|
"reviewer": "frontend-platform-security",
|
||||||
|
"reason": "Parse the source TechLog Markdown records into the migrated typed public-render model.",
|
||||||
|
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changeId": "add:unified@11.0.5",
|
||||||
|
"owner": "tech-log-frontend",
|
||||||
|
"reviewer": "frontend-platform-security",
|
||||||
|
"reason": "Compose the source-equivalent Markdown and directive parsing stages without framework coupling.",
|
||||||
|
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"ISC",
|
"ISC",
|
||||||
"MIT",
|
"MIT",
|
||||||
"MIT-0",
|
"MIT-0",
|
||||||
"MPL-2.0"
|
"MPL-2.0",
|
||||||
|
"OFL-1.1"
|
||||||
],
|
],
|
||||||
"deniedLicensePatterns": [
|
"deniedLicensePatterns": [
|
||||||
"(^|\\s)AGPL",
|
"(^|\\s)AGPL",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# 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,12 +1,27 @@
|
|||||||
# Manual accessibility review checklist
|
# Manual accessibility review checklist
|
||||||
|
|
||||||
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
||||||
review all six route records in `artifacts/tests/a11y-manual/` against one
|
review all 27 route records in `artifacts/tests/a11y-manual/` against one
|
||||||
release candidate and sign them. The required scope is derived from the route
|
release candidate and sign them. The required TechLog Public and Studio scope is
|
||||||
registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`,
|
derived from the installed route registry, so the gate rejects stale, missing,
|
||||||
`REFERENCE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the
|
or additional route records as well as blank identity/timestamp/signature
|
||||||
gate rejects blank identity/timestamp/signature fields, pending verdicts,
|
fields, pending verdicts, and mismatched release IDs. The scope is:
|
||||||
mismatched release IDs, or missing routes.
|
`NOT_FOUND`, `TECH_LOG_CASE`, `TECH_LOG_EXPLORE`, `TECH_LOG_EXPLORE_KIND`,
|
||||||
|
`TECH_LOG_HOME`, `TECH_LOG_PROFILE`, `TECH_LOG_PROJECT`,
|
||||||
|
`TECH_LOG_PROJECTS`, `TECH_LOG_PROJECT_ACTIVITY`,
|
||||||
|
`TECH_LOG_PROJECT_DECISIONS`, `TECH_LOG_PROJECT_RECORDS`,
|
||||||
|
`TECH_LOG_QUESTION`, `TECH_LOG_REFERENCE`, `TECH_LOG_RELEASE`,
|
||||||
|
`TECH_LOG_RELEASES`, `TECH_LOG_SEARCH`, `TECH_LOG_STUDIO_DOCUMENTS`,
|
||||||
|
`TECH_LOG_STUDIO_DOCUMENT_EDIT`, `TECH_LOG_STUDIO_DOCUMENT_NEW`,
|
||||||
|
`TECH_LOG_STUDIO_DOCUMENT_PREVIEW`, `TECH_LOG_STUDIO_DOCUMENT_PUBLISH`,
|
||||||
|
`TECH_LOG_STUDIO_DOCUMENT_VALIDATION`, `TECH_LOG_STUDIO_HOME`,
|
||||||
|
`TECH_LOG_STUDIO_NOT_FOUND`, `TECH_LOG_STUDIO_PUBLICATIONS`,
|
||||||
|
`TECH_LOG_STUDIO_PUBLICATION_PREVIEW`, `TECH_LOG_TOPIC`.
|
||||||
|
|
||||||
|
This list is not maintained by hand: `verify:documentation` compares it against
|
||||||
|
the installed route registry and fails when a registered route is absent. The
|
||||||
|
template carried the same rule for its own example screens; the route set is
|
||||||
|
this product's, the rule is the template's.
|
||||||
|
|
||||||
Allowed item verdicts:
|
Allowed item verdicts:
|
||||||
|
|
||||||
@@ -17,7 +32,7 @@ Required record:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
Status: reviewed
|
Status: reviewed
|
||||||
Route ID: APP_HOME
|
Route ID: <exact installed route ID>
|
||||||
Release ID: <immutable release ID>
|
Release ID: <immutable release ID>
|
||||||
Reviewer: <human reviewer identity>
|
Reviewer: <human reviewer identity>
|
||||||
Reviewed at: <RFC 3339 timestamp>
|
Reviewed at: <RFC 3339 timestamp>
|
||||||
@@ -45,7 +60,8 @@ The reviewer must verify:
|
|||||||
- M7: non-essential motion is suppressed with reduced-motion preference
|
- M7: non-essential motion is suppressed with reduced-motion preference
|
||||||
- Screen reader: headings, live regions, errors, and actions are announced once
|
- Screen reader: headings, live regions, errors, and actions are announced once
|
||||||
|
|
||||||
`EXAMPLES_UI` requires real M4 modal and M5 field-error review; those items must
|
Routes with dialogs or form errors require real M4 modal-focus or M5
|
||||||
not be marked not-applicable on that route. Passing automated evidence means
|
error-association review; those items must not be marked not-applicable when the
|
||||||
|
reviewed route exposes the relevant behavior. Passing automated evidence means
|
||||||
only that tested pages had no critical or serious axe findings under the
|
only that tested pages had no critical or serious axe findings under the
|
||||||
recorded Chromium, Firefox, and WebKit runs.
|
recorded browser runs.
|
||||||
|
|||||||
@@ -17,11 +17,33 @@ The following edges are forbidden:
|
|||||||
|
|
||||||
- domain to application, presentation, adapters, bootstrap, React, or browser globals
|
- domain to application, presentation, adapters, bootstrap, React, or browser globals
|
||||||
- application to presentation, concrete adapters, bootstrap, React, or browser globals
|
- application to presentation, concrete adapters, bootstrap, React, or browser globals
|
||||||
|
- `contracts` to application or features: contracts is the lower package and
|
||||||
|
owns the shared vocabulary both of them read
|
||||||
- presentation to concrete adapters, raw DTO schemas, or storage implementations
|
- presentation to concrete adapters, raw DTO schemas, or storage implementations
|
||||||
|
- generic presentation to the installed-feature registries: which features exist
|
||||||
|
is a product decision owned by `bootstrap`
|
||||||
- an adapter to presentation, bootstrap internals, or another concrete adapter
|
- an adapter to presentation, bootstrap internals, or another concrete adapter
|
||||||
- feature domain/application to its presentation or outbound adapter, and
|
- feature domain/application to its presentation or outbound adapter, and
|
||||||
feature presentation to its outbound adapter
|
feature presentation to its outbound adapter
|
||||||
|
|
||||||
|
## The adapter kernel
|
||||||
|
|
||||||
|
"Another concrete adapter" excludes the adapter kernel, which is shared on
|
||||||
|
purpose and is the only adapter code an adapter may reach across a group for:
|
||||||
|
|
||||||
|
- `src/adapters/platform/**` — the system clock, the shared abort primitive and
|
||||||
|
the bounded-capacity guard
|
||||||
|
- `src/adapters/browser-file-storage/result.ts` — the browser-data result and
|
||||||
|
failure constructors
|
||||||
|
|
||||||
|
Each rule above is enforced by `check:architecture`, including the kernel
|
||||||
|
carve-out, so this table and the executable rules cannot drift apart. Two edges
|
||||||
|
are still open and are named explicitly in `.dependency-cruiser.json` rather
|
||||||
|
than left silent: the generic presentation modules that read the installed
|
||||||
|
registries today, and the two collaborator types `query-cache` reads from
|
||||||
|
`cross-context-invalidation`. Both lists are frozen — a new edge of either kind
|
||||||
|
fails the gate.
|
||||||
|
|
||||||
`bootstrap` contains composition only. Business rules and page-specific
|
`bootstrap` contains composition only. Business rules and page-specific
|
||||||
orchestration belong to domain/application.
|
orchestration belong to domain/application.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"standard": "rules/diagram-standards.md v2",
|
"standard": "rules/diagram-standards.md v2",
|
||||||
"evidenceReport": {
|
"evidenceReport": {
|
||||||
"repoPath": "docs/architecture/review-evidence.md",
|
"repoPath": "docs/architecture/review-evidence.md",
|
||||||
"canonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
|
"upstreamCanonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
|
||||||
"canonicalSha256": "b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29"
|
"canonicalSha256": "b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29"
|
||||||
},
|
},
|
||||||
"reviews": {
|
"reviews": {
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"thresholdSatisfied": true,
|
"thresholdSatisfied": true,
|
||||||
"scope": "immutable static assets and mutable /config.json delivery"
|
"scope": "immutable static assets and mutable /config.json delivery"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"note": "`repoPath` is this repository's copy and must resolve. `upstreamCanonicalPath` and every `reviews[*].sourcePath` name the reviewing workspace, not this tree; they are provenance labels and are deliberately not resolvable here. `canonicalSha256` is what binds the two, and the gate checks it appears in `repoPath`."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -462,6 +462,103 @@ The full `tests/unit` + `tests/integration` run is **1,845 passed / 1,864**,
|
|||||||
cgroup, RLIMIT and `/tmp` permission behaviour already recorded above — the same
|
cgroup, RLIMIT and `/tmp` permission behaviour already recorded above — the same
|
||||||
file failed identically before this work. No adapter test fails.
|
file failed identically before this work. No adapter test fails.
|
||||||
|
|
||||||
|
## Operational contract review (2026-08-15)
|
||||||
|
|
||||||
|
A fourth review looked past the adapter layer at the operational contract:
|
||||||
|
feature on/off, environment separation, folder boundaries, and which gates were
|
||||||
|
actually green. It found five red gates and three structural gaps. Every row
|
||||||
|
below names the defect, not the symptom.
|
||||||
|
|
||||||
|
| id | area | disposition | what was actually wrong |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `OPS-01` | release | `FIXED` | `public/` is copied verbatim into `dist/`, so every build — production included — shipped the local runtime document. Runtime config now comes from `config/runtime/<profile>.json`. |
|
||||||
|
| `OPS-02` | release | `FIXED` | Release coherence proved the artifacts agreed with each other, never that they belonged in production. `FE-GATE-027` refuses an artifact whose `APP_ENV`, auth mode, endpoints or build identity do not match a declared `RELEASE_TARGET`, and refuses an undeclared target outright. |
|
||||||
|
| `OPS-03` | runtime | `FIXED` | `REQUEST_TIMEOUT_MS` was validated and then never passed to the V3 executor; every operation ran on its contract's own deadline. It is now a ceiling that may tighten a contract, never loosen one. |
|
||||||
|
| `OPS-04` | build | `FIXED` | `VITE_ROUTER_BASE_PATH` drove the router and the Service Worker scope but not Vite's asset `base`, so a sub-path deployment emitted root-absolute assets. One value now feeds all three. |
|
||||||
|
| `OPS-05` | provider | `FIXED` | bubblewrap 0.9.0 drops whatever follows the option stream inside an `--args` file, so the sandboxed command was never executed: bwrap printed usage and exited 1. Options stay hidden; the command travels on real argv. |
|
||||||
|
| `OPS-06` | provider | `FIXED` | The scope wrapper read its liveness pipe through `fs`, a blocking `read(2)` on a pipe the supervisor never closes. `process.exit` deadlocked joining that thread, so a completed provider was reported as a timeout kill. |
|
||||||
|
| `OPS-07` | release | `FIXED` | `mkdir`/`open` modes were left to the ambient umask, so a hardened runner produced directories it could not enter and handed `tar` a file it could not re-open. |
|
||||||
|
| `OPS-08` | release | `FIXED` | Promotion cleanup deleted this promotion's exact five through a pinned descriptor and only then noticed the leaf had been substituted, leaving a half-emptied directory a retry could not distinguish from a completed one. |
|
||||||
|
| `OPS-09` | removability | `FIXED` | The removal fixture was not a repository, had no `.gitignore`, and each removal script kept its own copy-target list that had drifted. Supply-chain generation therefore failed inside every fixture and took the whole provider suite down with it. |
|
||||||
|
| `OPS-10` | removability | `FIXED` | A platform integration file asserted the reference feature's route ids, so removing the feature left it importing a deleted module. The assertion moved to the feature's own test tree. |
|
||||||
|
| `OPS-11` | removability | `FIXED` | A removal fixture runs against a deliberately reduced CI contract; the canonical exact-count tests re-imposed the full authority on it and failed the fixture for the reduction it exists to prove. |
|
||||||
|
| `OPS-12` | browser | `FIXED` | Four browser-capability specs answered capability requests without the `protocol` field the hardened envelope requires, so every capability was refused and the download and part-upload paths asserted against an empty transcript. |
|
||||||
|
| `OPS-13` | browser | `FIXED` | A refused capability document answered `recovery: NONE`, contradicting both the design record and the vault, which already answers `REISSUE_CAPABILITY`. |
|
||||||
|
| `OPS-14` | performance | `FIXED` | Playwright matches accessible names by substring, so the navigation entry matched the home page's call to action too; the run died on a strict-mode violation before the first measurement and produced no evidence at all. |
|
||||||
|
| `OPS-15` | visual | `FIXED` | The platform overview baseline predated the reference routes moving from `integration-defined` to `session-required`, so the only visual gate covering that page failed for its own staleness. |
|
||||||
|
| `OPS-16` | architecture | `FIXED` | `src/contracts` imported `src/application` for the shared `Result` and the compatibility predicate; neither package owned the shared vocabulary. Both moved down to contracts. |
|
||||||
|
| `OPS-17` | architecture | `FIXED` | The documented "no adapter depends on another concrete adapter" rule had no executable form, and `diagnostics` imported a guard out of `telemetry`. The guard moved to the adapter kernel and the rule is now enforced with a same-directory backreference. |
|
||||||
|
| `OPS-18` | architecture | `PARTIAL` | Generic presentation still reads the installed-feature registries. The rule freezes the exact set of modules doing so today; a new edge fails. Lifting the assembly into `bootstrap` is not done. |
|
||||||
|
| `OPS-19` | documentation | `FIXED` | README and the manual accessibility checklist both claimed six routes while ten were registered, leaving four screens outside the declared manual review scope. The list is now derived from the route registry by `verify:documentation`. |
|
||||||
|
|
||||||
|
### Product feature selection (2026-08-15, second pass)
|
||||||
|
|
||||||
|
| id | disposition | what changed |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `OPS-20` | `FIXED` | Which features a build contains is now a declared manifest rather than five registries spreading a literal. `VITE_PRODUCT_FEATURES` narrows it at build time; a test fails if a new registry forgets to consult it. |
|
||||||
|
| `OPS-21` | `FIXED` | `FEATURE_OVERRIDES` in the runtime document takes an installed feature out of service without a rebuild. The router refuses its routes, not just the navigation, so a typed deep link cannot still mount it. |
|
||||||
|
| `OPS-22` | `FIXED` | Both inputs are subtractive by vocabulary: the override enum has no `ENABLED`, and a build-time selection naming a feature the source tree does not declare is refused rather than ignored. |
|
||||||
|
| `OPS-23` | `FIXED` | A sandbox that fails to launch now reports why. The supervisor consumed the child's output only to enforce a byte cap and discarded it, so a host restriction surfaced as an unexplained `exit=1`. Lines the sandbox tooling itself emits are kept; provider output is still discarded. |
|
||||||
|
|
||||||
|
An env var does **not** shrink the bundle, and the code says so. A static import
|
||||||
|
cannot be undone by a value, and making the import graph depend on a
|
||||||
|
configuration string is what §3.5 exists to prevent. Measured: `none` changes
|
||||||
|
the output by 58 bytes. Physical removal is FE-GATE-020's job.
|
||||||
|
|
||||||
|
### Host restriction discovered during this pass
|
||||||
|
|
||||||
|
`bwrap --unshare-net` no longer works on this machine:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ printf '%s\0' --unshare-net --ro-bind /usr /usr ... | bwrap --args 3 -- /bin/true
|
||||||
|
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
|
||||||
|
$ sysctl kernel.apparmor_restrict_unprivileged_userns
|
||||||
|
kernel.apparmor_restrict_unprivileged_userns = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
That reproduction contains none of this repository's code. Earlier in the same
|
||||||
|
session the identical sandbox ran to completion, so the restriction became
|
||||||
|
active partway through. While it holds, 16 of the 108 provider tests cannot run
|
||||||
|
here — they need a sandbox the kernel will not grant. They are not counted as
|
||||||
|
green and not counted as product defects; under a host that permits the
|
||||||
|
namespace the same file was 107/108.
|
||||||
|
|
||||||
|
### Still red after this pass
|
||||||
|
|
||||||
|
*applies effective aggregate cgroup limits without exposing command or
|
||||||
|
credentials* was rewritten. It used to read the live process tree with one
|
||||||
|
`ps` per pid and assert mid-run, which lost a race against a sandbox that now
|
||||||
|
completes in a few hundred milliseconds; it records the tree from `/proc` every
|
||||||
|
5ms and asserts on the recording after the run. That restructuring is also what
|
||||||
|
revealed the host restriction above — the supervisor had been failing to launch
|
||||||
|
the sandbox and the test was dying on the observation first.
|
||||||
|
|
||||||
|
Tests that spawn processes, build archives and sign evidence were given a
|
||||||
|
30s budget instead of the 10s default sized for pure-JS unit tests. The default
|
||||||
|
was not raised: that would hide a genuinely hung test.
|
||||||
|
|
||||||
|
### FE-GATE-020 after this pass
|
||||||
|
|
||||||
|
| fixture | before | after |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| reference feature | failed before its first assertion | 1,612 pass / 1 fail |
|
||||||
|
| optional recipe | 39 failures | 1,386 pass / 2 fail |
|
||||||
|
| browser file + storage | 40 failures | 1,006 pass / 3 fail |
|
||||||
|
| realtime | not reached | 1,159 pass / 1 fail |
|
||||||
|
|
||||||
|
Every remaining failure is one of the three environment-limited tests above.
|
||||||
|
|
||||||
|
Lab performance now produces evidence, and that evidence shows the
|
||||||
|
named-interaction budget missed on this machine (367–724ms against 200ms). The
|
||||||
|
metric measures a full lazy-route navigation while the budget is an
|
||||||
|
INP-shaped 200ms, so the two do not describe the same thing. No budget was
|
||||||
|
changed to make this green.
|
||||||
|
|
||||||
|
WebKit remains unavailable in this environment (`libevent-2.1-7t64`,
|
||||||
|
`libavif16` are not installed), so 14 browser-capability specs and the WebKit
|
||||||
|
E2E project are unverified here. Chromium and Firefox are 28/28 and visual is
|
||||||
|
5/5.
|
||||||
|
|
||||||
## Rules for updating this ledger
|
## Rules for updating this ledger
|
||||||
|
|
||||||
- A row moves out of `NOT_STARTED` only with a linked red test, its green run, and the commit id.
|
- A row moves out of `NOT_STARTED` only with a linked red test, its green run, and the commit id.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
|||||||
|
# TechLog UI migration baseline
|
||||||
|
|
||||||
|
## Source provenance
|
||||||
|
|
||||||
|
- Source path: `/home/donghyeon/workspace/techlog-studio-frontend`
|
||||||
|
- Inspected: 2026-08-15 (Asia/Seoul)
|
||||||
|
- Git revision/status: unavailable. The supplied source directory is an exported
|
||||||
|
checkout with no `.git` metadata; both `git rev-parse HEAD` and
|
||||||
|
`git status --short` report that it is not a Git repository. The source path
|
||||||
|
and the byte checksums below are the reproducible provenance available for
|
||||||
|
this baseline.
|
||||||
|
|
||||||
|
## Approved copied assets
|
||||||
|
|
||||||
|
| Source and target-relative path | SHA-256 |
|
||||||
|
| --- | --- |
|
||||||
|
| `public/favicon.svg` | `e6d2e59b7b5bbb0342e0fb496dfc262decbfe4426bbb7b047aec8d467d1dc6f7` |
|
||||||
|
| `public/media/fetch-strategy-boundary.svg` | `b07926823ed77fc200f962a1f64a440e130cefa6bed31144b611612af9b02606` |
|
||||||
|
|
||||||
|
Only these two SVGs are approved for this baseline. They must remain exact
|
||||||
|
byte-for-byte copies of the source assets. The evidence key
|
||||||
|
`fetch-strategy-boundary` resolves to `/media/fetch-strategy-boundary.svg`
|
||||||
|
with dimensions `1080 × 420`, trigger label `Fetch Join과 Batch Fetch 비교
|
||||||
|
다이어그램 크게 보기`, and dialog label `Fetch Join과 Batch Fetch의 페이징 경계
|
||||||
|
확대`.
|
||||||
|
|
||||||
|
## Required dependency pins
|
||||||
|
|
||||||
|
| Package | Exact version |
|
||||||
|
| --- | --- |
|
||||||
|
| `pretendard` | `1.3.9` |
|
||||||
|
| `@fontsource/ibm-plex-mono` | `5.3.0` |
|
||||||
|
| `unified` | `11.0.5` |
|
||||||
|
| `remark-parse` | `11.0.0` |
|
||||||
|
| `remark-gfm` | `4.0.1` |
|
||||||
|
| `remark-directive` | `4.0.0` |
|
||||||
|
|
||||||
|
No Next.js, Vinext, or Cloudflare package is part of the migration baseline.
|
||||||
|
|
||||||
|
## Expected route inventory
|
||||||
|
|
||||||
|
| Layout | Route ID | Path |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| PUBLIC | `TECH_LOG_HOME` | `/` |
|
||||||
|
| PUBLIC | `TECH_LOG_EXPLORE` | `/explore` |
|
||||||
|
| PUBLIC | `TECH_LOG_EXPLORE_KIND` | `/explore/:kind` |
|
||||||
|
| PUBLIC | `TECH_LOG_CASE` | `/cases/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_REFERENCE` | `/references/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_QUESTION` | `/questions/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_TOPIC` | `/topics/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECTS` | `/projects` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT` | `/projects/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT_RECORDS` | `/projects/:slug/records` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT_DECISIONS` | `/projects/:slug/decisions` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT_ACTIVITY` | `/projects/:slug/activity` |
|
||||||
|
| PUBLIC | `TECH_LOG_RELEASES` | `/releases` |
|
||||||
|
| PUBLIC | `TECH_LOG_RELEASE` | `/releases/:version` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROFILE` | `/profile` |
|
||||||
|
| PUBLIC | `TECH_LOG_SEARCH` | `/search` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_HOME` | `/studio` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENTS` | `/studio/documents` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_NEW` | `/studio/documents/new` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_EDIT` | `/studio/documents/:id/edit` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_VALIDATION` | `/studio/documents/:id/validation` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PREVIEW` | `/studio/documents/:id/preview` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PUBLISH` | `/studio/documents/:id/publish` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_PUBLICATIONS` | `/studio/publications` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_PUBLICATION_PREVIEW` | `/studio/publications/:publicationEventId/preview` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_NOT_FOUND` | `/studio/*` |
|
||||||
|
| PUBLIC | `NOT_FOUND` | `*` |
|
||||||
|
|
||||||
|
## Source stylesheet inventory
|
||||||
|
|
||||||
|
- `app/globals.css` → `presentation/styles/globals.css` —
|
||||||
|
`901205054ee96fe15062aaef7ff39c701985fdece15b58f7656776d0b744e607`
|
||||||
|
- `app/studio.css` → `presentation/styles/studio.css` —
|
||||||
|
`d0d958baeb55b74796988d8c0967a7c62d211e595224e07912aa1f884e466fdd`
|
||||||
|
- `app/studio-editor.css` → `presentation/styles/studio-editor.css` —
|
||||||
|
`ae9a473192d24465021d021cd42a5a84f9bdf4c3af0664537b4e10006ca1f889`
|
||||||
|
- `components/studio/workflow.module.css` →
|
||||||
|
`presentation/styles/workflow.module.css` —
|
||||||
|
`8eaa478a88ef4d16429c7f6a32bb33631acb47deef0b40838edd0a72748f124e`
|
||||||
|
- `components/studio/publication-flow.module.css` →
|
||||||
|
`presentation/styles/publication-flow.module.css` —
|
||||||
|
`5b1fdebeb27248b5cebc700b12d15cf11fe70471a394c9dc643a98c4569e8674`
|
||||||
|
|
||||||
|
All five source/target pairs passed `cmp -s` and have identical SHA-256
|
||||||
|
digests. Production parity testing corrected the provisional bootstrap
|
||||||
|
assumption in the design: the target loads the byte-identical TechLog
|
||||||
|
`globals.css`, including its first-byte `@import "tailwindcss";`, exactly once
|
||||||
|
and does not load the starter `theme.css`. This reproduces the source cascade;
|
||||||
|
the resulting browser comparison is pixel-identical.
|
||||||
|
|
||||||
|
## Source-to-target browser parity
|
||||||
|
|
||||||
|
The source was copied to `/tmp/techlog-source-parity.I0CBK7` before build and
|
||||||
|
runtime caches were created. The supplied source directory was used only for
|
||||||
|
read-only file comparison; it was not edited. The temp-copy Vinext production
|
||||||
|
server on `4375` and the target `dist/server.mjs` production artifact on `4174`
|
||||||
|
were captured by one Playwright Chromium instance with two fresh contexts
|
||||||
|
under identical conditions:
|
||||||
|
|
||||||
|
- `ko-KR`, `Asia/Seoul`, light color scheme, reduced motion, device scale 1,
|
||||||
|
service workers blocked, and a fixed `2026-08-14T01:00:00.000Z` clock;
|
||||||
|
- 1000-pixel viewport height, full-page screenshots, `document.fonts.ready`,
|
||||||
|
Pretendard/IBM Plex font checks, zero-duration animation/transition/caret;
|
||||||
|
- no screenshot masks; exact RGBA pixel comparison plus normalized recursive
|
||||||
|
product-subtree tags, ordered children, complete classes, relevant
|
||||||
|
attributes, text/ARIA relationships, response metadata, boot lifecycle, and
|
||||||
|
console/request failure comparison.
|
||||||
|
|
||||||
|
The only attributes omitted by name after concrete diagnostics are React
|
||||||
|
Router `data-discover`, Next Image `data-nimg`/`decoding`/`srcset`, and Next SSR
|
||||||
|
`selected`; generated React IDs and CSS-module hashes are normalized by value.
|
||||||
|
No broad attribute class is omitted.
|
||||||
|
|
||||||
|
| Evidence group | Cases |
|
||||||
|
| --- | ---: |
|
||||||
|
| 27 canonical routes at 360 and 1440 pixels | 54 |
|
||||||
|
| Every additional known Public fixture slug/version | 18 |
|
||||||
|
| Public home breakpoint transitions | 12 |
|
||||||
|
| Studio screen/state fixtures | 19 |
|
||||||
|
| Ten unknown Public dynamic shapes at 360 and 1440 pixels | 20 |
|
||||||
|
| Search/menu/preview/dialog/publication interactions | 7 |
|
||||||
|
| **Total** | **130** |
|
||||||
|
|
||||||
|
Result: 130/130 passed, zero failed, zero different pixels, all recursive
|
||||||
|
DOM/class/attribute/text/ARIA trees and HTTP response metadata equal, and zero
|
||||||
|
unexplained source or target console/request errors. The target-only visual
|
||||||
|
regression suite exercises the same 130 cases against 129 committed PNGs; the
|
||||||
|
canonical and Studio-state 1440-pixel
|
||||||
|
`/studio/publications` cases intentionally share one identical snapshot.
|
||||||
|
Source screenshots were temporary comparison inputs and were not copied into
|
||||||
|
the target snapshot directory.
|
||||||
|
|
||||||
|
The checked clean-checkout command is
|
||||||
|
`TZ=Asia/Seoul corepack pnpm verify:tech-log-source-parity`; it runs directly
|
||||||
|
under the installed Node 24 runtime and has no `tsx` dependency. Durable
|
||||||
|
evidence is committed at
|
||||||
|
`docs/operations/evidence/tech-log-source-parity.json`. It records 178 source
|
||||||
|
checksums with tree digest
|
||||||
|
`6724c2f898eefc62d2fc0ee695bccc3ae61a69c5153ed43c69f2cf99ee45bca5`,
|
||||||
|
target candidate `3a7c5deca06679fb9b8710da2cce87bbca07ce8a`, build-manifest digest
|
||||||
|
`3579650faa482f566553c00d8b4a05a05b4f7a1ab93b83e48676289bbcf02984`,
|
||||||
|
Vite-manifest digest
|
||||||
|
`cfd583ed7b6c27dce7f47389447608636b6b9df3e28df00c6416674da2c7c46d`,
|
||||||
|
case inventory digest
|
||||||
|
`5689bcdb5d5637205cdeb92b7c57f72d99f0b039818b8f90afdde526bbafe0ac`,
|
||||||
|
and evidence payload digest
|
||||||
|
`f046047beded19ce468be607dcf99c6b74d4e07323457ec7145c56d2f67d2c79`.
|
||||||
|
|
||||||
|
The production source responds to both a missing dynamic slug such as
|
||||||
|
`/projects/missing-project` and an unmatched path such as
|
||||||
|
`/definitely-not-a-product-route` with HTTP 404,
|
||||||
|
`text/plain;charset=UTF-8`, and the exact nine-byte body `Not Found`. The
|
||||||
|
target production server intentionally removes the Public shell for those
|
||||||
|
paths and matches that response in direct HTTP and Chromium regressions. Known
|
||||||
|
Public routes and all known Studio routes remain SPA-served; `/studio/*`
|
||||||
|
unknown paths preserve the source's in-shell HTML with HTTP 404. This is the
|
||||||
|
observed production source contract, not a generic runtime error.
|
||||||
|
|
||||||
|
## Accessibility review boundary
|
||||||
|
|
||||||
|
Automated Chromium `@a11y` coverage passes 29/29. The source-controlled manual
|
||||||
|
inventory contains exactly the 27 installed route IDs and removes the obsolete
|
||||||
|
starter records. Every human record remains `pending-manual-review` until one
|
||||||
|
reviewer evaluates keyboard, focus, modal/error behavior, color, reduced
|
||||||
|
motion, and screen-reader output against immutable candidate
|
||||||
|
`3a7c5deca06679fb9b8710da2cce87bbca07ce8a`. Therefore
|
||||||
|
`corepack pnpm review:a11y-manual` intentionally exits nonzero with 27
|
||||||
|
incomplete records and `release IDs do not match`; neither the manual gate nor
|
||||||
|
`FE-GATE-009` is represented as passing. The exact completion format is in
|
||||||
|
`docs/accessibility/manual-checklist.md`, and signed evidence must be committed
|
||||||
|
separately after it cites the candidate SHA.
|
||||||
|
|
||||||
|
## Governed route migration
|
||||||
|
|
||||||
|
The migration records 23 TechLog-owned breaking-change evidence IDs with owner
|
||||||
|
`tech-log-frontend`, an atomic same-release route/runtime/manifest migration,
|
||||||
|
and rollback to `05e3d50ba01f01c27f257d2e9040c2bc413ea053`. The accepted registry
|
||||||
|
snapshot digest is
|
||||||
|
`428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b`;
|
||||||
|
its approval reason is `Install approved TechLog Public and Studio route
|
||||||
|
contract`. Final compatibility impact is `none` with no unacknowledged change.
|
||||||
|
|
||||||
|
## Restricted-environment test baseline
|
||||||
|
|
||||||
|
The exact repository aggregate command was run in the managed workspace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm test:all
|
||||||
|
```
|
||||||
|
|
||||||
|
Its fresh staged-candidate runtime-schema phase passed 3 files/40 tests. The
|
||||||
|
unit phase passed 122 files/1,773 tests and reported 19 failures, all in the
|
||||||
|
pre-existing `ci-artifact-contract` provider/cgroup, RLIMIT/EMFILE,
|
||||||
|
restrictive-umask, `/tmp`, timing, and identity environment cases. No
|
||||||
|
`tests/features/tech-log` test failed. A pre-staging diagnostic run also found
|
||||||
|
39 `APP_HOME` release-inventory failures because new serving files were not yet
|
||||||
|
visible to `git ls-files`; staging the complete candidate fixed that test
|
||||||
|
precondition, and all 39 disappeared. The same run's one guardian aggregate
|
||||||
|
timeout passed 1/1 in isolation and 21/21 in the fresh staged aggregate.
|
||||||
|
|
||||||
|
The failing files were then reproduced in isolation outside that child-process
|
||||||
|
restriction:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts tests/unit/ci-workflow-generation.test.ts tests/unit/http-scenario-evidence.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: 3 files/529 tests, 510 passed and the same 19 environment-only
|
||||||
|
`ci-artifact-contract` cases failed. The 407 `ci-workflow-generation` and 14
|
||||||
|
`http-scenario-evidence` tests all passed. These 19 contain no TechLog code or
|
||||||
|
test. The aggregate phases after unit were also run directly: component 18
|
||||||
|
files/124 tests, integration 11 files/82 tests under the required child-process
|
||||||
|
scope, reference feature 4 files/13 tests, and recipes 2 files/17 tests all
|
||||||
|
passed.
|
||||||
|
|
||||||
|
The inventories in this document are human review baselines. Automated
|
||||||
|
coverage is intentionally limited to the two SVG byte contracts, evidence
|
||||||
|
asset lookup behavior, package manager frozen-lockfile verification, committed
|
||||||
|
target visual regressions, and governed registry/release gates. The external
|
||||||
|
source path is never required by committed CI tests.
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Template merge — `main` → `feature/techlog-ui-migration`
|
||||||
|
|
||||||
|
Record of how the frontend template sync on `main` was integrated into the
|
||||||
|
TechLog UI migration branch, and of every decision taken to resolve a conflict.
|
||||||
|
|
||||||
|
## Why this direction first
|
||||||
|
|
||||||
|
The template sync landed on `main` while the UI migration ran in a worktree.
|
||||||
|
Two orders were possible.
|
||||||
|
|
||||||
|
Merging the feature branch into `main` first would have resolved 15 conflicts
|
||||||
|
directly on the integration branch: a bad resolution would already be on `main`,
|
||||||
|
and it would have to be repaired forward, on the branch other work depends on.
|
||||||
|
|
||||||
|
Merging `main` into the worktree first keeps the resolution where the UI work
|
||||||
|
lives. Every gate runs against the resolved tree before anything reaches `main`,
|
||||||
|
and a resolution that turns out wrong is discarded by resetting one feature
|
||||||
|
branch. `main` is then only ever fast-forwarded, so it never holds a state that
|
||||||
|
was not already proved in the worktree.
|
||||||
|
|
||||||
|
That is the order used here.
|
||||||
|
|
||||||
|
## Starting state
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| merge base | `325a2a0` |
|
||||||
|
| `main` | `bdee07a` — `chore: sync the frontend template from a0fbafb to 5434760`, 1 commit, 101 files, +3116/−448 |
|
||||||
|
| `feature/techlog-ui-migration` | `0355b64` — 29 commits |
|
||||||
|
| files changed by `main` | 101 |
|
||||||
|
| files changed by the feature branch | 423 |
|
||||||
|
| overlap | 23 |
|
||||||
|
|
||||||
|
Rollback refs were created before touching anything:
|
||||||
|
|
||||||
|
```
|
||||||
|
backup/ui-before-template-merge → 0355b64
|
||||||
|
backup/main-before-ff → bdee07a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-merge baseline on the feature branch
|
||||||
|
|
||||||
|
Captured so that a pre-existing failure could not be mistaken for a merge
|
||||||
|
regression.
|
||||||
|
|
||||||
|
| Gate | Result |
|
||||||
|
| --- | --- |
|
||||||
|
| `check:types` | pass |
|
||||||
|
| `lint` | pass |
|
||||||
|
| `verify:documentation` | `PASS_SCOPED` |
|
||||||
|
| `tests/unit` + `tests/component` | 1815 passed / 101 failed |
|
||||||
|
|
||||||
|
The 101 failures were confined to `tests/unit/ci-artifact-contract.test.ts` (19)
|
||||||
|
and `tests/unit/ci-workflow-generation.test.ts` (82) — sandbox subprocess gates
|
||||||
|
that cannot run in this environment.
|
||||||
|
|
||||||
|
## Conflicts and how each was decided
|
||||||
|
|
||||||
|
15 conflicts. The rule applied throughout: **keep the template's mechanism, keep
|
||||||
|
the product's content, and never invent a third state that neither branch
|
||||||
|
would accept.**
|
||||||
|
|
||||||
|
### Deletions the UI migration made deliberately (2)
|
||||||
|
|
||||||
|
| Path | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/presentation/examples/platform-overview-page.tsx` | deletion kept |
|
||||||
|
| `tests/visual/.../platform-overview-light-chromium-visual-linux.png` | deletion kept |
|
||||||
|
|
||||||
|
`main` modified both; the feature branch deleted them in `c5c8b94`. Nothing on
|
||||||
|
the branch references either, so the deletion stands.
|
||||||
|
|
||||||
|
### `src/features/installed-feature-contracts.ts`
|
||||||
|
|
||||||
|
The template introduced a product manifest: registries are composed from
|
||||||
|
`INSTALLED_PRODUCT_FEATURES` so that narrowing the selection withdraws a
|
||||||
|
feature's routes, operations, schemas and messages.
|
||||||
|
|
||||||
|
The manifest composition is kept for operations, schemas, mappers and
|
||||||
|
invalidation. The **route registry is deliberately not composed from
|
||||||
|
`contract.routes`**, because the reference feature still declares
|
||||||
|
`REFERENCE_RESOURCE_*` routes whose screens this product deleted during the
|
||||||
|
migration. Reducing over them would register paths with no component behind
|
||||||
|
them — a typed deep link resolving to nothing.
|
||||||
|
|
||||||
|
The registry therefore lists the platform routes (empty on this branch) and
|
||||||
|
TechLog's. This was caught by running the gates: the first resolution did
|
||||||
|
compose from `contract.routes`, and `tests/unit/product-features.test.ts`
|
||||||
|
rejected it.
|
||||||
|
|
||||||
|
`ROUTE_FEATURE_OWNER` was narrowed to registered routes for the same reason.
|
||||||
|
Attributing an unregistered route to a feature claims the kill switch governs
|
||||||
|
something no router can mount.
|
||||||
|
|
||||||
|
### `src/features/installed-feature-runtimes.tsx`
|
||||||
|
|
||||||
|
The template gates the reference feature's route codecs and components on the
|
||||||
|
manifest. This product deleted that feature's presentation layer, so the import
|
||||||
|
does not resolve and there is nothing to gate. The gating was removed and the
|
||||||
|
reason recorded in the file; it belongs back the day those screens return.
|
||||||
|
|
||||||
|
Consequence: this file no longer references the manifest, so it was moved to the
|
||||||
|
exempt list in `tests/unit/product-features.test.ts` with the same note.
|
||||||
|
`tests/component/product-feature-switch.test.tsx` was rewritten to hold the
|
||||||
|
invariant that still applies here — no registered route without a component —
|
||||||
|
which is exactly the trap the first resolution fell into.
|
||||||
|
|
||||||
|
### `src/features/installed-feature-adapters.ts`
|
||||||
|
|
||||||
|
Both the manifest check and TechLog's input are kept. The reference feature's
|
||||||
|
input stays `Partial` because the manifest may narrow it out; TechLog's is total
|
||||||
|
because it is this product's own UI and is always installed. A build that
|
||||||
|
narrows the reference feature out still ships TechLog.
|
||||||
|
|
||||||
|
### `src/features/installed-feature-messages.ts`
|
||||||
|
|
||||||
|
The template's rationale — message keys stay total so `message()` cannot become
|
||||||
|
partial — is kept, and TechLog's catalog is merged in on the same terms.
|
||||||
|
|
||||||
|
### `src/presentation/routes/app-router.tsx`
|
||||||
|
|
||||||
|
The template looked the route definition and runtime component up by id inside
|
||||||
|
the route element; this branch passes both as props from the grouped route
|
||||||
|
contract. The prop-driven signature is kept.
|
||||||
|
|
||||||
|
The template's **feature kill switch is adopted**: a route whose owning feature
|
||||||
|
the runtime document disabled renders the disabled surface instead of mounting.
|
||||||
|
Withdrawing it from navigation alone would leave a working deep link. `getRoute`
|
||||||
|
was dropped from the imports because the definition arrives as a prop.
|
||||||
|
|
||||||
|
### `vite.config.ts`
|
||||||
|
|
||||||
|
Two independent additions — TechLog route chunking and `routerBasePath` — both
|
||||||
|
kept. A brace was lost in the first concatenation and caught by `check:types`.
|
||||||
|
|
||||||
|
### `scripts/build-frontend.ts`
|
||||||
|
|
||||||
|
Both new steps exist in the merged body, so the header comment was renumbered:
|
||||||
|
runtime config becomes step 4, the TechLog serving boundary step 7, the release
|
||||||
|
manifest step 8, and the inline step comments were corrected to match.
|
||||||
|
|
||||||
|
### `scripts/test-performance.ts`
|
||||||
|
|
||||||
|
The template clicked a navigation link before measuring; this product measures
|
||||||
|
its own landing route, which `goto` already reached, and has no `targetLabel`.
|
||||||
|
The click was dropped. The template's lesson was kept as a comment because it
|
||||||
|
applies to the next click that lands here: Playwright matches accessible names
|
||||||
|
by substring, so a nav entry can also match a call to action and resolve to two
|
||||||
|
links — a strict-mode violation that produces no performance evidence at all.
|
||||||
|
|
||||||
|
### Derived baselines — recomputed, not chosen (3)
|
||||||
|
|
||||||
|
`scripts/contracts/ci-gates.ts`, `tests/unit/task3-selective-integration.test.ts`
|
||||||
|
and `tests/unit/ci-workflow-generation.test.ts` each pin counts and a digest
|
||||||
|
describing the gate contract. **Neither side's numbers describe the merged
|
||||||
|
`config/ci/gates.json`**, so taking either would have been wrong. They were
|
||||||
|
recomputed from the merged file:
|
||||||
|
|
||||||
|
| Value | Result |
|
||||||
|
| --- | --- |
|
||||||
|
| canonical gate shape SHA-256 | `5063586d799f51de94c0f0ddaf9b75e180825bba5051bc309550425013ea81ef` |
|
||||||
|
| gates | 27 |
|
||||||
|
| commands | 82 |
|
||||||
|
| command references | 94 |
|
||||||
|
| evidence artifact references | 107 |
|
||||||
|
| artifacts | 128 (126 product + 2 from the template) |
|
||||||
|
|
||||||
|
### `README.md`, `docs/accessibility/manual-checklist.md`
|
||||||
|
|
||||||
|
The template enumerated its own example routes. Replacing them with generic
|
||||||
|
prose broke `verify:documentation`, which requires both documents to name every
|
||||||
|
installed route id — a rule the template sync itself introduced. Both documents
|
||||||
|
now enumerate this product's 27 routes.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
Merge commit parents: `0355b64` (UI) and `bdee07a` (template).
|
||||||
|
|
||||||
|
### Gates on the merged tree
|
||||||
|
|
||||||
|
| Gate | Result |
|
||||||
|
| --- | --- |
|
||||||
|
| `check:types` | pass (6 projects) |
|
||||||
|
| `lint` | pass, `--max-warnings=0` |
|
||||||
|
| `check:architecture` | 399 modules, 1232 dependencies, 12 fixtures pass |
|
||||||
|
| `check:adapter-inventory` | 120 files, 5 importers of the shared abort primitive |
|
||||||
|
| `check:remediation-ledger` | 25 dispositions, 0 open |
|
||||||
|
| `check:diagnostics` | 8 diagnostics, 5 telemetry producers |
|
||||||
|
| `check:i18n` | 197 keys, 4 locales |
|
||||||
|
| `check:design-system` | 48 tokens |
|
||||||
|
| `verify:documentation` | `PASS_SCOPED` |
|
||||||
|
| `test:integration` | 81 passed |
|
||||||
|
| `test:recipes` | 17 passed |
|
||||||
|
| `test:runtime-schema` | 40 passed |
|
||||||
|
| `tests/unit` + `tests/component` | 1851 passed / 98 failed |
|
||||||
|
|
||||||
|
The 98 failures are the same two sandbox files as the baseline —
|
||||||
|
`ci-workflow-generation` (82) and `ci-artifact-contract` (16, down from 19). No
|
||||||
|
file fails that did not fail before the merge.
|
||||||
|
|
||||||
|
## Mistake made during this merge
|
||||||
|
|
||||||
|
`git stash` was run inside the worktree while the merge was still in progress,
|
||||||
|
to compare a gate against the pre-merge tree. That removed `MERGE_HEAD`: the
|
||||||
|
resolved content survived, but git no longer knew a merge was underway, and
|
||||||
|
committing then would have produced a single-parent commit — leaving `main` off
|
||||||
|
the ancestry and breaking the fast-forward that step 2 depends on. `MERGE_HEAD`
|
||||||
|
was restored to `bdee07a` before committing, and the resulting commit has both
|
||||||
|
parents.
|
||||||
|
|
||||||
|
To compare against a pre-merge state, use a separate checkout rather than
|
||||||
|
stashing an in-progress merge.
|
||||||
|
|
||||||
|
## Correction after review
|
||||||
|
|
||||||
|
Two of the resolutions above were wrong, and were fixed in a follow-up commit.
|
||||||
|
|
||||||
|
The template's demonstration screens — `platform-overview-page`, the UI and
|
||||||
|
state galleries, the auth example and the reference feature's screens — exist to
|
||||||
|
explain the template. A product replaces them with its domain, and deleting them
|
||||||
|
is the expected end state, not a regression. Two gates were nevertheless coupled
|
||||||
|
to them, and the first resolution accommodated that coupling instead of fixing
|
||||||
|
it.
|
||||||
|
|
||||||
|
### `tests/unit/product-features.test.ts` — a hard-coded exemption became a rule
|
||||||
|
|
||||||
|
The guard requires every installed registry to compose from the manifest.
|
||||||
|
`installed-feature-runtimes.tsx` was added to its exempt list once the reference
|
||||||
|
feature's presentation layer was gone. That silenced the guard for that file
|
||||||
|
permanently.
|
||||||
|
|
||||||
|
It now derives its own scope: a registry must gate on the manifest **when it
|
||||||
|
imports a module belonging to a manifest-declared feature**. A product whose
|
||||||
|
registries compose only its own domain drops out of the rule honestly, and the
|
||||||
|
guard fires again the moment a declared feature is imported without gating —
|
||||||
|
verified by removing the manifest reference from
|
||||||
|
`installed-feature-adapters.ts` and watching the guard fail. A counter asserts
|
||||||
|
the sweep is still watching at least one file, so an empty scope cannot pass
|
||||||
|
silently.
|
||||||
|
|
||||||
|
### `tests/component/product-feature-switch.test.tsx` — coverage restored
|
||||||
|
|
||||||
|
The end-to-end kill-switch assertions were replaced with composition checks
|
||||||
|
because the screens they rendered were gone. The mechanism under test is the
|
||||||
|
ownership lookup plus `isFeatureActive`, which has nothing to do with which
|
||||||
|
screens ship, so **the ownership map is now the fixture**: one real registered
|
||||||
|
route is attributed to a real installed feature, and the router, shell,
|
||||||
|
components and codecs are all the product's own. The deep-link half is asserted
|
||||||
|
end to end again.
|
||||||
|
|
||||||
|
### What that restoration exposed
|
||||||
|
|
||||||
|
The navigation-withdrawal half **is not implemented in this product**. It lives
|
||||||
|
in the template's `PrimaryNavigation`, and this product does not render the
|
||||||
|
template's `AppShell` at all — the public site header is a hand-written list of
|
||||||
|
paths in `src/features/tech-log/presentation/public/components/site-header.tsx`,
|
||||||
|
and the studio has its own shell.
|
||||||
|
|
||||||
|
So a disabled feature's route is refused by the router but its link would still
|
||||||
|
be advertised. That is harmless only while no feature-owned route is navigable,
|
||||||
|
which is true today and is now asserted. If that assertion fails, the header has
|
||||||
|
to consult `ROUTE_FEATURE_OWNER` — or navigation has to move back onto
|
||||||
|
`NAVIGATION_ROUTES` — before the route ships.
|
||||||
|
|
||||||
|
## Follow-ups this merge deliberately did not decide
|
||||||
|
|
||||||
|
1. **TechLog is outside the product manifest.** It is composed directly rather
|
||||||
|
than as a `SelectableProductFeature`, so the runtime kill switch does not
|
||||||
|
govern it. That is defensible — a product's own domain is not an optional
|
||||||
|
feature — but it means the switch governs nothing user-visible today.
|
||||||
|
2. **The reference feature declares routes it cannot serve.** Its screens were
|
||||||
|
deleted with the rest of the demonstration UI, but its contract still
|
||||||
|
declares `REFERENCE_RESOURCE_*` routes. Either the declarations go, or the
|
||||||
|
feature does. `TechLog` does not import it (`git grep reference-feature --
|
||||||
|
src/features/tech-log` is empty), so removing it is a live option and
|
||||||
|
FE-GATE-020 exists to prove it can be removed.
|
||||||
|
3. **The public site header is not feature-aware.** See above.
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
# Tech Log 운영 출시 전 체크리스트 — 실측 검증 보고서
|
||||||
|
|
||||||
|
**검증일** 2026-08-19 · **방식** 두 저장소를 로컬에서 실제 기동해 엔드포인트·브라우저 단위로 실측
|
||||||
|
|
||||||
|
| 대상 | 위치 | 리비전 |
|
||||||
|
|---|---|---|
|
||||||
|
| Frontend | `tech-log-frontend` | `main` eb86708 → `fix/release-gate-frontend` fff5e6f |
|
||||||
|
| Backend | `tech-log-backend` | `develop` ab0447a |
|
||||||
|
| Keycloak | 로컬 컨테이너 `local-keycloak` | 26.7.0 (`:18080`) |
|
||||||
|
| PostgreSQL | 로컬 컨테이너 `techlog-pg` | 16.15 (`:5433`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 요약 판정: **출시 보류 (P0 미충족)**
|
||||||
|
|
||||||
|
체크리스트 §31의 P0 항목 중 **인증 우회 불가 · 인가 우회 불가 · Studio 주요 기능 정상 · Publish 정상**이
|
||||||
|
현재 충족되지 않는다. 아래 근거는 전부 실행 결과다.
|
||||||
|
|
||||||
|
### 가장 중요한 구조적 사실
|
||||||
|
|
||||||
|
백엔드는 계약(`studio-v1.yaml`)이 선언한 **18개 오퍼레이션 중 2개**만 구현되어 있다.
|
||||||
|
|
||||||
|
| 상태 | 오퍼레이션 |
|
||||||
|
|---|---|
|
||||||
|
| 구현됨 (2) | `getStudioSession`, `listStudioCatalog` |
|
||||||
|
| 미구현 (16) | `getStudioDashboard`, `listStudioDocuments`, `createStudioDocument`, `getStudioDocument`, `saveStudioDocument`, `validateStudioDocument`, `getCurrentStudioPreview`, `createStudioPreview`, `publishStudioDocument`, `listStudioPublications`, `unpublishStudioPublication`, `getStudioPublicationSnapshot`, `listStudioAssets`, `uploadStudioAsset`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset` |
|
||||||
|
|
||||||
|
또한 **Public 읽기 엔드포인트는 계약에 아예 없다.** `studio-v1.yaml`은 Studio 전용이고,
|
||||||
|
프론트엔드의 Public 화면(`/`, `/explore`, `/projects`, `/releases`, 문서 상세)은
|
||||||
|
`src/features/tech-log/adapters/static/public-content.ts`의 **번들에 컴파일된 정적 콘텐츠**를 읽는다.
|
||||||
|
|
||||||
|
따라서 체크리스트의 다음 절은 검증 대상 자체가 존재하지 않는다:
|
||||||
|
§2(탐색·검색·프로젝트·변경기록의 백엔드 연동), §3.1~3.3(문서 작성·관계·Publish),
|
||||||
|
§12(Public/Private 데이터 경계), §17(파일/Object Storage), §29(E2E 시나리오).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — 출시 차단 결함
|
||||||
|
|
||||||
|
### P0-1. Studio 라우트가 인증을 검사하지 않았다 — **수정 완료**
|
||||||
|
|
||||||
|
`TECH_LOG_ROUTE_REGISTRY`가 모든 TechLog 라우트를 `access: "public"`으로 등록하고 있었다.
|
||||||
|
라우터에 `decideRouteAccessForDefinition` 가드가 존재하지만 Studio에 대해 무력화된 상태였다.
|
||||||
|
|
||||||
|
운영 프로파일 빌드(`AUTH_MODE=external`)로 실측한 수정 전:
|
||||||
|
|
||||||
|
```
|
||||||
|
/studio http=200 h1="작업 흐름" ← 비로그인 상태에서 Studio UI 렌더링
|
||||||
|
/studio/documents http=200 h1="작업본"
|
||||||
|
/studio/assets http=200 h1="Asset"
|
||||||
|
```
|
||||||
|
|
||||||
|
`spec.layoutGroup === "STUDIO"`에서 `access`를 유도하도록 수정한 뒤:
|
||||||
|
|
||||||
|
```
|
||||||
|
/studio http=200 h1="로그인 연동이 필요합니다."
|
||||||
|
/studio/documents http=200 h1="로그인 연동이 필요합니다."
|
||||||
|
/ , /explore 변화 없음
|
||||||
|
```
|
||||||
|
|
||||||
|
로그인 후 원래 요청 화면으로 복귀하는 것도 확인했다(`/studio/documents` → 로그인 → `작업본`).
|
||||||
|
|
||||||
|
커밋 `fff5e6f`.
|
||||||
|
|
||||||
|
### P0-2. 백엔드 Studio API에 인가 검사가 없다 — **미해결**
|
||||||
|
|
||||||
|
`SecurityConfig`는 `anyRequest().authenticated()`로 끝나고, Studio 컨트롤러에
|
||||||
|
`@RequiresPermission` 계열 애노테이션이 **하나도 없다**.
|
||||||
|
|
||||||
|
Keycloak에 Studio 권한이 없는 사용자(`plain`, realm role `plain-user`)를 만들어 확인:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/studio/catalog?type=TOPIC
|
||||||
|
studio 사용자 (studio-author) → HTTP 200
|
||||||
|
plain 사용자 (권한 없음) → HTTP 200 ← 인가 우회
|
||||||
|
```
|
||||||
|
|
||||||
|
체크리스트 §11 "인증된 사용자라고 해서 무조건 Studio API를 호출할 수 있지 않다",
|
||||||
|
§31 P0 "인가 우회 불가" 미충족.
|
||||||
|
|
||||||
|
### P0-3. 모든 Studio 경로가 `/api/api/v1/...`에 매핑된다 (Double Prefix) — **미해결**
|
||||||
|
|
||||||
|
`PresentationWebConfig`가 `configurer.addPathPrefix("/api", c -> true)`로 전 컨트롤러에
|
||||||
|
`/api`를 붙이는데, Studio 컨트롤러는 `@GetMapping("/api/v1/studio/...")`로 이미 `/api`를 포함해 선언한다.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/studio/catalog → 404 ROUTE_NOT_FOUND
|
||||||
|
GET /api/api/v1/studio/catalog → 200
|
||||||
|
GET /api/v1/studio/session → 404 ROUTE_NOT_FOUND
|
||||||
|
GET /api/api/v1/studio/session → 503
|
||||||
|
```
|
||||||
|
|
||||||
|
프론트엔드는 계약대로 `/api/v1/studio/...`를 호출하므로 **현재 상태로는 단 한 건도 연결되지 않는다.**
|
||||||
|
체크리스트 §25 "`/api` Prefix 처리에서 Double Prefix가 발생하지 않는다" 미충족.
|
||||||
|
|
||||||
|
### P0-4. `getStudioSession`이 항상 503을 반환한다 — **미해결**
|
||||||
|
|
||||||
|
`auth-mode: jwt`(저장소 기본값, `src/.env:115`)에서 `SecurityConfig`가 `csrf.disable()`로
|
||||||
|
`CsrfFilter`를 제거하므로 `CsrfToken` 파라미터가 항상 `null`이고, 컨트롤러는 이를
|
||||||
|
`STUDIO_UNAVAILABLE`(503)로 정직하게 보고한다.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/api/v1/studio/session (유효한 studio 토큰)
|
||||||
|
→ 503 {"code":"STUDIO_UNAVAILABLE","category":"TRANSIENT_DEPENDENCY","retryable":true}
|
||||||
|
로그: "CSRF token unavailable: CSRF protection is disabled for the active auth-mode"
|
||||||
|
```
|
||||||
|
|
||||||
|
프론트엔드 HTTP 모드는 `getStudioSession`으로 CSRF 토큰을 받아 부트스트랩하므로,
|
||||||
|
**이 한 건 때문에 Studio HTTP 경로 전체가 시작조차 못 한다.**
|
||||||
|
`auth-mode: redis-session`에 필요한 세션 빈이 저장소에 없다는 점은 백엔드 HANDOFF.md도 명시하고 있다.
|
||||||
|
|
||||||
|
### P0-5. `main` 브랜치의 dev 부팅이 깨져 있었다 — **수정 완료**
|
||||||
|
|
||||||
|
`eb86708`(계약 3.0.0 머지) 이후 `public/release-manifest.json`이 2.0.0으로 남아
|
||||||
|
부팅 시 contract-set 검증이 fail-closed → **빈 화면**. 이전에 한 번 겪은 것과 같은 실패 양식이다.
|
||||||
|
|
||||||
|
```
|
||||||
|
setDigest drift: manifest sha256:e0da7765…, build sha256:261ac630…
|
||||||
|
package drift: manifest 2.0.0 / ce2e748 vs build 3.0.0 / b20d7a2
|
||||||
|
```
|
||||||
|
|
||||||
|
`generate:dev-release-manifest`로 재생성하고, 같은 값을 하드코딩하던
|
||||||
|
`tests/runtime-schema/release-manifest.test.ts`도 함께 갱신했다. 커밋 `fff5e6f`.
|
||||||
|
|
||||||
|
### P0-6. 커밋된 `.env`로는 prod 프로파일이 부팅하지 않는다 — **미해결**
|
||||||
|
|
||||||
|
`src/.env:140`이 `APP_DATASOURCE_DDL_AUTO=update`인데, `application-prod.yml`이 문서화한
|
||||||
|
`JpaSchemaSafetyValidator`는 prod에서 `none|validate`만 허용하고 위반 시 exit 71로 종료한다.
|
||||||
|
|
||||||
|
### P0-7. `ddl-auto=validate`로는 PostgreSQL에서 부팅하지 않는다 — **미해결**
|
||||||
|
|
||||||
|
```
|
||||||
|
SchemaManagementException: Schema-validation: missing table [fs_cleanup_item]
|
||||||
|
```
|
||||||
|
|
||||||
|
`PostgreSqlPersistenceConfig`가 Flyway 위치를 `classpath:db/migration/postgresql`로 고정해
|
||||||
|
`db/migration/jpa/fileserver` 트리가 **한 번도 적용되지 않는데**, 해당 JPA 엔티티는 스캔된다.
|
||||||
|
`ddl-auto=update`가 이 사실을 가려 온 것이고, prod가 요구하는 `validate`로 바꾸는 순간 드러난다.
|
||||||
|
(본 검증은 `ddl-auto=none`으로 우회해 진행했다.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — 출시 전 해결 권장
|
||||||
|
|
||||||
|
| # | 항목 | 실측 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| P1-1 | Keycloak realm 구성이 두 저장소 어디에도 없다 | compose에 keycloak 서비스 없음, realm export 파일 없음. 검증을 위해 `ca-skeleton` realm·클라이언트·audience 매퍼·테스트 사용자를 수기로 생성해야 했다. §27 "Keycloak Realm 설정을 복원할 수 있다" 미충족 |
|
||||||
|
| P1-2 | 프론트엔드에 로그인 구현이 없다 | OIDC/Keycloak 클라이언트 코드 0건. `AUTH_MODE=external`은 호스팅 페이지가 `window.__CA_FRONTEND_AUTH_OWNER__`를 주입하기를 기대하며, 없으면 `createUnavailableSessionAdapter`가 "로그인 연동이 필요합니다"를 띄운다. §1.4 인증 항목 전부 검증 불가 |
|
||||||
|
| P1-3 | production 런타임 설정이 플레이스홀더 | `API_BASE_URL: https://api.example.com/`, `TELEMETRY_ENDPOINT: https://telemetry.example.com/v1/events` |
|
||||||
|
| P1-4 | Rate Limit 비활성 | `APP_RATE_LIMIT_ENABLED=false`, `APP_RATE_LIMIT_PROVIDER=disabled`. 60회 연속 호출 전부 200 |
|
||||||
|
| P1-5 | 보안 헤더를 적용하는 주체가 없다 | `config/hosting/security-headers.json`에 CSP·HSTS·X-Frame-Options 등이 정의돼 있으나 `dist/server.mjs`는 **하나도 적용하지 않는다**. `verify:hosting-headers`는 기본적으로 fixture 모드로 동작해 실 서버를 검사하지 않는다 |
|
||||||
|
| P1-6 | 캐시 정책도 미적용 | `cache-policy.json`은 `/assets/*`에 `public, max-age=31536000, immutable`을 요구하나 실제 응답은 전부 `no-cache` |
|
||||||
|
| P1-7 | 프론트엔드 배포 아티팩트 부재 | Dockerfile·nginx conf·compose 없음. `dist/server.mjs`는 프리뷰용이지 운영 파일 서버가 아니다 |
|
||||||
|
| P1-8 | robots.txt / sitemap.xml 없음 | **Studio 경로가 검색 엔진에 차단되지 않는다.** §7 미충족 |
|
||||||
|
| P1-9 | Open Graph·canonical 메타데이터 없음 | `dist/index.html`에 `og:*`·canonical 없음. `<title>`은 라우트별로 정상 동작하나 **런타임에 설정**되므로 JS를 실행하지 않는 공유 미리보기 크롤러에는 "Tech Log" 고정값만 노출된다 |
|
||||||
|
| P1-10 | DB 타임아웃 30초 | `APP_DATASOURCE_CONNECTION_TIMEOUT=30000`. `application.yml`이 문서화한 D2 fail-fast 의도(기본 5s)와 어긋난다. 프론트엔드 `REQUEST_TIMEOUT_MS=10000`이므로 DB 장애 시 프론트가 항상 먼저 끊겨 `DB_UNAVAILABLE` 503을 보지 못한다 |
|
||||||
|
| P1-11 | Tech Log Asset의 Object Storage 배선 없음 | objectstorage 어댑터는 템플릿 자산으로 존재하나 techlog 참조 0건, MinIO/S3 환경변수 0건, `uploadStudioAsset` 엔드포인트 미구현 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 검증되어 통과한 항목
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
| 항목 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| Production Build | PASS (local·production 프로파일 모두) |
|
||||||
|
| TypeScript compile | PASS (`check:types` 6개 프로젝트) |
|
||||||
|
| ESLint | PASS (수정 후 0 error) |
|
||||||
|
| 전체 테스트 | 1,818 passed / 16 skipped / **1 기존 flake** (`provider-guardian-transaction` — 단독 실행 2회 모두 PASS, 부하 의존) |
|
||||||
|
| architecture / contract / dev-release-manifest / browser-security 게이트 | PASS |
|
||||||
|
| Production 번들에 dev·localhost URL 없음 | PASS (`localhost`·`127.0.0.1` 0건, `.local` 매치는 전부 `locale`/`localeCompare`) |
|
||||||
|
| Production 번들에 Mock API 미포함 | PASS (`createMockStudioGateway` 0건) |
|
||||||
|
| Source Map 비공개 | PASS (`.map` 0개) |
|
||||||
|
| Route 단위 Lazy Loading | PASS (30 청크, 총 954 KB / 최대 569 KB) |
|
||||||
|
| SPA 라우팅·새로고침 | PASS (열거형 allowlist 방식. 존재하지 않는 문서 경로는 의도적으로 404) |
|
||||||
|
| Route별 `<title>` | PASS (`탐색 · Tech Log`, `프로젝트 · Tech Log` …) |
|
||||||
|
| 반응형 | PASS — 360/414/768/1440 × 6개 Public 라우트 **24개 조합 전부 가로 스크롤 없음** |
|
||||||
|
| 접근성 | PASS — axe(wcag2a/2aa/21a/21aa) **serious+critical 0건** (Public 6 + Studio 4 라우트). h1 정확히 1개, heading 건너뜀 없음, alt 누락 0, 레이블 없는 icon button 0 |
|
||||||
|
| 로그인 흐름 | PASS (게이트 → 로그인 → 원래 화면 복귀) |
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
| 항목 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| Production Profile Build | PASS — `:app-bootstrap:bootJar` 성공 |
|
||||||
|
| 전체 테스트 | PASS — **3,530 tests / 0 failures / 7 skipped** (BUILD SUCCESSFUL 8m 9s). app-bootstrap 797 · application-core 568 · cache-redis 423 · fileserver 398 · inbound-web 341 · httpclient 283 · shared-contract 224 · objectstorage 140 · persistence-jpa 122 · 그 외 |
|
||||||
|
| Docker Image Build | PASS — 623MB. `BUILD_VERSION`/`GIT_SHA`/`SOURCE_URL` build-arg를 강제하는 provenance 게이트가 있어 인자 없이는 의도적으로 실패한다 |
|
||||||
|
| Production Image 실제 실행 | PASS — 컨테이너에서 14.5초 기동, `healthcheck` 200 · `readiness` 200 · `catalog` 200(실데이터 2건) |
|
||||||
|
| Flyway 마이그레이션 (신규 DB, 처음부터) | PASS — 6개 적용, V7 techlog core 포함, 테이블 33개 생성 |
|
||||||
|
| 응답 봉투 일관성 | PASS — `{success,data,error,meta}` 전 경로 동일 |
|
||||||
|
| HTTP 상태 코드 | PASS — 401 / 404 / 405 / 422 / 500 / 503 모두 적절 |
|
||||||
|
| 인증 오류 코드 분리 | PASS — `AUTH_TOKEN_MISSING` / `AUTH_TOKEN_MALFORMED` / `AUTH_TOKEN_INVALID_SIGNATURE` / `AUTH_TOKEN_EXPIRED` |
|
||||||
|
| Validation | PASS — 잘못된 enum·필수 누락은 422 + `fieldErrors`, `limit` 상·하한 강제 |
|
||||||
|
| SQL Injection | PASS — `' OR 1=1--` 파라미터 바인딩되어 빈 결과 |
|
||||||
|
| Visibility 필터 | PASS — `ARCHIVED` 토픽이 catalog 결과에서 제외됨 |
|
||||||
|
| CORS | PASS — 허용 origin 200 + `Allow-Credentials: true`, 미허용 origin 403, 와일드카드 없음 |
|
||||||
|
| 보안 헤더 | PASS — `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Cache-Control: no-store` |
|
||||||
|
| 오류 정보 노출 | PASS — Stack trace·SQL·내부 클래스명 모두 미노출 (`details: null`) |
|
||||||
|
| 로그 위생 | PASS — 토큰·Authorization·쿠키·비밀번호 **0건**. `user=`는 가명화 해시 |
|
||||||
|
| 추적성 | PASS — 모든 요청 로그에 `req=`·`trace=`, `http_request method= uri_template= status= duration_ms=` |
|
||||||
|
| Metrics | PASS — Prometheus 127개 메트릭 패밀리 (`http_server_requests_seconds_bucket`, `jvm_gc_*`, `jvm_memory_*`, `hikaricp_connections_*`) |
|
||||||
|
| Liveness / Readiness 분리 | PASS — DB 중단 시 readiness 503 DOWN, liveness 200 UP 유지 |
|
||||||
|
| 의존성 장애 대응 | PASS(동작) — DB 중단 시 무한 대기 없이 **503 `DB_UNAVAILABLE` (retryable)** 반환, DB 복구 후 27ms/정상 데이터로 자동 회복. 단 응답까지 30초 소요(P1-10) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 다음 단계 (권장 순서)
|
||||||
|
|
||||||
|
1. **Double Prefix 해소** (P0-3) — 컨트롤러 매핑에서 `/api`를 제거하거나 `addPathPrefix` 대상에서 제외. 이걸 고치기 전에는 프론트-백엔드가 한 건도 연결되지 않으므로 최우선.
|
||||||
|
2. **세션 인프라** (P0-4) — `redis-session` 배선. 백엔드 HANDOFF.md도 Plan 02보다 앞선 선행 작업으로 지목하고 있다.
|
||||||
|
3. **Studio 인가** (P0-2) — Studio 컨트롤러에 권한 검사 추가 + 권한 없는 사용자 403 회귀 테스트.
|
||||||
|
4. **prod 부팅 설정** (P0-6, P0-7) — `.env`의 `ddl-auto`, fileserver 마이그레이션 위치.
|
||||||
|
5. **나머지 16개 오퍼레이션** — 백엔드 HANDOFF.md가 지적한 생성 union 5종의 Jackson 파손 전략 결정이 선행.
|
||||||
|
6. 배포 레이어 (P1-5·6·7) — nginx/CDN에 보안 헤더·캐시 정책 적용, 프론트엔드 이미지.
|
||||||
|
7. robots.txt로 Studio 차단 (P1-8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2차 검증 — 로컬에서 가능한 항목 완주 (2026-08-19)
|
||||||
|
|
||||||
|
1차에서 "물리적으로 불가능"이라 분류했던 항목 중 상당수가 실제로는 검증 가능했다.
|
||||||
|
Studio는 mock 게이트웨이가 18개 오퍼레이션을 **전부** 구현하고 있고(낙관적 락·검증
|
||||||
|
staleness·미리보기 만료·경고 승인·멱등성 포함), Public은 정적 콘텐츠지만 UI 동작
|
||||||
|
항목은 그대로 검증된다. 아래는 그 재검증 결과다.
|
||||||
|
|
||||||
|
## 1차 판정 정정
|
||||||
|
|
||||||
|
### 정정 1 — P0-6 은 결함이 아니다
|
||||||
|
|
||||||
|
`prod` 프로파일이 커밋된 `src/.env`로 부팅하지 않는 것은 **의도된 설계**다.
|
||||||
|
`application-prod.yml`이 문서화한 5개 startup validator가 개발용 값을 거부한다:
|
||||||
|
|
||||||
|
```
|
||||||
|
JpaSchemaSafetyValidator ddl-auto must be none|validate (exit 71)
|
||||||
|
FlywayProdSafetyValidator baseline-on-migrate / out-of-order / clean 비활성
|
||||||
|
StartupSafetyValidator error-detail 노출 · body-capture 로깅 off
|
||||||
|
PostgreSqlTransportSecurityValidator pgJDBC sslmode=verify-full
|
||||||
|
PersistenceVendorProdSafetyValidator vendor·URL 모두 H2 금지
|
||||||
|
```
|
||||||
|
|
||||||
|
실측으로 2개가 순서대로 발화하는 것을 확인했다:
|
||||||
|
|
||||||
|
```
|
||||||
|
exit=71 error.code=PROFILE_MISMATCH
|
||||||
|
"prod profile requires APP_DATASOURCE_DDL_AUTO ... to be none or validate,
|
||||||
|
but was update; Flyway is the production schema writer"
|
||||||
|
|
||||||
|
ddl-auto=none 으로 넘긴 뒤:
|
||||||
|
"prod PostgreSQL transport requires pgJDBC sslmode=verify-full"
|
||||||
|
```
|
||||||
|
|
||||||
|
즉 **§9 "운영 환경에서 개발용 설정이 활성화되지 않는다"는 PASS**다.
|
||||||
|
남는 진짜 갭은 별개다 — **운영 값 세트가 저장소에도 배포 시스템에도 아직 없다**(P1로 이동).
|
||||||
|
|
||||||
|
### 정정 2 — P0-7 의 심각도 하향
|
||||||
|
|
||||||
|
`ddl-auto=validate`가 `fs_cleanup_item` 누락으로 실패하는 것은 사실이나,
|
||||||
|
prod는 `none|validate` **둘 다** 허용하므로 `none`으로 부팅할 수 있다(실제로 그렇게 기동해 검증했다).
|
||||||
|
따라서 출시 차단은 아니고, **스키마 검증을 포기해야 한다는 제약**으로 남는다 → P1.
|
||||||
|
|
||||||
|
### 정정 3 — 1차의 오탐 2건
|
||||||
|
|
||||||
|
- **"Public UI에 Studio 노출"** — 오탐. 매칭된 "Studio"는 전부 게시된 릴리스 노트의 본문
|
||||||
|
텍스트였다("TechLog Public·Studio 경계를 확정했습니다"). 실제 `a[href^="/studio"]`는
|
||||||
|
모든 Public 화면에서 **0건**. §1.3 PASS.
|
||||||
|
- **"코드 블록 미표시"** — 오탐. `code-block.tsx`가 `<figure class="code-block">` +
|
||||||
|
`<pre role="region" tabindex=0>`을 렌더하고 CSS가 `overflow-x:auto`·`max-width:100%`를
|
||||||
|
준다. 정적 공개 문서에 CODE_BLOCK이 0건이라 발견하지 못한 것이며, Studio 편집기에
|
||||||
|
직접 넣어 확인하니 정상 렌더되고 페이지 가로 오버플로도 없었다. §4 PASS.
|
||||||
|
|
||||||
|
## 새로 발견한 결함
|
||||||
|
|
||||||
|
| # | 항목 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| **N-1** | **로그아웃할 방법이 없다** | `signOut` 포트와 `app-shell.tsx`의 세션 버튼(`action.signOut`="로그아웃")은 존재하지만, **TechLog는 자체 셸(`public-shell.tsx` + Studio 셸)을 쓰고 `AppShell`을 렌더하지 않는다.** 로그인 후 Public·Studio 어느 화면에서도 로그아웃 버튼이 없다. §1.4 "로그아웃", "로그아웃 후 보호된 데이터가 UI 상태에 남지 않는다" 미충족 |
|
||||||
|
| **N-2** | **중복 관계 생성이 방지되지 않는다** | 같은 대상을 두 번 연결해 저장해도 경고가 없다. 계약에 `uniqueItems` 제약이 없고(`relations: maxItems 20`뿐), `validate-working-copy.ts`도 slug 중복만 검사한다(`SLUG_DUPLICATE`). **백엔드를 구현해도 계약이 허용하므로 같은 결과가 난다.** §3.2 미충족 |
|
||||||
|
| **N-3** | **CLS 0.192 (기준 0.1)** | 원인 단일: `FOOTER.site-footer`가 t=538ms에 0.1922 이동. 나머지 shift는 0.0001. 세 라우트 모두 동일 값 → 앱 셸 마운트 시점의 footer 점프. §4 "주요 화면의 Layout Shift가 없다" 미충족 |
|
||||||
|
| **N-4** | **`navigation_path`(slug 조회)에 인덱스가 없다** | 20,000행 기준 `Seq Scan`, `Rows Removed by Filter: 19999`, **236ms**. `enable_seqscan=off`로도 인덱스를 못 쓴다 → 존재하지 않는다. 체크리스트가 명시한 "Slug 조회" 쿼리 패턴 |
|
||||||
|
| **N-5** | 검색 trgm 인덱스가 플래너에 선택되지 않음 | GIN trgm 인덱스는 존재하고 강제하면 3.96ms로 동작하나, 20k 규모에서 플래너가 Seq Scan(10.3ms)을 고른다. 운영 규모에서 재확인 필요 |
|
||||||
|
| **N-6** | `/api/v3/api-docs`가 500 | `/swagger-ui`·`/v3/api-docs`는 404로 미배포(정상)인데, path prefix가 붙은 `/api/v3/api-docs`만 500 INTERNAL_ERROR |
|
||||||
|
|
||||||
|
## 검증 결과 — 절별
|
||||||
|
|
||||||
|
### §1.2 Routing · §1.3 경계 · §2 기능 — 27/27 PASS
|
||||||
|
|
||||||
|
```
|
||||||
|
§1.2 존재하지 않는 Case / 잘못된 explore kind / 없는 프로젝트 / 없는 릴리스
|
||||||
|
→ 전부 "페이지를 찾을 수 없습니다."
|
||||||
|
§1.2 Not Found 화면, Back/Forward (/explore→/projects→back→forward) 정상
|
||||||
|
§1.3 Public 6개 화면에 studio 링크 0건, Draft 표식 0건
|
||||||
|
§2.1 탐색 목록 6건 · 중복 0 · 필터 적용 6→2건
|
||||||
|
§2.2 검색창 열림 / Focus 이동 / Overlay 겹침 없음 / 입력 중 과요청 0
|
||||||
|
결과 없음 UI / ESC 닫기 / 빈 검색어 정책 / 결과 클릭 → 상세 이동
|
||||||
|
§2.3 프로젝트 목록 2건 · 상세("Backend Skeleton") · 포함 문서 6건
|
||||||
|
§2.4 변경 기록 목록·상세, 연결 문서 7건, 시간순 정렬 일관
|
||||||
|
```
|
||||||
|
|
||||||
|
### §3 Studio — 20/22 PASS (mock 기준)
|
||||||
|
|
||||||
|
```
|
||||||
|
§3.1 새 문서(유형 4종) → 편집 진입 → 저장 → 상태 전달 PASS
|
||||||
|
§3.1 저장 버튼 3연타 → 문서 수 8→9 (증가 1) PASS ← 멱등성 실동작
|
||||||
|
§3.1 미저장 변경 이동 경고 [머무르기/변경 버리기/저장 후 이동] PASS
|
||||||
|
§3.1 머무르기 후 입력값 보존 PASS
|
||||||
|
§3.1 검증 화면("저장본 검증") / 게시 화면("게시 준비") PASS
|
||||||
|
§3.2 관계 추가·순서 이동·삭제, 대상 카탈로그 4건 PASS
|
||||||
|
§3.2 중복 관계 방지 FAIL (N-2)
|
||||||
|
§3.3 즉시 미리보기 렌더 / Public Preview 화면 PASS
|
||||||
|
§3.3 게시 기록 8건 · 게시 취소 버튼 3개 PASS
|
||||||
|
§20 저장 충돌(409) 사용자 안내 PASS
|
||||||
|
콘솔 오류 0건
|
||||||
|
```
|
||||||
|
|
||||||
|
문서 **삭제**는 계약에 오퍼레이션 자체가 없다(`deleteStudioAsset`만 존재). §3.1의 "삭제"는 설계 범위 밖.
|
||||||
|
|
||||||
|
### §4 UX/UI · §5 접근성 · §6 성능 — 15/18 PASS
|
||||||
|
|
||||||
|
```
|
||||||
|
§4 Layout Shift FAIL CLS=0.1924 (N-3)
|
||||||
|
§4 Header가 콘텐츠를 가리지 않음 PASS
|
||||||
|
§4 긴 제목(150자)/긴 본문/긴 URL PASS scrollWidth==clientWidth 1440
|
||||||
|
§4 코드 블록 (pre overflow-x:auto) PASS
|
||||||
|
§5 Modal Focus 이동 / role=dialog / Focus Trap / 닫은 뒤 복귀 PASS
|
||||||
|
§5 키보드 순회 19개 요소 · Focus 표시 전부 존재 PASS
|
||||||
|
§6 긴 문서 렌더링 296ms PASS
|
||||||
|
§6 이미지 lazy loading · width/height 명시 PASS
|
||||||
|
§6 동일 요청 중복 0 · 2초간 DOM 변경 0건(render loop 없음) PASS
|
||||||
|
§6 검색 21자 입력+반영 847ms PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### §14 데이터베이스
|
||||||
|
|
||||||
|
```
|
||||||
|
Constraint PK 33 · FK 36 · UNIQUE 18 · CHECK 89 · NOT NULL 267 · PK 없는 테이블 0 PASS
|
||||||
|
Index 실행계획 (20,000행 기준)
|
||||||
|
Public 목록(최신순) Index Scan idx_public_latest 0.113ms PASS
|
||||||
|
유형별 조회 Index Scan idx_public_type 0.129ms PASS
|
||||||
|
Topic별 조회 Bitmap Index Scan idx_public_topic 0.229ms PASS
|
||||||
|
검색(trgm) Seq Scan (인덱스 미선택) 10.3ms 주의 (N-5)
|
||||||
|
slug 조회 Seq Scan (인덱스 부재) 236ms FAIL (N-4)
|
||||||
|
```
|
||||||
|
|
||||||
|
`public_resource_projection`의 인덱스들이 `WHERE publication_state='ACTIVE' AND
|
||||||
|
visibility='PUBLIC'` 부분 인덱스로 정의되어 있다 — Public/Private 경계를 인덱스 수준에서
|
||||||
|
강제하는 좋은 설계다(§12를 구현할 때 그대로 활용 가능).
|
||||||
|
|
||||||
|
### §19 악용 방지 · §28 Swagger
|
||||||
|
|
||||||
|
```
|
||||||
|
pagination 최대 크기 (limit=1000) 422 REQUEST_VALIDATION_FAILED PASS
|
||||||
|
q 길이 제한 (500자) 422 REQUEST_VALIDATION_FAILED PASS
|
||||||
|
Rate Limit APP_RATE_LIMIT_ENABLED=false 미적용
|
||||||
|
대용량 Body 쓰기 엔드포인트 부재로 검증 불가
|
||||||
|
/swagger-ui, /v3/api-docs 404 (미배포) PASS
|
||||||
|
/api/v3/api-docs 500 주의 (N-6)
|
||||||
|
```
|
||||||
|
|
||||||
|
### §26 의존성 장애
|
||||||
|
|
||||||
|
```
|
||||||
|
PostgreSQL Down catalog 503 DB_UNAVAILABLE(retryable) 30s · readiness 503 DOWN
|
||||||
|
liveness 200 UP 유지 · 복구 후 27ms 정상 PASS
|
||||||
|
Keycloak Down JWKS 캐시로 기존 토큰 32ms/200 · 잘못된 서명 21ms/401
|
||||||
|
readiness 200 UP 유지(외부 IdP를 readiness에 걸지 않음)
|
||||||
|
복구 후 정상 PASS
|
||||||
|
Backend 단절 Public 화면 정상 유지(정적 소스) PASS
|
||||||
|
MinIO / Redis 해당 없음(미배선)
|
||||||
|
```
|
||||||
|
|
||||||
|
### §0 · §9 설정
|
||||||
|
|
||||||
|
```
|
||||||
|
src/.env 가 git에 커밋되어 있다 — 값은 local 프로파일용이지만 .gitignore에 .env가 없어
|
||||||
|
구조적으로 막혀 있지 않다. Redis HMAC은 secret://environment/... 간접 참조를 쓴다(좋은 패턴).
|
||||||
|
prod 5개 validator 실동작 확인 (정정 1)
|
||||||
|
show-sql=false · 로그에 토큰/쿠키/비밀번호 0건 · user= 는 가명화 해시
|
||||||
|
```
|
||||||
|
|
||||||
|
## 남은 것 — 로컬에서 불가능
|
||||||
|
|
||||||
|
| 절 | 이유 |
|
||||||
|
|---|---|
|
||||||
|
| §12 Public/Private 경계 | Public 엔드포인트·문서 엔드포인트 부재 |
|
||||||
|
| §15 N+1 / JPA Query | Tech Log에 JPA 리포지토리 0건 (catalog는 raw JDBC 단일 쿼리) |
|
||||||
|
| §16 Transaction | 쓰기 유스케이스 부재 |
|
||||||
|
| §17 파일/Object Storage | 업로드 엔드포인트·스토리지 배선 부재 |
|
||||||
|
| §18 HTTPS/HSTS/Redirect | TLS 종단 필요 |
|
||||||
|
| §22 Grafana·Loki 대시보드 | 관측 스택 필요 (수집 측 127개 메트릭은 확인 완료) |
|
||||||
|
| §24 Kubernetes | 매니페스트·오케스트레이터 부재 |
|
||||||
|
| §25 Ingress 라우팅 · X-Forwarded-* | 리버스 프록시 필요 |
|
||||||
|
| §27 Backup / Restore | 실제 볼륨·운영 DB 필요 |
|
||||||
|
| §30 Production Smoke Test | 운영 환경 부재 |
|
||||||
|
| §1.4 세션 만료 · 토큰 만료 후 프론트 동작 | demo 어댑터에 만료 개념이 없음 (외부 IdP 연동 필요) |
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Adapter Review — TechLog Asset Multipart Upload
|
||||||
|
|
||||||
|
> 검토 기준: `feature/techlog-backend-alignment` (2026-08-18, Task 7)
|
||||||
|
>
|
||||||
|
> 범위: `src/features/tech-log/adapters/http/asset-upload-transport.ts` 1개 파일과 그 wiring — `create-tech-log-feature-input.ts`, `installed-feature-adapters.ts`, `bootstrap/runtime-adapters.ts`의 `attachCredentials`/`techLogCsrf`. `src/adapters/**` 전수 리뷰([INVENTORY](./INVENTORY.md))와는 별도 트랙이다: 이 파일은 `src/features/tech-log/adapters/**` 아래에 있고, TechLog는 자체 canonical HTTP 계약을 갖는 product feature이지 템플릿의 범용 adapter 계층이 아니다.
|
||||||
|
|
||||||
|
## 결론
|
||||||
|
|
||||||
|
TechLog Studio는 canonical 계약상 19개 operation을 갖는다. 그중 18개는 `external-contract-runtime.ts`가 표현할 수 있는 `requestBody: "NONE" | "JSON"` 범위 안에 있고, 플랫폼의 V3 실행기(`http-execution-v3.ts`) · 저수준 client(`client.ts`) · `attachCredentials` credential seam을 그대로 통과한다. 나머지 1개, `uploadStudioAsset`(`POST /api/v1/studio/assets`)만 `multipart/form-data`를 요구한다. 이 요구를 플랫폼이 표현할 수 없으므로, 이 operation 하나만 별도의 좁은 transport(`asset-upload-transport.ts`)로 분리했다.
|
||||||
|
|
||||||
|
이 seam은 플랫폼을 대체하지 않는다. CSRF, `Idempotency-Key`, canonical 오류 코드 매핑, timeout, credentials는 동일한 provider·동일한 오류 taxonomy로 다시 구현해 대칭을 유지한다. 포기하는 것은 플랫폼이 대신 강제해 주던 부분 — 계약 실행기의 byte 상한, retry policy, V3 진단 계측 — 뿐이며 이는 아래에 명시적으로 기록한다. 조립 지점은 `createTechLogFeatureInstalledInput`이 무조건(HTTP/MOCK 무관) `createHttpStudioAssetGateway`를 구성하고, 그 안에 이 transport를 주입하는 한 곳뿐이다.
|
||||||
|
|
||||||
|
## 우회 대상과 이유
|
||||||
|
|
||||||
|
- `src/contracts/external-contract-runtime.ts`의 `requestBody` union은 `"NONE" | "JSON"` 두 값만 갖는다. `multipart/form-data`를 표현할 세 번째 값이 없다.
|
||||||
|
- `src/adapters/http/client.ts:719` 부근의 저수준 client는 본문이 있는 모든 request를 `JSON.stringify(input)`으로 직렬화해 고정 `content-type: application/json`으로 보낸다. `File`을 이 경로에 태우면 파일 바이트 대신 그 JSON 표현(빈 객체거나 오류)이 전송된다.
|
||||||
|
- 두 제약 모두 이번 task의 global constraint로 수정 금지 대상이다(`external-contract-runtime.ts`, `client.ts`, `http-execution-v3.ts`, `mutation-intent.ts`, 생성된 계약 산출물, `studio-gateway.ts` 포트). 계약 실행기 자체를 바꾸는 대신, `uploadStudioAsset` 한 operation만 포트 경계 뒤에서 다른 구현으로 우회한다.
|
||||||
|
|
||||||
|
## 우회 범위
|
||||||
|
|
||||||
|
canonical Studio API 전체는 19개 operation이다. `tech-log-studio-contract-contribution.ts`는 그중 18개만 등록한다 — `uploadStudioAsset`은 계약 실행기가 표현할 수 없으므로 애초에 그 파일에 없다(`studio-contract-contribution.test.ts`의 "declares every canonical operation except the multipart upload"가 18을 고정한다).
|
||||||
|
|
||||||
|
| 분류 | operation | 경로 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| JSON, 계약 실행기 경유 | `getStudioSession`, `getStudioDashboard`, `listStudioDocuments`, `createStudioDocument`, `getStudioDocument`, `saveStudioDocument`, `validateStudioDocument`, `getCurrentStudioPreview`, `createStudioPreview`, `publishStudioDocument`, `listStudioPublications`, `unpublishStudioPublication`, `getStudioPublicationSnapshot`, `listStudioCatalog`, `listStudioAssets`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset` (18개) | `contractHttp.execute()` → `client.ts` → `attachCredentials` |
|
||||||
|
| multipart, 플랫폼 우회 | `uploadStudioAsset` (1개) | `asset-upload-transport.ts`의 직접 `fetch()` |
|
||||||
|
|
||||||
|
18개는 `contractOperations.execute(operationId, input, { routeId, intent? })`를 통해 나가며, 그중 17개(`getStudioSession` 제외)에 `attachCredentials`가 매 요청 `x-csrf-token`을 싣는다 — `getStudioSession`은 그 토큰을 발급하는 operation 자신이라 CSRF 헤더를 요구하지 않는 `TECH_LOG_STUDIO_BOOTSTRAP` auth profile을 쓴다(자세한 내용은 아래 "유지되는 보증"의 CSRF 행). 1개(`uploadStudioAsset`)만 이 경로를 완전히 벗어나 `createAssetUploadTransport`가 직접 `fetch()`한다. `StudioAssetGateway.uploadAsset()`이 이 transport를 호출하는 유일한 지점이며, 포트 시그니처(`Promise<Asset>`)는 나머지 4개 asset operation과 동일해 호출자는 어느 경로인지 알 필요가 없다.
|
||||||
|
|
||||||
|
## 유지되는 보증
|
||||||
|
|
||||||
|
플랫폼이 18개 JSON operation에 자동으로 제공하는 것을, 이 transport는 같은 provider·같은 값으로 손으로 다시 만든다.
|
||||||
|
|
||||||
|
| 보증 | JSON 경로 | multipart 경로 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| CSRF | `attachCredentials`가 `techLogCsrf.token()`/`headerName()`으로 얻은 값을 그 이름 그대로 요청 헤더에 싣는다 | `StudioAssetGateway.uploadAsset()`이 **같은** `techLogCsrf` provider에서 `token()`/`headerName()`을 읽어 transport에 넘긴다 — provider가 composition root에 하나뿐이므로 세션당 토큰도 하나다. `getStudioSession` 자신은 이 provider를 거치지 않는 `TECH_LOG_STUDIO_BOOTSTRAP` auth profile을 쓴다: 그 provider가 토큰을 얻으려고 호출하는 operation이 같은 provider의 토큰을 요구하면 순환이 되기 때문이다(`docs`가 아니라 코드로 고정: `studio-csrf-composition.test.ts`) |
|
||||||
|
| Idempotency-Key | 실행 intent(`mutationIntent()`)에서 나와 client가 헤더로 싣는다 | 호출자(`uploadAsset` options)의 `idempotencyKey`를 gateway가 그대로 헤더로 전달한다 |
|
||||||
|
| canonical 오류 코드 매핑 | `toStudioGatewayError()`가 `STUDIO_ERROR_CODES`에 있는 `problem.code`만 `StudioGatewayError`로 승격하고, 계약 밖 코드는 `STUDIO_UNAVAILABLE`로 접는다 | transport가 동일한 `STUDIO_ERROR_CODES` 집합을 재사용해 같은 규칙으로 매핑한다. 서버가 `PAYLOAD_TOO_LARGE`/`UNSUPPORTED_MEDIA_TYPE`처럼 이 목록에 있는 코드를 보내면 그대로 `StudioGatewayError`가 되고, 계약에 없는 코드나 파싱 불가능한 본문은 도메인 코드를 지어내지 않고 `STUDIO_UNAVAILABLE`로 접는다 |
|
||||||
|
| timeout | 계약의 `requestDeadlineCeilingMs` | `AbortSignal.timeout(deps.timeoutMs)`를 호출자 signal과 `AbortSignal.any()`로 합성한다 |
|
||||||
|
| credentials | `TECH_LOG_STUDIO_SESSION` 프로필의 `credentials: "include"` | `fetch()` 호출에 동일하게 `credentials: "include"`를 명시한다 |
|
||||||
|
|
||||||
|
## 포기하는 보증
|
||||||
|
|
||||||
|
- **byte 상한**: 계약 실행기의 bounded body reader/writer가 응답 크기를 강제하는 것과 달리, 이 transport의 요청 body(`FormData`)와 응답 JSON 파싱에는 별도 상한이 없다. 서버가 `413 PAYLOAD_TOO_LARGE`로 거절하는 것에 의존한다.
|
||||||
|
- **retry policy**: 계약 실행기의 `retry-policy.ts`는 이 operation에 적용되지 않는다. `uploadStudioAsset`은 애초에 계약에서 `retrySemantics`를 선언할 수 없는 경로 밖에 있으므로, 재시도는 호출자(향후 Task 11의 Asset Library UI)가 명시적으로 다시 `uploadAsset()`을 호출하는 형태로만 존재한다.
|
||||||
|
- **V3 진단 계측**: `createHttpObservationProjector`가 만드는 `api.request.*` diagnostics/telemetry 이벤트는 `contractHttp.execute()` 내부에서만 발생한다. 이 transport는 그 관찰 경계 밖에서 직접 `fetch()`하므로 업로드 성공/실패는 diagnostics 스트림에 나타나지 않는다. `routeId: "TECH_LOG_STUDIO_ASSETS"`는 나머지 4개 asset JSON operation에는 여전히 붙지만, `uploadStudioAsset` 자체에는 대응하는 diagnostics 레코드가 없다.
|
||||||
|
|
||||||
|
이 세 항목 모두 이번 task 범위에서 새로 만들지 않는다 — 다시 만들려면 플랫폼과 동일한 bounded reader/retry/observation을 복제해야 하고, 그것은 계약 실행기를 다시 짓는 것과 다르지 않다. 대신 아래 교체 계획으로 닫는다.
|
||||||
|
|
||||||
|
## `ROUTE_ID` 검토 (Task 6 리뷰 인계 항목)
|
||||||
|
|
||||||
|
`http-studio-asset-gateway.ts`의 `const ROUTE_ID = "TECH_LOG_STUDIO_ASSETS"`는 diagnostics/telemetry 버킷을 나누는 low-cardinality routing 메타데이터이지, 등록된 route 경로가 아니다. 이제 gateway가 실제로 배선되어 나머지 4개 asset JSON operation(`listAssets`, `getAsset`, `updateAssetMetadata`, `deleteAsset`)이 이 값으로 나가는 시점에서 다시 확인한 결과, **그대로 유지한다.** 문서/편집 operation의 `TECH_LOG_STUDIO`와 별개 값을 쓰는 것은 두 가지를 갖는다.
|
||||||
|
|
||||||
|
1. Asset 수명주기(업로드·목록·삭제)는 문서 편집과 실패 특성이 다르다 — 예를 들어 `PAYLOAD_TOO_LARGE`/`UNSUPPORTED_MEDIA_TYPE`/`ASSET_QUARANTINED`는 asset 쪽에만 있다. 별도 routeId는 이 실패를 diagnostics에서 문서 편집 트래픽과 섞지 않는다.
|
||||||
|
2. `uploadStudioAsset` 자체는 계약 실행기를 우회해 이 routeId를 진단에 보고하지 않지만, 같은 이름을 4개 JSON operation에 유지해 두면 향후 업로드가 presigned/resumable로 옮겨가거나 플랫폼에 MULTIPART 모드가 생겨 계약 경로로 복귀할 때, 같은 routeId 아래 asset 트래픽 전체가 이미 일관되게 모여 있다.
|
||||||
|
|
||||||
|
값을 바꿀 이유(예: 기존 registry 충돌, 명명 규칙 위반)는 없었다.
|
||||||
|
|
||||||
|
## 교체 계획
|
||||||
|
|
||||||
|
1. **presigned/resumable 업로드로 이전.** `src/adapters/browser-transfer/`에 이미 presigned capability와 resumable checkpoint 인프라가 있다(별도 리뷰: [04 — Browser transfer](./04-browser-transfer.md)). Studio asset 업로드가 그쪽으로 옮겨가면, 이 transport는 presigned URL 발급을 위한 작은 JSON operation(계약 실행기 경유 가능)과 실제 바이트 전송을 위한 presigned executor 호출로 나뉜다. `POST /api/v1/studio/assets`의 multipart 자체가 없어진다.
|
||||||
|
2. **플랫폼에 `requestBody: "MULTIPART"` 모드가 생기는 경우.** `external-contract-runtime.ts`와 `client.ts`가 `FormData` 본문을 표현할 수 있게 확장되면, `uploadStudioAsset`을 이미 등록된 다른 18개 operation과 함께 `tech-log-studio-contract-contribution.ts`에 등록하고 `createHttpStudioAssetGateway`의 `upload` 의존성을 제거한다. `StudioAssetGateway` 포트 시그니처(`uploadAsset(form, options): Promise<Asset>`)는 바뀌지 않는다 — 교체는 이 파일과 `create-tech-log-feature-input.ts`의 배선 한 줄에서 끝난다.
|
||||||
|
|
||||||
|
두 경로 모두 `StudioAssetUploadTransport`/`StudioAssetGateway` 포트 경계 뒤에서 일어나므로, presentation 계층(Task 11의 Asset Library UI)은 재작성하지 않는다.
|
||||||
|
|
||||||
|
## MOCK 의존성 리비전은 계약의 요구가 아니라 mock의 구현이다
|
||||||
|
|
||||||
|
`createMockStudioGateway`의 기본 `dependencyRevision.current()`가 무엇을 관찰하는지 — 그리고 그것이 **계약이 요구하는 계산이 아니라는 점** — 을 여기에 남긴다. 나중에 mock의 구현을 계약의 요구로 오독하지 않기 위해서다.
|
||||||
|
|
||||||
|
### 실제 백엔드
|
||||||
|
|
||||||
|
`DependencyRevision`(`studio-api.openapi.yaml` / `generated.ts`)은 값의 **형식**만 계약이다: 불투명한 문자열. 계약이 요구하는 것은 값이 아니라 규칙 하나뿐이다 — *검증에 사용한 dependency set을 publish 시 다시 계산해 값이 다르면 `VALIDATION_STALE`로 거절한다.* 무엇을 dependency set에 넣을지(Topic/Project 존재, relation target 상태, Asset READY/QUARANTINED 상태, slug/route ownership, catalog revision, 필요 시 renderer/content-format version), 그리고 그것을 어떻게 정규화·hash할지는 **서버가 스스로 정한다.** 프론트엔드는 이 값을 생성하지도, 해석하지도, 비교하지도 않는다. `ValidationReport.dependencyRevision`을 받아 그대로 되돌려 보내고, 서버가 내린 `VALIDATION_STALE` 판정을 표시할 뿐이다. HTTP gateway(`http-studio-gateway.ts`)에는 리비전을 계산하는 코드가 없다 — 있어서도 안 된다.
|
||||||
|
|
||||||
|
### MOCK
|
||||||
|
|
||||||
|
MOCK `studioSource`에는 그 서버가 없으므로, mock이 같은 규칙을 스스로 만족시켜야 한다. 기본 리비전은 `dependency-revision.ts`의 `mockDependencyRevision`이 계산한다.
|
||||||
|
|
||||||
|
- **catalog 성분**: `MOCK_CATALOG_REVISION`(`"catalog-2026-08-14"`) 상수. `createMockStudioState`가 고정 fixture catalog 하나를 싣고 변경하지 않으므로 catalog의 기여는 실제로 상수다. `fixtures.ts`의 seed validation/preview도 같은 정의를 import해 쓴다 — 두 값이 갈라지면 seed된 문서가 전부 조용히 stale이 된다.
|
||||||
|
- **asset 성분**: Asset store를 정규화해 만든 128비트 digest. 각 Asset을 **투영(projection)** 으로 줄이고(`id`, `assetKey`, `managementStatus`, `publicPath`, `updatedAt`, `decorative`, `altText`, `mediaType`, `width`, `height`), `stableStringify`로 정규 문자열을 만든 뒤 정렬해 접는다. 따라서 `Map` 삽입 순서와 무관하게 같은 논리적 Asset 집합은 항상 같은 리비전을 낸다 — 이 mock의 재현성은 저장소 전체 테스트가 의존하는 성질이다.
|
||||||
|
- 빈 store는 성분을 더하지 않아 `MOCK_CATALOG_REVISION` 그대로다. seed fixture가 Asset이 없는 세계에서 만들어졌고 그 문자열을 그대로 싣기 때문이다.
|
||||||
|
|
||||||
|
레코드 전체가 아니라 투영을 hash하는 이유: `usageCount`는 그 Asset을 참조하는 문서 수라 실제 백엔드였다면 **아무 문서나 publish할 때마다** 다른 저자의 진행 중인 검증이 전부 무효가 된다 — 검증기도 렌더러도 읽지 않는 필드인데도. `version`은 이 mock에서 `updatedAt`과 함께 움직여 신호를 더하지 않고, `kind`·`originalFilename`·`byteSize`·`createdAt`은 검증에도 render model에도 도달하지 않는다.
|
||||||
|
|
||||||
|
### 이 기본값이 닫는 구멍
|
||||||
|
|
||||||
|
기본 리비전이 리터럴 상수였을 때, `createStudioPreview`/`publishStudioDocument`의 staleness guard는 Asset store를 전혀 관찰하지 못했다. 그래서 **validate와 preview 사이의 Asset 변경이 guard에게 보이지 않았다.** 구체적으로: 어떤 evidence key의 Asset이 `decorative: true`뿐이면 `alt=""`인 directive는 정당하게 VALID다(장식용 이미지는 대체 텍스트가 없어도 된다). 그 사이에 같은 key에 `decorative: false`인 더 새로운 Asset이 도착하면, `createStudioPreview`는 성공하고 figure는 `decorative: false, alt: ""`로 해석되며 `publishDocument`가 그 render model을 그대로 snapshot한다. **의미 있는 이미지가 접근 가능한 이름 없이, 검증은 깨끗한 채로, 아무도 오류를 보고하지 않은 채 공개된다.** 이제 그 변경이 리비전을 움직여 guard가 `VALIDATION_STALE`을 내고, 저자가 재검증하면 `EVIDENCE_ALT_REQUIRED`로 진짜 문제를 듣는다.
|
||||||
|
|
||||||
|
수정은 `findResolvableAsset`이 아니라 리비전에 있다. `findResolvableAsset`은 *한 시점의* 술어이고 그 자체로는 옳다 — 두 시점 사이의 변화를 보는 것은 리비전의 일이다.
|
||||||
|
|
||||||
|
**주의**: 위 필드 목록은 이 mock이 스스로 무엇을 읽는지에 대한 서술이지, 서버가 무엇을 dependency set에 넣어야 하는지에 대한 요구가 아니다. 서버는 프론트엔드가 볼 수 없는 것(예: relation target의 게시 상태, route ownership)까지 포함할 수 있고 그래야 한다. 이 mock을 계약의 참조 구현으로 삼지 말 것.
|
||||||
|
|
||||||
|
고정 테스트: `tests/features/tech-log/mock-dependency-revision.test.ts`(보고된 시나리오 end-to-end, 순서 무관 결정성, 무변경 authoring loop 안정성).
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
```
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/asset-upload-transport.test.ts
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-asset-gateway.test.ts
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/runtime-composition.test.ts
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-csrf-composition.test.ts
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-session-csrf.test.ts
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/mock-dependency-revision.test.ts
|
||||||
|
corepack pnpm check:types
|
||||||
|
corepack pnpm test:tech-log
|
||||||
|
```
|
||||||
|
|
||||||
|
`studio-csrf-composition.test.ts`는 fix round 1에서 추가됐다 — 실 `createContractHttpExecutor` · `createCsrfTokenProvider` · `attachStudioSessionCredentials`를 composition root와 같은 방식으로 조립해 `getStudioSession`이 정확히 한 번만 나가고 그 토큰이 JSON operation과 업로드 양쪽에 모두 실리는지 검증한다. `studio-session-csrf.test.ts`는 provider의 재진입 가드를 단독으로 고정한다.
|
||||||
|
|
||||||
|
fix round 2에서 같은 파일에 "JSON operation의 403이 캐시된 토큰을 무효화해 다음 operation이 세션을 다시 가져온다"는 테스트를 더했다 — `invalidateTechLogCsrfOnOutcome`(`studio-session-credentials.ts`)를 composition root와 동일하게 호출한다. `asset-upload-transport.test.ts`에는 업로드 transport가 계약 밖 상태 코드의 실제 HTTP status를 그대로 통과시키는지, 그리고 계약 밖 401 본문도 게이트웨이의 토큰 무효화를 실제로 촉발하는지 검증하는 테스트를 더했다. `studio-contract-contribution.test.ts`에는 `getStudioSession`이 bootstrap profile의 유일한 사용자인지와 `assertExactlyOneTechLogStudioBootstrapOperation`이 0개·2개 위반을 거절하는지 고정하는 테스트를 더했다.
|
||||||
|
|
||||||
|
정확한 실행 결과는 `.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-7-report.md`에 있다.
|
||||||
@@ -69,64 +69,71 @@
|
|||||||
| 59 | `src/adapters/platform/abortable-operation.ts` | [Network/state](./01-network-and-state.md) |
|
| 59 | `src/adapters/platform/abortable-operation.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 60 | `src/adapters/platform/browser-lifecycle.ts` | [Network/state](./01-network-and-state.md) |
|
| 60 | `src/adapters/platform/browser-lifecycle.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 61 | `src/adapters/platform/browser-mutation-intent-factory.ts` | [Network/state](./01-network-and-state.md) |
|
| 61 | `src/adapters/platform/browser-mutation-intent-factory.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 62 | `src/adapters/platform/system-clock.ts` | [Network/state](./01-network-and-state.md) |
|
| 62 | `src/adapters/platform/bounded-capacity.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 63 | `src/adapters/query-cache/conditional-validator-store.ts` | [Network/state](./01-network-and-state.md) |
|
| 63 | `src/adapters/platform/system-clock.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 64 | `src/adapters/query-cache/cursor-pagination-runtime.ts` | [Network/state](./01-network-and-state.md) |
|
| 64 | `src/adapters/query-cache/conditional-validator-store.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 65 | `src/adapters/query-cache/server-state-scope-runtime.ts` | [Network/state](./01-network-and-state.md) |
|
| 65 | `src/adapters/query-cache/cursor-pagination-runtime.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 66 | `src/adapters/query-cache/tanstack-cache-coordinator.ts` | [Network/state](./01-network-and-state.md) |
|
| 66 | `src/adapters/query-cache/server-state-scope-runtime.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 67 | `src/adapters/query-cache/tanstack-query-cache.ts` | [Network/state](./01-network-and-state.md) |
|
| 67 | `src/adapters/query-cache/tanstack-cache-coordinator.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 68 | `src/adapters/realtime/event-codec.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 68 | `src/adapters/query-cache/tanstack-query-cache.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 69 | `src/adapters/realtime/event-consumer.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 69 | `src/adapters/realtime/event-codec.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 70 | `src/adapters/realtime/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 70 | `src/adapters/realtime/event-consumer.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 71 | `src/adapters/realtime/json-member-scanner.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 71 | `src/adapters/realtime/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 72 | `src/adapters/realtime/live-poll-handoff-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 72 | `src/adapters/realtime/json-member-scanner.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 73 | `src/adapters/realtime/polling/bounded-poll-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 73 | `src/adapters/realtime/live-poll-handoff-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 74 | `src/adapters/realtime/polling/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 74 | `src/adapters/realtime/polling/bounded-poll-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 75 | `src/adapters/realtime/reconnect-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 75 | `src/adapters/realtime/polling/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 76 | `src/adapters/realtime/reconnect-policy.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 76 | `src/adapters/realtime/reconnect-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 77 | `src/adapters/realtime/result.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 77 | `src/adapters/realtime/reconnect-policy.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 78 | `src/adapters/realtime/sse/fetch-sse-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 78 | `src/adapters/realtime/result.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 79 | `src/adapters/realtime/sse/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 79 | `src/adapters/realtime/sse/fetch-sse-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 80 | `src/adapters/realtime/sse/sse-parser.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 80 | `src/adapters/realtime/sse/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 81 | `src/adapters/realtime/stream-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 81 | `src/adapters/realtime/sse/sse-parser.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 82 | `src/adapters/realtime/websocket/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 82 | `src/adapters/realtime/stream-coordinator.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 83 | `src/adapters/realtime/websocket/websocket-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 83 | `src/adapters/realtime/websocket/index.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 84 | `src/adapters/realtime/websocket/websocket-protocol.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
| 84 | `src/adapters/realtime/websocket/websocket-connection.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 85 | `src/adapters/service-worker/service-worker-entry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 85 | `src/adapters/realtime/websocket/websocket-protocol.ts` | [Realtime/RPC](./02-realtime-and-browser-rpc.md) |
|
||||||
| 86 | `src/adapters/service-worker/service-worker-lifecycle.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 86 | `src/adapters/service-worker/service-worker-entry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 87 | `src/adapters/service-worker/service-worker-page-controller.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 87 | `src/adapters/service-worker/service-worker-lifecycle.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 88 | `src/adapters/service-worker/service-worker-protocol.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 88 | `src/adapters/service-worker/service-worker-page-controller.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 89 | `src/adapters/service-worker/service-worker-removal.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 89 | `src/adapters/service-worker/service-worker-protocol.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 90 | `src/adapters/service-worker/service-worker-static-assets.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 90 | `src/adapters/service-worker/service-worker-removal.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 91 | `src/adapters/storage/browser-storage-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 91 | `src/adapters/service-worker/service-worker-static-assets.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 92 | `src/adapters/storage/browser-storage-codec.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 92 | `src/adapters/storage/browser-storage-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 93 | `src/adapters/storage/indexeddb/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 93 | `src/adapters/storage/browser-storage-codec.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 94 | `src/adapters/storage/indexeddb/indexeddb-failure.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 94 | `src/adapters/storage/indexeddb/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 95 | `src/adapters/storage/indexeddb/indexeddb-governance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 95 | `src/adapters/storage/indexeddb/indexeddb-failure.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 96 | `src/adapters/storage/indexeddb/indexeddb-maintenance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 96 | `src/adapters/storage/indexeddb/indexeddb-governance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 97 | `src/adapters/storage/indexeddb/indexeddb-migrations.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 97 | `src/adapters/storage/indexeddb/indexeddb-maintenance.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 98 | `src/adapters/storage/indexeddb/indexeddb-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 98 | `src/adapters/storage/indexeddb/indexeddb-migrations.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 99 | `src/adapters/storage/indexeddb/indexeddb-types.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 99 | `src/adapters/storage/indexeddb/indexeddb-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 100 | `src/adapters/storage/opfs/browser-opfs-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 100 | `src/adapters/storage/indexeddb/indexeddb-types.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 101 | `src/adapters/storage/opfs/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 101 | `src/adapters/storage/opfs/browser-opfs-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 102 | `src/adapters/storage/opfs/indexeddb-opfs-journal.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 102 | `src/adapters/storage/opfs/index.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 103 | `src/adapters/storage/opfs/opfs-byte-store-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 103 | `src/adapters/storage/opfs/indexeddb-opfs-journal.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 104 | `src/adapters/storage/opfs/opfs-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 104 | `src/adapters/storage/opfs/opfs-byte-store-adapter.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 105 | `src/adapters/storage/opfs/opfs-worker-client.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 105 | `src/adapters/storage/opfs/opfs-policy.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 106 | `src/adapters/storage/opfs/opfs-worker-protocol.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 106 | `src/adapters/storage/opfs/opfs-worker-client.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 107 | `src/adapters/storage/opfs/opfs-worker-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
| 107 | `src/adapters/storage/opfs/opfs-worker-protocol.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 108 | `src/adapters/telemetry/best-effort-telemetry.ts` | [Network/state](./01-network-and-state.md) |
|
| 108 | `src/adapters/storage/opfs/opfs-worker-runtime.ts` | [Storage/files](./03-storage-and-browser-files.md) |
|
||||||
| 109 | `src/adapters/web-push/inbound/notification-click-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 109 | `src/adapters/telemetry/best-effort-telemetry.ts` | [Network/state](./01-network-and-state.md) |
|
||||||
| 110 | `src/adapters/web-push/inbound/push-event-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 110 | `src/adapters/web-push/inbound/notification-click-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 111 | `src/adapters/web-push/index.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 111 | `src/adapters/web-push/inbound/push-event-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 112 | `src/adapters/web-push/notification-registry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 112 | `src/adapters/web-push/index.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 113 | `src/adapters/web-push/push-association-fence-store.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 113 | `src/adapters/web-push/notification-registry.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 114 | `src/adapters/web-push/push-codec.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 114 | `src/adapters/web-push/push-association-fence-store.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 115 | `src/adapters/web-push/push-registration-gateway.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 115 | `src/adapters/web-push/push-codec.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 116 | `src/adapters/web-push/push-subscription-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 116 | `src/adapters/web-push/push-registration-gateway.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 117 | `src/adapters/web-push/runtime-support.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 117 | `src/adapters/web-push/push-subscription-adapter.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 118 | `src/adapters/web-push/service-worker-runtime.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 118 | `src/adapters/web-push/runtime-support.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
| 119 | `src/adapters/web-push/service-worker-scope-host.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
| 119 | `src/adapters/web-push/service-worker-runtime.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
|
| 120 | `src/adapters/web-push/service-worker-scope-host.ts` | [Worker/push](./05-service-worker-and-web-push.md) |
|
||||||
|
|
||||||
합계: **119/119**. 새 adapter 파일이 추가되면 이 ledger와 해당 상세 리뷰 inventory를 같은 변경에서 갱신한다.
|
합계: **120/120**. 새 adapter 파일이 추가되면 이 ledger와 해당 상세 리뷰 inventory를 같은 변경에서 갱신한다.
|
||||||
|
|
||||||
|
## Feature-scoped adapter 리뷰 (`src/adapters/**` 밖)
|
||||||
|
|
||||||
|
이 표는 `corepack pnpm check:adapter-inventory`가 `git ls-files src/adapters`와 대조하는 목록이라 `src/features/**/adapters/**` 파일은 포함하지 않는다. TechLog는 자체 canonical HTTP 계약을 갖는 product feature이며 그 adapter는 별도 트랙으로 검토한다.
|
||||||
|
|
||||||
|
- `src/features/tech-log/adapters/http/asset-upload-transport.ts` — [06 — TechLog asset multipart upload](./06-tech-log-asset-upload.md)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,812 @@
|
|||||||
|
# TechLog Public and Studio UI Migration Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Move every Public and Studio screen from `/home/donghyeon/workspace/techlog-studio-frontend` into this Vite frontend while preserving the source DOM, text, assets, CSS, responsive behavior, accessibility semantics, and user-visible interactions exactly.
|
||||||
|
|
||||||
|
**Architecture:** Keep the target repository's Vite, React Router, application API, diagnostics, route lifecycle, and adapter boundaries. Install one `tech-log` feature whose presentation is split into nested `PUBLIC` and `STUDIO` layout groups; expose immutable Public queries and a factory-scoped `StudioGateway` through application feature inputs; use source-equivalent static Public data and the deterministic mock Studio adapter. The Public renderer is shared with Studio previews so one content-format implementation produces both views.
|
||||||
|
|
||||||
|
**Tech Stack:** Node 24, pnpm 11, TypeScript 7, React 19, React Router 7, Vite 8, Vitest 4, Testing Library, Playwright, Axe, Tailwind 4, Pretendard 1.3.9, IBM Plex Mono 5.3.0, Unified 11.0.5, Remark Parse 11.0.0, Remark GFM 4.0.1, Remark Directive 4.0.0.
|
||||||
|
|
||||||
|
## Source of truth and delivery branch
|
||||||
|
|
||||||
|
- Approved design: [`docs/superpowers/specs/2026-08-15-techlog-ui-migration-design.md`](../specs/2026-08-15-techlog-ui-migration-design.md).
|
||||||
|
- Visual/behavior source: `/home/donghyeon/workspace/techlog-studio-frontend` at the locally inspected revision used when Task 1 records the baseline.
|
||||||
|
- Delivery flow is already initialized with `main` as production and `develop` as integration. Execute every task and commit on `feature/techlog-ui-migration`; finish through `git flow feature finish techlog-ui-migration` only after Task 14 is green and the user authorizes integration.
|
||||||
|
- Do not push, merge, finish the feature, or delete branches as part of an individual task.
|
||||||
|
|
||||||
|
## Non-negotiable constraints
|
||||||
|
|
||||||
|
- Visual parity means no redesign: preserve source element order, nesting, class names, visible copy, labels, ARIA, SVGs, typography, spacing, color values, borders, shadows, animation, and responsive rules.
|
||||||
|
- Copy `app/globals.css`, `app/studio.css`, `app/studio-editor.css`, `components/studio/workflow.module.css`, and `components/studio/publication-flow.module.css` without value or selector changes. Remove only the source `@import "tailwindcss"` because target `theme.css` already owns that import.
|
||||||
|
- Preserve the source breakpoints, including 1179, 1050, 1024, 980, 900, 767, and 420 pixels. Do not substitute the target generic design-system components where doing so changes source markup or styles.
|
||||||
|
- Preserve `public/favicon.svg` and `public/media/fetch-strategy-boundary.svg` byte-for-byte. Add only assets demonstrably referenced by a migrated screen.
|
||||||
|
- The only permitted framework translations are: `next/link` to React Router `Link` with `href` renamed to `to`; the Studio `공개 사이트 보기` boundary remains a plain `<a href="/">` so it performs the source-intended full reload/session reset; `usePathname`/Next navigation to `useLocation`/`useNavigate`; `next/image` to an `img` that preserves classes, dimensions, alt text, loading intent, and wrapper structure; server page inputs to validated route params/search plus injected feature queries.
|
||||||
|
- No Next.js, Vinext, Cloudflare, server actions, or direct presentation-to-adapter imports enter the target.
|
||||||
|
- Public behavior stays deterministic and static. Studio behavior stays session-scoped and deterministic through one provider-owned mock gateway instance. Reload resets Studio state; remounting child routes does not.
|
||||||
|
- Studio authentication is deliberately deferred. Studio routes use `access: "public"` in this migration so the current shell is reachable, while `layoutGroup: "STUDIO"` and the application feature input remain the future auth seam. Do not add fake sign-in UI.
|
||||||
|
- Unknown public content renders the Public not-found experience; unknown `/studio/*` and unknown document/publication IDs render Studio not-found inside the Studio shell. Param codecs accept non-empty strings and do not reject unknown IDs before gateway lookup.
|
||||||
|
- Every production change follows red → green → focused regression → commit. Never update a test merely to legitimize a visual or behavioral difference.
|
||||||
|
- The pre-existing `tests/unit/ci-artifact-contract.test.ts` child-process failures caused by the restricted environment are baseline infrastructure evidence, not permission to add failures. Record exact commands/counts; focused TechLog tests and static gates must pass.
|
||||||
|
|
||||||
|
## Fixed contracts
|
||||||
|
|
||||||
|
### Route grouping
|
||||||
|
|
||||||
|
Add this field to every route definition:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type RouteLayoutGroup = "PUBLIC" | "STUDIO";
|
||||||
|
|
||||||
|
export type RouteDefinition = Readonly<{
|
||||||
|
routeId: string;
|
||||||
|
path: string;
|
||||||
|
layoutGroup: RouteLayoutGroup;
|
||||||
|
paramsSchema: string | null;
|
||||||
|
searchSchema: string | null;
|
||||||
|
access: "public" | "session-required";
|
||||||
|
loadingSurface: string;
|
||||||
|
errorSurface: string;
|
||||||
|
chunkId: string;
|
||||||
|
title: string;
|
||||||
|
navigationLabel: string | null;
|
||||||
|
navigationOrder: number | null;
|
||||||
|
}>;
|
||||||
|
```
|
||||||
|
|
||||||
|
The installed TechLog route IDs and paths are fixed:
|
||||||
|
|
||||||
|
| Layout | Route ID | Path |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| PUBLIC | `TECH_LOG_HOME` | `/` |
|
||||||
|
| PUBLIC | `TECH_LOG_EXPLORE` | `/explore` |
|
||||||
|
| PUBLIC | `TECH_LOG_EXPLORE_KIND` | `/explore/:kind` |
|
||||||
|
| PUBLIC | `TECH_LOG_CASE` | `/cases/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_REFERENCE` | `/references/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_QUESTION` | `/questions/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_TOPIC` | `/topics/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECTS` | `/projects` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT` | `/projects/:slug` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT_RECORDS` | `/projects/:slug/records` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT_DECISIONS` | `/projects/:slug/decisions` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROJECT_ACTIVITY` | `/projects/:slug/activity` |
|
||||||
|
| PUBLIC | `TECH_LOG_RELEASES` | `/releases` |
|
||||||
|
| PUBLIC | `TECH_LOG_RELEASE` | `/releases/:version` |
|
||||||
|
| PUBLIC | `TECH_LOG_PROFILE` | `/profile` |
|
||||||
|
| PUBLIC | `TECH_LOG_SEARCH` | `/search` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_HOME` | `/studio` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENTS` | `/studio/documents` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_NEW` | `/studio/documents/new` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_EDIT` | `/studio/documents/:id/edit` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_VALIDATION` | `/studio/documents/:id/validation` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PREVIEW` | `/studio/documents/:id/preview` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_DOCUMENT_PUBLISH` | `/studio/documents/:id/publish` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_PUBLICATIONS` | `/studio/publications` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_PUBLICATION_PREVIEW` | `/studio/publications/:publicationEventId/preview` |
|
||||||
|
| STUDIO | `TECH_LOG_STUDIO_NOT_FOUND` | `/studio/*` |
|
||||||
|
| PUBLIC | `NOT_FOUND` | `*` |
|
||||||
|
|
||||||
|
Route schema IDs are also fixed. Parameter codecs are `TechLogExploreKindParams` (`kind` non-empty; page-level lookup recognizes only `cases | references | questions`), `TechLogSlugParams` (`slug` non-empty), `TechLogVersionParams` (`version` non-empty), `TechLogDocumentIdParams` (`id` non-empty), `TechLogPublicationEventIdParams` (`publicationEventId` non-empty), `TechLogStudioSplat`, and the existing `NotFoundSplat`. Search codecs are `TechLogHomeSearch` (`focus`, `state`), `TechLogExploreSearch` (`type`, `topic`, `project`), `TechLogExploreKindSearch` (`topic`, `project`), `TechLogSearchQuery` (`q`), and `TechLogCaseStateSearch` (`state`); all fields are optional single strings and canonicalization keeps the first repeated value, trims selections where the source does, and drops unknown fields. Other routes use `none`.
|
||||||
|
|
||||||
|
Each TechLog route uses a unique kebab-cased chunk/module identity derived from its ID: lower-case the route ID, replace underscores with hyphens, and prefix `route-` (for example, `TECH_LOG_STUDIO_DOCUMENT_EDIT` → `route-tech-log-studio-document-edit`). The global `NOT_FOUND` keeps `route-not-found`. This same identity must appear in the runtime contract, lazy import map, release manifest, diagnostics, and chunk-recovery tests.
|
||||||
|
|
||||||
|
### Feature input and gateway
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const TECH_LOG_FEATURE_ID = "tech-log" as const;
|
||||||
|
|
||||||
|
export type TechLogFeatureInput = Readonly<{
|
||||||
|
publicContent: PublicContentQueries;
|
||||||
|
createStudioGateway(): StudioGateway;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
declare module "../../../application/ports/in/application-api.ts" {
|
||||||
|
interface ApplicationFeatureInputs {
|
||||||
|
"tech-log": TechLogFeatureInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StudioGateway {
|
||||||
|
getDashboard(options?: RequestOptions): Promise<StudioDashboard>;
|
||||||
|
listDocuments(query: ListDocumentsQuery, options?: RequestOptions): Promise<DocumentPage>;
|
||||||
|
createDocument(input: CreateDocumentInput, options: IdempotentOptions): Promise<WorkingCopy>;
|
||||||
|
getDocument(documentId: string, options?: RequestOptions): Promise<WorkingCopyDetail>;
|
||||||
|
saveDocument(documentId: string, command: SaveDocumentCommand, options: IdempotentOptions): Promise<WorkingCopyDetail>;
|
||||||
|
validateDocument(documentId: string, command: ValidateDocumentCommand, options: IdempotentOptions): Promise<ValidationReport>;
|
||||||
|
createPreview(documentId: string, command: CreatePreviewCommand, options: IdempotentOptions): Promise<PublicPreview>;
|
||||||
|
getCurrentPreview(documentId: string, options?: RequestOptions): Promise<PreviewDetail>;
|
||||||
|
publishDocument(documentId: string, command: PublishDocumentCommand, options: IdempotentOptions): Promise<PublishResult>;
|
||||||
|
unpublishPublication(publicationId: string, command: UnpublishCommand, options: IdempotentOptions): Promise<PublishResult>;
|
||||||
|
listPublications(query: ListPublicationsQuery, options?: RequestOptions): Promise<PublicationPage>;
|
||||||
|
getPublicationSnapshot(publicationEventId: string, options?: RequestOptions): Promise<PublicationSnapshot>;
|
||||||
|
getCatalog(query: CatalogQuery, options?: RequestOptions): Promise<CatalogPage>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the source generated contract names and exact payload fields from `lib/studio/api/generated.ts`. `StudioGatewayError` retains RFC 9457-like problem details, HTTP status, stable code, and retryability. Abort remains distinguishable from not-found/conflict/validation failures.
|
||||||
|
|
||||||
|
`PublicContentQueries` is the immutable boundary for the source functions `listRecords`, `getRecord`, `getProject`, `getRelease`, `getProjectRecords`, `getProjectDecisions`, `getProjectActivity`, `getHomeFocusItems`, and `searchPublicContent`, with the source argument and return types unchanged.
|
||||||
|
|
||||||
|
### State derivation
|
||||||
|
|
||||||
|
Port the source pure functions and values exactly:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
deriveValidationState(input: StateInput): ValidationState;
|
||||||
|
derivePreviewState(input: StateInput): PreviewState;
|
||||||
|
deriveNextAction(input: StateInput): NextAction;
|
||||||
|
deriveDocumentState(input: StateInput): StudioDocumentState;
|
||||||
|
```
|
||||||
|
|
||||||
|
Editor state is `CLEAN | DIRTY | SAVING | CONFLICT`; validation freshness is `NONE | CURRENT | STALE`; publication state is `NEVER_PUBLISHED | PUBLISHED | UNPUBLISHED`. The saved revision/version token owns optimistic concurrency.
|
||||||
|
|
||||||
|
## Deterministic source-to-target map
|
||||||
|
|
||||||
|
| Source | Target |
|
||||||
|
| --- | --- |
|
||||||
|
| `lib/content-format/*`, `lib/public-render-content.ts` | `src/features/tech-log/domain/content-format/*`, `src/features/tech-log/domain/public-render-content.ts` |
|
||||||
|
| `lib/content.ts`, `lib/evidence-assets.ts`, `lib/public-content.ts`, `lib/public-query.ts` | `src/features/tech-log/adapters/static/*` behind `PublicContentQueries` |
|
||||||
|
| `lib/studio/api/*`, `contracts/studio-api.openapi.yaml` | `src/features/tech-log/contracts/studio/*` and `src/features/tech-log/application/ports/studio-gateway.ts` |
|
||||||
|
| `lib/studio/document-state.ts`, `lib/studio/local-id.ts` | `src/features/tech-log/domain/studio/*` |
|
||||||
|
| `lib/studio/mock/*` | `src/features/tech-log/adapters/mock/*` |
|
||||||
|
| public `components/*.tsx` | `src/features/tech-log/presentation/public/components/*` |
|
||||||
|
| Studio `components/studio/*.tsx` | `src/features/tech-log/presentation/studio/components/*` |
|
||||||
|
| public `app/**/page.tsx` | `src/features/tech-log/presentation/public/pages/*` |
|
||||||
|
| Studio `app/studio/**` | `src/features/tech-log/presentation/studio/pages/*` |
|
||||||
|
| source CSS | `src/features/tech-log/presentation/styles/*` |
|
||||||
|
| `public/favicon.svg`, `public/media/fetch-strategy-boundary.svg` | same target-relative paths |
|
||||||
|
|
||||||
|
Component and stylesheet ports must retain source file boundaries where practical. Rename a file only for Vite/React Router clarity; do not combine components in a way that obscures parity review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Freeze the migration baseline and dependency/assets contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `docs/operations/techlog-ui-migration-baseline.md`
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
- Create: `public/favicon.svg`
|
||||||
|
- Create: `public/media/fetch-strategy-boundary.svg`
|
||||||
|
- Test: `tests/features/tech-log/migration-baseline.test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the red asset-integrity test.** Assert each target SVG's SHA-256 against a hand-recorded expected source hash (never an external source-path read in CI) and exercise the evidence asset lookup so an altered path/hash breaks a consumer-visible contract. Dependency pins are verified by the package manager's frozen-lockfile command; the human baseline document and its route/CSS inventory are reviewed rather than tested as source text. Include the source commit SHA or, if the source worktree has uncommitted changes, its HEAD SHA plus `git status --short` in the baseline document.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/migration-baseline.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: the migrated assets and evidence lookup are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add exact dependencies and assets.** Run `corepack pnpm add pretendard@1.3.9 @fontsource/ibm-plex-mono@5.3.0 unified@11.0.5 remark-parse@11.0.0 remark-gfm@4.0.1 remark-directive@4.0.0`, copy only the two approved assets, and record checksums, source state, the 27 expected routes, and the five CSS files in the baseline document. Do not add Next/Vinext/Cloudflare packages.
|
||||||
|
- [ ] **Step 4: Run green and lockfile verification.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/migration-baseline.test.ts
|
||||||
|
corepack pnpm verify:lockfile
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add package.json pnpm-lock.yaml public/favicon.svg public/media/fetch-strategy-boundary.svg docs/operations/techlog-ui-migration-baseline.md tests/features/tech-log/migration-baseline.test.ts
|
||||||
|
git commit -m "chore: establish TechLog migration baseline"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Port Studio API contracts and the application-facing feature seam
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/contracts/studio/studio-api.openapi.yaml`
|
||||||
|
- Create: `src/features/tech-log/contracts/studio/generated.ts`
|
||||||
|
- Create: `src/features/tech-log/contracts/studio/contract.ts`
|
||||||
|
- Create: `src/features/tech-log/application/ports/studio-gateway.ts`
|
||||||
|
- Create: `src/features/tech-log/application/ports/studio-gateway-error.ts`
|
||||||
|
- Create: `src/features/tech-log/application/ports/public-content-queries.ts`
|
||||||
|
- Create: `src/features/tech-log/application/tech-log-feature-input.ts`
|
||||||
|
- Test: `tests/features/tech-log/studio-contract.test.ts`
|
||||||
|
- Test: `tests/features/tech-log/feature-input.test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write red contract tests.** Port the source shape assertions and add compile/runtime assertions that the exact gateway method set above is exposed, feature ID is `tech-log`, and `ApplicationFeatureInputs["tech-log"]` accepts queries plus a gateway factory but no concrete adapter.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-contract.test.ts tests/features/tech-log/feature-input.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: TechLog contracts and feature input do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port contracts without reshaping payloads.** Copy the OpenAPI and generated types, replace source-local aliases only, implement the port/error, define `PublicContentQueries` from the source query return shapes, and add the module augmentation shown above.
|
||||||
|
- [ ] **Step 4: Run green and inward-boundary checks.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-contract.test.ts tests/features/tech-log/feature-input.test.ts
|
||||||
|
corepack pnpm check:types:app
|
||||||
|
corepack pnpm check:architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/contracts src/features/tech-log/application tests/features/tech-log/studio-contract.test.ts tests/features/tech-log/feature-input.test.ts
|
||||||
|
git commit -m "feat: add TechLog feature contracts"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Port Content Format v1 and the shared public render model
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/domain/content-format/heading-id.ts`
|
||||||
|
- Create: `src/features/tech-log/domain/content-format/inline-plain-text.ts`
|
||||||
|
- Create: `src/features/tech-log/domain/content-format/parse-case-content.ts`
|
||||||
|
- Create: `src/features/tech-log/domain/content-format/serialize-case-content.ts`
|
||||||
|
- Create: `src/features/tech-log/domain/content-format/project-public-render-model.ts`
|
||||||
|
- Create: `src/features/tech-log/domain/public-render-content.ts`
|
||||||
|
- Create: `src/features/tech-log/presentation/shared/public-render/*`
|
||||||
|
- Test: `tests/features/tech-log/content-format.test.ts`
|
||||||
|
- Test: `tests/features/tech-log/public-render.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Port the source parser/serializer/renderer tests first.** Keep headings, inline text, GFM tables, directives, evidence figures, code blocks, callouts, malformed-input fallback, unsafe HTML/script/`javascript:`/unknown-asset rejection, and parse→serialize round-trip fixtures byte-equivalent. Include code-copy success/failure/live-region reset and evidence zoom open/backdrop-close/button-close/trigger-focus restoration.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: shared content functions/components are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port the pure format code and renderer components.** Map `components/public-render/*`, `code-block.tsx`, `document-toc.tsx`, and both evidence-figure implementations into `presentation/shared`. Preserve emitted tags/classes/ARIA and sanitize/escape behavior; do not use raw HTML insertion.
|
||||||
|
- [ ] **Step 4: Run green and security/architecture gates.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
|
||||||
|
corepack pnpm check:browser-security
|
||||||
|
corepack pnpm check:architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/domain src/features/tech-log/presentation/shared tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
|
||||||
|
git commit -m "feat: port TechLog content format and renderer"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Compose deterministic Public content and the mock Studio gateway
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/adapters/static/content.ts`
|
||||||
|
- Create: `src/features/tech-log/adapters/static/evidence-assets.ts`
|
||||||
|
- Create: `src/features/tech-log/adapters/static/public-content.ts`
|
||||||
|
- Create: `src/features/tech-log/adapters/static/public-query.ts`
|
||||||
|
- Create: `src/features/tech-log/domain/studio/document-state.ts`
|
||||||
|
- Create: `src/features/tech-log/domain/studio/local-id.ts`
|
||||||
|
- Create: `src/features/tech-log/adapters/mock/*`
|
||||||
|
- Create: `src/features/tech-log/adapters/create-tech-log-feature-input.ts`
|
||||||
|
- Modify: `src/features/installed-feature-adapters.ts`
|
||||||
|
- Test: `tests/features/tech-log/public-query.test.ts`
|
||||||
|
- Test: `tests/features/tech-log/studio-document-state.test.ts`
|
||||||
|
- Test: `tests/features/tech-log/mock-studio-gateway.test.ts`
|
||||||
|
- Test: `tests/features/tech-log/runtime-composition.test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Port red source tests.** Cover Public type/kind/status/search filtering, stable ordering, source fixtures, state derivation, deterministic IDs/cursors, create/save conflict, validation, preview freshness/expiry, publish/unpublish, snapshots, idempotency, abort, and gateway error shapes.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-query.test.ts tests/features/tech-log/studio-document-state.test.ts tests/features/tech-log/mock-studio-gateway.test.ts tests/features/tech-log/runtime-composition.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: data adapters and installed `tech-log` input are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port data/state/mock code exactly.** Preserve fixture IDs, timestamps, copy, pagination cursors, validation issue order, error codes, stable stringify rules, preview content, and publication history. Add the TechLog input beside the temporarily retained reference-feature input in `createInstalledFeatureInputs`; one call to `createStudioGateway` creates one isolated mutable session.
|
||||||
|
- [ ] **Step 4: Run green and boundary checks.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-query.test.ts tests/features/tech-log/studio-document-state.test.ts tests/features/tech-log/mock-studio-gateway.test.ts tests/features/tech-log/runtime-composition.test.ts
|
||||||
|
corepack pnpm check:types:app
|
||||||
|
corepack pnpm check:architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/adapters src/features/tech-log/domain/studio src/features/installed-feature-adapters.ts tests/features/tech-log/public-query.test.ts tests/features/tech-log/studio-document-state.test.ts tests/features/tech-log/mock-studio-gateway.test.ts tests/features/tech-log/runtime-composition.test.ts
|
||||||
|
git commit -m "feat: compose TechLog static and mock adapters"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Add the nested route-group capability and freeze TechLog route contracts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/contracts/routes.ts`
|
||||||
|
- Modify: `src/contracts/route-runtime-contract.ts`
|
||||||
|
- Modify: `src/features/installed-feature-contracts.ts`
|
||||||
|
- Modify: `src/features/installed-feature-runtimes.tsx`
|
||||||
|
- Modify: `src/features/reference-feature/contracts/reference-feature-contract.ts`
|
||||||
|
- Modify: `config/contracts/registry-governance.json`
|
||||||
|
- Modify: `src/presentation/routes/app-router.tsx`
|
||||||
|
- Modify: `src/presentation/routes/route-codecs.ts`
|
||||||
|
- Modify: `src/presentation/routes/platform-route-codecs.ts`
|
||||||
|
- Create: `src/features/tech-log/contracts/tech-log-route-contract.ts`
|
||||||
|
- Create: `src/features/tech-log/contracts/tech-log-message-catalog.ts`
|
||||||
|
- Create: `src/features/tech-log/presentation/tech-log-route-codecs.ts`
|
||||||
|
- Test: `tests/features/tech-log/route-contract.test.ts`
|
||||||
|
- Modify: `tests/component/router.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red route tests.** Assert the exact standalone 27-entry TechLog contract table, `layoutGroup`, Studio routes currently public, non-empty string codecs, generic Public/Studio parent assembly, Studio catch-all precedence over global catch-all, and canonical URL creation. The installed starter registry remains intact through this task so no route points to an unfinished screen.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/route-contract.test.ts tests/component/router.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: layout grouping and the standalone TechLog route contract are missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement grouped route assembly without incomplete runtime entries.** Build a pure `createGroupedRouteObjects` helper that accepts a registry, matching runtime, and layout elements, then creates Public/Studio parent `RouteObject`s while retaining `RouteLifecycle`, `RouteInputProvider`, `ProtectedRoute`, Suspense, render boundary, and chunk recovery around each registered leaf. Add `layoutGroup: "PUBLIC"` to currently installed platform/reference definitions and keep the existing `AppShell` as the installed Public layout until Task 13 atomically installs the complete TechLog runtime. Extend `FE-REG-ROUTE` governance with required string field `layoutGroup`, allowed values `PUBLIC | STUDIO`, and the TechLog route/search schema IDs; add `layoutGroup` to breaking fields because layout lifetime changes navigation behavior.
|
||||||
|
- [ ] **Step 4: Run green and registry gates.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/route-contract.test.ts tests/component/router.test.tsx
|
||||||
|
corepack pnpm check:registries:structure
|
||||||
|
corepack pnpm check:architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/contracts/routes.ts src/contracts/route-runtime-contract.ts src/features/installed-feature-contracts.ts src/features/installed-feature-runtimes.tsx src/features/reference-feature/contracts/reference-feature-contract.ts src/features/tech-log/contracts/tech-log-route-contract.ts src/features/tech-log/contracts/tech-log-message-catalog.ts src/features/tech-log/presentation/tech-log-route-codecs.ts src/presentation/routes/app-router.tsx src/presentation/routes/route-codecs.ts src/presentation/routes/platform-route-codecs.ts config/contracts/registry-governance.json tests/features/tech-log/route-contract.test.ts tests/component/router.test.tsx
|
||||||
|
git commit -m "feat: add grouped TechLog route contracts"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 6: Port exact styles, fonts, Public shell, header, and search dialog
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/styles/globals.css`
|
||||||
|
- Create: `src/features/tech-log/presentation/styles/studio.css`
|
||||||
|
- Create: `src/features/tech-log/presentation/styles/studio-editor.css`
|
||||||
|
- Create: `src/features/tech-log/presentation/styles/workflow.module.css`
|
||||||
|
- Create: `src/features/tech-log/presentation/styles/publication-flow.module.css`
|
||||||
|
- Modify: `src/main.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/site-header.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/search-dialog.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/fatal-error-state.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/public-shell.tsx`
|
||||||
|
- Test: `tests/features/tech-log/style-contract.test.ts`
|
||||||
|
- Test: `tests/features/tech-log/public-shell.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red style and interaction contracts.** Render the real shell and assert its DOM/class/ARIA relationships plus consumer-visible computed typography, color, width, spacing, focus, and minimum target behavior where the test browser supports it; media-query transitions and full computed-style/pixel equality remain Task 14 browser assertions. Assert header links/labels match, `/` brand navigation works, search opens by click and keyboard, Escape/focus restoration work, and dialog results navigate to canonical routes. Do not grep CSS source text or assert private CSS-module key inventories.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/style-contract.test.ts tests/features/tech-log/public-shell.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: source styles/shell are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Copy styles and port shell components.** Import fonts and TechLog CSS after target `theme.css`; translate navigation APIs only. Preserve source header DOM and mobile behavior. Ensure Public pages render inside source-equivalent `<main>` without the old `AppShell` chrome.
|
||||||
|
- [ ] **Step 4: Run green and static style checks.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/style-contract.test.ts tests/features/tech-log/public-shell.test.tsx
|
||||||
|
corepack pnpm lint
|
||||||
|
corepack pnpm check:types:app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/main.tsx src/features/tech-log/presentation/styles src/features/tech-log/presentation/public tests/features/tech-log/style-contract.test.ts tests/features/tech-log/public-shell.test.tsx
|
||||||
|
git commit -m "feat: port TechLog shells and styles"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 7: Port Public home, explore, and search screens
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/home-focus.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/latest-index.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/explore-filter-form.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/public-record-list.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/home-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/explore-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/explore-kind-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/search-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/domain/public/focus-state.ts`
|
||||||
|
- Test: `tests/features/tech-log/public-discovery-screens.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red component cases.** Port source expectations for headings, introductory copy, counts, latest records, focus-tab URL normalization and Arrow/Home/End keyboard movement, explore kind/status/query filters, empty results, URL search synchronization, result ordering, keyboard submit, and result navigation.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-discovery-screens.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: discovery pages are missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port exact source JSX and bind queries.** Replace async Next server inputs with `useRouteInput` plus `application.features.get("tech-log").publicContent`; preserve DOM/classes/copy and query semantics. Use `Link`/`useNavigate` translations only.
|
||||||
|
- [ ] **Step 4: Run green and typecheck.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-discovery-screens.test.tsx
|
||||||
|
corepack pnpm check:types:app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/domain/public/focus-state.ts src/features/tech-log/presentation/public tests/features/tech-log/public-discovery-screens.test.tsx
|
||||||
|
git commit -m "feat: port TechLog discovery screens"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 8: Port Public cases, references, questions, and topics
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/public-document-header.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/public-document-relations.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/case-document-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/reference-document-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/question-document-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/case-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/reference-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/question-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/topic-page.tsx`
|
||||||
|
- Test: `tests/features/tech-log/public-document-screens.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red cases from source rendered-HTML and interaction tests.** Assert each known slug's exact title, metadata, relation sections, table of contents, rendered blocks, evidence media, anchors, back links, and topic aggregation. Assert unknown slugs use Public not-found rather than a generic runtime error.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-document-screens.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: document route pages are missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port page/component JSX and connect shared renderer.** Preserve all source record ordering and classes. Keep specialized hard-coded source case pages represented by the same exact output at their canonical slugs.
|
||||||
|
- [ ] **Step 4: Run green plus accessibility component checks.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-document-screens.test.tsx tests/features/tech-log/public-render.test.tsx
|
||||||
|
corepack pnpm check:types:app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/presentation/public tests/features/tech-log/public-document-screens.test.tsx
|
||||||
|
git commit -m "feat: port TechLog document screens"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 9: Port projects, releases, profile, and Public fallbacks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/project-navigation.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/components/project-page-header.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/projects-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/project-overview-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/project-records-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/project-decisions-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/project-activity-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/releases-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/release-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/profile-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/public/pages/public-not-found-page.tsx`
|
||||||
|
- Test: `tests/features/tech-log/public-index-screens.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red cases.** Assert exact project tabs/active states, record/decision/activity filtering and order, release index/detail text, profile content, cross-links, and Public unknown-route/unknown-record copy.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-index-screens.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: remaining Public screens are missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port exact source markup and route wiring.** Translate Next links only; derive active tab from React Router location without changing element structure.
|
||||||
|
- [ ] **Step 4: Run complete Public green suite.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/public-*.test.tsx tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-query.test.ts
|
||||||
|
corepack pnpm check:types:app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/presentation/public tests/features/tech-log/public-index-screens.test.tsx
|
||||||
|
git commit -m "feat: complete TechLog public screens"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 10: Port Studio provider, runtime boundary, shell, dashboard, list, and creation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/studio-provider.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/use-studio.ts`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/studio-runtime-boundary.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/studio-header.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/studio-dashboard.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/document-list.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/new-document-form.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/studio-home-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/documents-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/new-document-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/studio-not-found-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/studio-shell.tsx`
|
||||||
|
- Test: `tests/features/tech-log/studio-shell-smoke.test.tsx`
|
||||||
|
- Test: `tests/features/tech-log/studio-screens-smoke.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Port red shell/screen tests.** Assert one gateway creation per Studio shell session, a new gateway plus cleared requests/dialogs on `pageshow` with `persisted === true`, source header/nav/labels, dashboard totals/status links, list filters/pagination/empty/error/retry states, new-document type selection and redirect, direct route access without auth UI, and in-shell Studio not-found behavior.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Studio provider and real pages are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port provider/shell/screens.** Resolve `createStudioGateway` through the application feature input once via lazy state/ref, preserve gateway state across child navigation, cancel obsolete requests, and preserve source loading/error/not-found markup. Recreate the gateway and provider generation when a persisted bfcache page is shown; use provider generation keys so all child state and dialogs reset with it.
|
||||||
|
- [ ] **Step 4: Run green and composition regression.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx tests/features/tech-log/runtime-composition.test.ts
|
||||||
|
corepack pnpm check:architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/presentation/studio tests/features/tech-log/studio-shell-smoke.test.tsx tests/features/tech-log/studio-screens-smoke.test.tsx
|
||||||
|
git commit -m "feat: port TechLog Studio shell and indexes"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 11: Port document editors and instant preview
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/common-document-fields.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/case-fields.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/reference-fields.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/question-fields.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/ordered-text-list.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/relation-editor.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/document-editor.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/document-editor-screen.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/instant-preview.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/document-status-rail.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/publication-flow-classes.ts`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/document-edit-page.tsx`
|
||||||
|
- Test: `tests/features/tech-log/studio-editor-smoke.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Port red editor tests.** Assert exact controls/order/labels for case/reference/question, loaded working-copy values, add/remove/reorder relations and ordered lists, dirty state, status rail, instant preview updates, focus behavior, and source accessibility names.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-editor-smoke.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: the edit route screen is missing or lacks source controls.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port editor components exactly.** Keep local working-copy state presentation-owned, reuse Content Format v1/shared Public renderer for preview, and preserve CSS module class-name composition through a typed `publication-flow-classes.ts` equivalent.
|
||||||
|
- [ ] **Step 4: Run green and renderer regression.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-editor-smoke.test.tsx tests/features/tech-log/content-format.test.ts tests/features/tech-log/public-render.test.tsx
|
||||||
|
corepack pnpm check:types:app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/presentation/studio tests/features/tech-log/studio-editor-smoke.test.tsx
|
||||||
|
git commit -m "feat: port TechLog Studio editors"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 12: Implement save, conflict, dirty-leave, validation, and preview workflows
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/guarded-studio-link.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/unsaved-leave-dialog.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/use-before-unload.ts`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/validation-report.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/validation-screen.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/public-preview-screen.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/document-validation-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/document-preview-page.tsx`
|
||||||
|
- Modify: `src/features/tech-log/presentation/studio/components/document-editor-screen.tsx`
|
||||||
|
- Test: `tests/features/tech-log/studio-save-navigation.test.tsx`
|
||||||
|
- Test: `tests/features/tech-log/studio-validation-preview.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red workflow cases.** Cover save pending/success, revision conflict without data loss, gateway error/retry, internal link leave dialog, stay/discard/save-then-navigate choices, trigger focus restoration, browser `beforeunload`, validation issue anchors, validation state/freshness, preview create/current/stale/expired states, and exact next-action labels.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: saves/guards and validation/preview pages are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port workflow behavior.** Generate a new idempotency key per user command and reuse it only for that command's safe retry. Keep source dirty/conflict semantics, dialog DOM/focus trap/return focus, and derived state copy. Abort route-obsolete reads without converting aborts into visible errors.
|
||||||
|
- [ ] **Step 4: Run green and relevant browser smoke.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx tests/features/tech-log/mock-studio-gateway.test.ts
|
||||||
|
corepack pnpm check:browser-security
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/features/tech-log/presentation/studio tests/features/tech-log/studio-save-navigation.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx
|
||||||
|
git commit -m "feat: port TechLog Studio validation workflow"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 13: Port publish, unpublish, publication history, and immutable snapshots
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/warning-acknowledgements.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/publish-screen.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/unpublish-dialog.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/publication-list.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/components/publication-event-preview-screen.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/document-publish-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/publications-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/studio/pages/publication-preview-page.tsx`
|
||||||
|
- Create: `src/features/tech-log/presentation/tech-log-route-runtime.tsx`
|
||||||
|
- Modify: `src/contracts/routes.ts`
|
||||||
|
- Modify: `src/contracts/route-runtime-contract.ts`
|
||||||
|
- Modify: `src/features/installed-feature-contracts.ts`
|
||||||
|
- Modify: `src/features/installed-feature-runtimes.tsx`
|
||||||
|
- Modify: `src/features/installed-feature-adapters.ts`
|
||||||
|
- Modify: `src/features/installed-feature-messages.ts`
|
||||||
|
- Modify: `src/presentation/routes/route-runtime.tsx`
|
||||||
|
- Modify: `src/presentation/routes/platform-route-codecs.ts`
|
||||||
|
- Modify: `public/release-manifest.json`
|
||||||
|
- Modify: `scripts/test-performance.ts`
|
||||||
|
- Modify: `tests/component/router.test.tsx`
|
||||||
|
- Modify: `tests/component/runtime-application.test.tsx`
|
||||||
|
- Modify: `tests/features/reference-feature/reference-contract.test.ts`
|
||||||
|
- Remove: `src/features/reference-feature/presentation/**`
|
||||||
|
- Remove: `src/presentation/examples/auth-example-page.tsx`
|
||||||
|
- Remove: `src/presentation/examples/platform-overview-page.tsx`
|
||||||
|
- Remove: `src/presentation/examples/state-gallery-page.tsx`
|
||||||
|
- Remove: `src/presentation/examples/ui-gallery-page.tsx`
|
||||||
|
- Remove: `src/presentation/pages/home-page.tsx`
|
||||||
|
- Remove: `src/presentation/pages/not-found-page.tsx`
|
||||||
|
- Remove: `tests/component/platform-overview-page.test.tsx`
|
||||||
|
- Remove: `tests/features/reference-feature/reference-page.test.tsx`
|
||||||
|
- Remove: `tests/features/reference-feature/reference-production-vertical.test.tsx`
|
||||||
|
- Remove: `tests/e2e/platform-overview.spec.ts`
|
||||||
|
- Remove: `tests/e2e/reference-form.spec.ts`
|
||||||
|
- Remove: `tests/e2e/reference-route.spec.ts`
|
||||||
|
- Remove: `tests/e2e/ui-gallery.spec.ts`
|
||||||
|
- Test: `tests/features/tech-log/studio-publication-flow.test.tsx`
|
||||||
|
- E2E: `tests/e2e/tech-log-studio-workflow.spec.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Port red publication tests.** Assert blocked publish for invalid/stale preview, warning acknowledgement requirements, pending/error/retry behavior, successful event/URL/state, publication filtering, unpublish reason/confirmation, immutable historical snapshot rendering after later edits, and unknown publication Studio not-found.
|
||||||
|
- [ ] **Step 2: Run red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-publication-flow.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: publication screens are missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Port publication flow exactly.** Preserve source DOM/classes/copy and gateway command ordering. Render event snapshots through the shared Public renderer; never substitute current working-copy content.
|
||||||
|
- [ ] **Step 4: Atomically install the complete feature.** Make `ROUTE_REGISTRY`, `ROUTE_RUNTIME_CONTRACT`, codecs, runtime imports, and release-manifest chunk entries expose only the complete TechLog routes; configure `PublicShell` and `StudioShell` as their layout elements. Keep the non-UI reference contracts/adapters and their HTTP/platform tests installed as template contract fixtures, but remove their product routes, runtime pages, page-level tests, and every `/examples/*` screen. Update the reference contract test so it continues to prove API/schema/invalidation behavior without asserting product route installation. Retain platform boot, diagnostics, providers, lifecycle boundaries, service worker, and generic design-system infrastructure.
|
||||||
|
- [ ] **Step 5: Update route consumers.** Rewrite router/runtime-application expectations for the TechLog home, point the performance probe at `TECH_LOG_HOME`, and write all 27 derived chunk IDs to `public/release-manifest.json`. Search the active source/tests/config for old product route imports and prove only deliberately retained platform test-fixture IDs remain.
|
||||||
|
- [ ] **Step 6: Run green, removal, and end-to-end workflow.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log/studio-publication-flow.test.tsx tests/features/tech-log/studio-validation-preview.test.tsx tests/features/tech-log/mock-studio-gateway.test.ts
|
||||||
|
corepack pnpm exec vitest run tests/component/router.test.tsx tests/component/runtime-application.test.tsx tests/features/reference-feature/reference-contract.test.ts
|
||||||
|
corepack pnpm test:sample-removal
|
||||||
|
corepack pnpm check:registries:structure
|
||||||
|
corepack pnpm exec playwright test tests/e2e/tech-log-studio-workflow.spec.ts --project=chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A -- src/contracts src/features src/presentation/examples src/presentation/pages src/presentation/routes public/release-manifest.json scripts/test-performance.ts tests/features tests/component tests/e2e/platform-overview.spec.ts tests/e2e/reference-form.spec.ts tests/e2e/reference-route.spec.ts tests/e2e/ui-gallery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts
|
||||||
|
git commit -m "feat: complete TechLog Studio publication flow"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 14: Prove visual, responsive, accessibility, architecture, and release parity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `tests/visual/tech-log.visual.spec.ts`
|
||||||
|
- Create: `tests/e2e/tech-log-public-discovery.spec.ts`
|
||||||
|
- Create: `tests/e2e/tech-log-accessibility.spec.ts`
|
||||||
|
- Create: `tests/e2e/tech-log-responsive.spec.ts`
|
||||||
|
- Modify: `tests/e2e/app-shell.spec.ts`
|
||||||
|
- Modify: `tests/e2e/accessibility.spec.ts`
|
||||||
|
- Modify: `tests/e2e/responsive.spec.ts`
|
||||||
|
- Remove: `tests/e2e/compact-smoke.spec.ts`
|
||||||
|
- Remove: `tests/e2e/design-system-interactions.spec.ts`
|
||||||
|
- Remove: `tests/e2e/i18n.spec.ts`
|
||||||
|
- Remove: `tests/e2e/theme.spec.ts`
|
||||||
|
- Remove: `tests/visual/platform.visual.spec.ts`
|
||||||
|
- Remove: `tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/**`
|
||||||
|
- Modify: `config/contracts/registry-change-evidence.json`
|
||||||
|
- Modify: `config/contracts/registry-baseline.json`
|
||||||
|
- Modify: `config/contracts/registry-baseline.approval.json`
|
||||||
|
- Modify: `docs/operations/techlog-ui-migration-baseline.md`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add parity suites before accepting snapshots.** Cover every canonical route definition, all known Public fixture slugs/versions, and every Studio screen/state. Capture full parity at 360 and 1440 pixels, breakpoint transitions at 1179/1180, 1050, 1024, 980, 900, 767/768, 420, 390, 820, and a compact 375-pixel viewport, with fixed timezone, fonts-ready wait, animation disabled, deterministic clock/data, and no masks.
|
||||||
|
- [ ] **Step 2: Establish source references.** Run the source app and target app under the same Chromium viewport/device scale/color scheme, capture both into temporary artifact directories, and use pixel diff plus DOM/class/text/ARIA assertions. Require zero pixel difference after deterministic controls; if browser rasterization still differs, document the exact pixels/cause and obtain user approval before accepting a target baseline. Source reference images are not copied into target snapshots as a shortcut, and committed/CI tests read only target fixtures and snapshots rather than the external source path.
|
||||||
|
- [ ] **Step 3: Run visual/responsive/a11y red.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec playwright test tests/visual/tech-log.visual.spec.ts --config=playwright.visual.config.ts --project=chromium
|
||||||
|
corepack pnpm exec playwright test tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected before final corrections: any remaining framework-port drift is reported with a route/viewport-specific diff.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Correct only parity defects.** Fix DOM/CSS/import ordering/router lifecycle differences without redesign. Confirm zero unexpected console errors, no horizontal overflow, keyboard-accessible dialogs/navigation, valid heading/landmark order, restored focus, and Axe results matching or improving on source without changing appearance.
|
||||||
|
- [ ] **Step 5: Retire starter-only browser evidence.** Rewrite `app-shell`, registry-wide accessibility, and responsive suites against TechLog. Delete the example-gallery/theme/locale E2E cases because those controls intentionally leave the product UI, while retaining direct component/design-system tests for the underlying platform capabilities. Replace old platform screenshots with reviewed TechLog screenshots; do not leave stale snapshots unreferenced.
|
||||||
|
- [ ] **Step 6: Record and accept the governed route/schema migration.** Run `corepack pnpm check:registries` once to generate `artifacts/quality/registries.json` and list the exact breaking change IDs. Add one complete evidence row per reported breaking change to `registry-change-evidence.json`, covering the TechLog route migration/version, atomic release-manifest/runtime update, same-release compatibility window, rollback to the prior feature commit, and owner `tech-log-frontend`. Rerun until evidence passes, then execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
REGISTRY_BASELINE_OWNER=tech-log-frontend REGISTRY_BASELINE_REASON="Install approved TechLog Public and Studio route contract" node scripts/update-registry-baseline.ts artifacts/quality/registries.json
|
||||||
|
corepack pnpm check:registries
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: approval digest matches the newly committed snapshot and compatibility impact is `none` with no unacknowledged change.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run focused full TechLog gates.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec vitest run tests/features/tech-log
|
||||||
|
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||||
|
corepack pnpm test:visual
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run repository gates and classify baseline-only failures.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm check:types
|
||||||
|
corepack pnpm lint
|
||||||
|
corepack pnpm check:architecture
|
||||||
|
corepack pnpm check:design-system
|
||||||
|
corepack pnpm check:i18n
|
||||||
|
corepack pnpm check:registries
|
||||||
|
corepack pnpm check:browser-security
|
||||||
|
corepack pnpm test:all
|
||||||
|
corepack pnpm build
|
||||||
|
git diff --check
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
All gates must pass except an exactly reproduced, documented environment-only baseline. For any baseline exception, rerun its test in isolation, record command/output/count in the baseline document, and prove no TechLog test is among the failures.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Use the verification and review skills.** Invoke `superpowers:verification-before-completion`, then `superpowers:requesting-code-review`. Resolve findings with focused red/green tests and rerun affected gates.
|
||||||
|
- [ ] **Step 10: Commit final parity evidence.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A -- tests/visual tests/e2e config/contracts/registry-change-evidence.json config/contracts/registry-baseline.json config/contracts/registry-baseline.approval.json README.md docs/operations/techlog-ui-migration-baseline.md
|
||||||
|
git commit -m "test: prove TechLog UI migration parity"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 11: Stop before integration.** Report the feature branch commit range, exact green commands, baseline-only exceptions, visual-diff result, and changed-route inventory. Wait for explicit user approval before `git flow feature finish techlog-ui-migration`, merging into `develop`, pushing, or deleting the feature branch.
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
|
||||||
|
- All 27 canonical route definitions resolve under the correct nested shell, with source-equivalent unknown-content behavior.
|
||||||
|
- Public content/search/filter/navigation output matches the source data and UI.
|
||||||
|
- Studio create/edit/save/conflict/validate/preview/publish/unpublish/history flows match the source and persist for one shell session.
|
||||||
|
- No migrated presentation module imports an adapter, Next.js, Vinext, or Cloudflare module.
|
||||||
|
- Source assets and all non-Tailwind CSS rules are preserved exactly; every specified viewport has reviewed visual evidence with no unexplained pixel drift.
|
||||||
|
- Focus, keyboard, dialog, landmarks, labels, and Axe coverage pass.
|
||||||
|
- Focused tests, typecheck, lint, architecture, registry, browser-security, build, and applicable repository suites pass, with any infrastructure-only baseline reproduced and documented.
|
||||||
|
- The work remains on `feature/techlog-ui-migration` until the user explicitly authorizes GitFlow feature completion into `develop`.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,328 @@
|
|||||||
|
# TechLog 전체 UI 이식 설계
|
||||||
|
|
||||||
|
## 상태
|
||||||
|
|
||||||
|
- 승인일: 2026-08-15
|
||||||
|
- 원본: `/home/donghyeon/workspace/techlog-studio-frontend`
|
||||||
|
- 대상: `/home/donghyeon/workspace/desktop-server-git/tech-log-frontend`
|
||||||
|
- 결정: 원본 UI를 보존하고 현재 Vite·React Router·포트/어댑터 구조로 내부 경계만 치환한다.
|
||||||
|
|
||||||
|
## 목적
|
||||||
|
|
||||||
|
원본 TechLog의 Public과 Studio 전체 화면, URL 구조, 표시 콘텐츠, 상호작용과 세션 기반 Mock 동작을 대상 프로젝트로 이식한다. 대상 프로젝트의 부트스트랩, 오류 경계, 진단, 서비스 워커, route registry와 clean architecture 경계는 유지한다.
|
||||||
|
|
||||||
|
이 작업은 새 디자인을 만드는 작업이 아니다. 원본의 DOM 구조, CSS 계산값, 폰트, 자산, 문구, 반응형 동작과 접근성 구조를 기준본으로 삼는다. 구현 편의를 위한 시각적 재해석이나 대상 디자인 시스템에 맞춘 재디자인을 허용하지 않는다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### Public
|
||||||
|
|
||||||
|
- 홈과 현재 집중 항목
|
||||||
|
- 통합 탐색과 유형별 탐색
|
||||||
|
- Case, Reference, Question 공개 문서
|
||||||
|
- Topic별 기록
|
||||||
|
- Project 목록, 개요, 기록, 결정, 활동
|
||||||
|
- Release 목록과 상세
|
||||||
|
- Profile
|
||||||
|
- 헤더 검색 dialog와 검색 결과
|
||||||
|
- Public 오류, 빈 상태와 404
|
||||||
|
- 공통 header, footer, document renderer, TOC, 관계, 코드, 표, callout, evidence figure
|
||||||
|
|
||||||
|
### Studio
|
||||||
|
|
||||||
|
- Studio dashboard
|
||||||
|
- 작업본 검색·필터·정렬 목록
|
||||||
|
- Case, Reference, Question 생성
|
||||||
|
- 편집, 즉시 preview와 저장 상태
|
||||||
|
- 검증과 issue 위치 이동
|
||||||
|
- Public preview
|
||||||
|
- 게시·재게시·게시 취소
|
||||||
|
- 게시 이벤트 목록과 불변 snapshot
|
||||||
|
- 충돌, 만료, 요청 실패, 세션 전용 not-found
|
||||||
|
- 미저장 변경의 내부 이동 dialog와 native `beforeunload`
|
||||||
|
|
||||||
|
### 동작 경계
|
||||||
|
|
||||||
|
- 원본의 정적 Public 콘텐츠와 query 동작을 유지한다.
|
||||||
|
- 원본의 `StudioGateway` 계약과 `MockStudioGateway` 상태 전이를 유지한다.
|
||||||
|
- Studio 세션의 게시 결과는 Public 정적 콘텐츠와 검색 결과를 변경하지 않는다.
|
||||||
|
- `localStorage`, 실제 서버 저장, 파일 업로드와 실제 배포 게시를 추가하지 않는다.
|
||||||
|
- Studio 인증은 후속 작업으로 유보한다. 이번 이식에서는 Studio URL에 직접 접근할 수 있고 가짜 로그인 UI를 추가하지 않는다.
|
||||||
|
|
||||||
|
## 선택한 접근
|
||||||
|
|
||||||
|
### 원본 UI 보존형 이식
|
||||||
|
|
||||||
|
원본의 React 마크업, class 이름, CSS, 폰트, 자산과 문구를 유지하고 Next/Vinext 전용 경계만 대상 런타임으로 치환한다.
|
||||||
|
|
||||||
|
- `next/link`는 React Router `Link` 또는 의도된 전체 문서 이동 `<a>`로 바꾼다.
|
||||||
|
- `usePathname`, App Router params와 search params는 React Router route input으로 바꾼다.
|
||||||
|
- Next layout 수명은 React Router 중첩 layout route로 재현한다.
|
||||||
|
- 서버 컴포넌트의 정적 조회는 순수 application query와 동기 projection으로 바꾼다.
|
||||||
|
- Studio의 비동기 요청과 오류는 주입된 `StudioGateway` port를 통해 유지한다.
|
||||||
|
|
||||||
|
별도 legacy SPA 삽입은 라우터·상태·오류 경계를 이중화하므로 사용하지 않는다. 대상을 Next/Vinext로 전환하는 방식은 현재 템플릿과 운영 계약을 폐기하므로 사용하지 않는다.
|
||||||
|
|
||||||
|
## 대상 아키텍처
|
||||||
|
|
||||||
|
TechLog 기능은 하나의 feature boundary 안에서 Public과 Studio 하위 영역을 공유한다. Public renderer를 Studio preview가 함께 사용해야 하므로 두 영역을 서로 독립된 feature로 분리하지 않는다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/features/tech-log/
|
||||||
|
├── contracts/ route, content format, gateway DTO와 schema
|
||||||
|
├── domain/ Public content와 Studio document/publication 모델
|
||||||
|
├── application/ Public query, projection, Studio state 계산과 port
|
||||||
|
├── adapters/
|
||||||
|
│ ├── static/ 원본 Public 콘텐츠 catalog
|
||||||
|
│ └── mock/ 세션 수명의 MockStudioGateway
|
||||||
|
└── presentation/
|
||||||
|
├── public/ Public shell, pages와 renderer
|
||||||
|
├── studio/ Studio shell, pages, provider와 editor
|
||||||
|
├── shared/ 양쪽이 공유하는 안전한 render component
|
||||||
|
└── styles/ 원본 CSS와 CSS Module
|
||||||
|
```
|
||||||
|
|
||||||
|
공통 플랫폼은 feature의 route contract, route runtime과 adapter factory만 조립한다. Presentation이 adapter 구현을 직접 import하지 않으며 composition root가 gateway와 Public catalog를 주입한다.
|
||||||
|
|
||||||
|
대상 템플릿의 기존 example navigation, sidebar, theme·locale selector와 인증 예제 UI는 TechLog 제품 화면에서 제거한다. 더 이상 route registry에서 참조되지 않는 sample presentation은 대상의 sample-removal 정책에 따라 제거한다. 부트 오류와 플랫폼 진단 경계는 유지한다.
|
||||||
|
|
||||||
|
## 라우팅과 layout 수명
|
||||||
|
|
||||||
|
공통 router는 플랫폼 provider와 오류 경계를 유지하고 두 개의 시각 layout group을 만든다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
공통 부트스트랩·플랫폼 오류 경계
|
||||||
|
├── PUBLIC layout
|
||||||
|
│ ├── PublicShell
|
||||||
|
│ ├── Public routes
|
||||||
|
│ └── Public 404
|
||||||
|
└── STUDIO layout
|
||||||
|
├── StudioRuntimeBoundary
|
||||||
|
├── StudioProvider
|
||||||
|
├── StudioShell
|
||||||
|
├── Studio routes
|
||||||
|
└── Studio 전용 상태
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public route pattern
|
||||||
|
|
||||||
|
| 경로 | 책임 |
|
||||||
|
| --- | --- |
|
||||||
|
| `/` | 홈 |
|
||||||
|
| `/explore` | 통합 탐색 |
|
||||||
|
| `/explore/:kind` | 유형별 탐색 |
|
||||||
|
| `/cases/:slug` | Case 문서 |
|
||||||
|
| `/references/:slug` | Reference 문서 |
|
||||||
|
| `/questions/:slug` | Question 문서 |
|
||||||
|
| `/topics/:slug` | Topic별 기록 |
|
||||||
|
| `/projects` | Project 목록 |
|
||||||
|
| `/projects/:slug` | Project 개요 |
|
||||||
|
| `/projects/:slug/records` | Project 기록 |
|
||||||
|
| `/projects/:slug/decisions` | Project 결정 |
|
||||||
|
| `/projects/:slug/activity` | Project 활동 |
|
||||||
|
| `/releases` | Release 목록 |
|
||||||
|
| `/releases/:version` | Release 상세 |
|
||||||
|
| `/profile` | Profile |
|
||||||
|
| `/search` | 검색 결과 |
|
||||||
|
| `*` | Public 404 |
|
||||||
|
|
||||||
|
### Studio route pattern
|
||||||
|
|
||||||
|
| 경로 | 책임 |
|
||||||
|
| --- | --- |
|
||||||
|
| `/studio` | dashboard |
|
||||||
|
| `/studio/documents` | 작업본 목록 |
|
||||||
|
| `/studio/documents/new` | 새 문서 생성 |
|
||||||
|
| `/studio/documents/:id/edit` | 편집과 즉시 preview |
|
||||||
|
| `/studio/documents/:id/validation` | 검증 보고서 |
|
||||||
|
| `/studio/documents/:id/preview` | 저장·검증된 Public preview |
|
||||||
|
| `/studio/documents/:id/publish` | 게시·재게시 |
|
||||||
|
| `/studio/publications` | 게시 이벤트 목록 |
|
||||||
|
| `/studio/publications/:publicationEventId/preview` | 불변 snapshot |
|
||||||
|
| 정의되지 않은 `/studio/*` | 실제 route 404 |
|
||||||
|
|
||||||
|
알 수 없는 동적 document ID와 publication event ID는 route에는 일치하지만 Studio shell 안의 전용 찾을 수 없음 상태를 렌더링한다. 선행 검증이나 preview가 부족한 직접 진입은 redirect하지 않고 원본과 같은 차단 이유와 다음 행동을 보여 준다.
|
||||||
|
|
||||||
|
Studio layout의 provider는 Studio 내부 client navigation 동안 유지된다. Studio에서 Public으로 가는 `공개 사이트 보기`는 일반 `<a>`를 사용해 전체 문서 이동, `beforeunload`와 세션 초기화를 보존한다.
|
||||||
|
|
||||||
|
라우트 정의에는 `PUBLIC` 또는 `STUDIO` layout group을 명시한다. Studio 인증을 구현할 때는 `STUDIO` group의 access policy만 `session-required`로 전환할 수 있어야 하며 화면 컴포넌트나 URL을 다시 설계하지 않는다.
|
||||||
|
|
||||||
|
## 컴포넌트 이식 규칙
|
||||||
|
|
||||||
|
- 원본 HTML tag, class 이름, 표시 문구, 요소 순서와 ARIA 관계를 유지한다.
|
||||||
|
- 원본 컴포넌트 경계를 가능한 한 유지하되 Next layout과 router hook에만 필요한 변경을 한다.
|
||||||
|
- 대상 generic design-system primitive로 화면을 다시 그리지 않는다.
|
||||||
|
- Public과 Studio가 공유하는 Public renderer는 하나만 유지한다.
|
||||||
|
- 문자열 HTML과 `dangerouslySetInnerHTML`을 도입하지 않는다.
|
||||||
|
- source의 semantic heading, landmark, tab, dialog, live region과 focus restoration을 유지한다.
|
||||||
|
- 검색 dialog는 하나만 렌더링하고 원본처럼 viewport 중앙에 둔다.
|
||||||
|
- Studio editor 탭 전환은 draft를 잃지 않으며 즉시 preview는 gateway 상태를 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 스타일·폰트·자산 보존
|
||||||
|
|
||||||
|
원본의 다음 파일을 시각 기준으로 사용한다.
|
||||||
|
|
||||||
|
- `app/globals.css`
|
||||||
|
- `app/studio.css`
|
||||||
|
- `app/studio-editor.css`
|
||||||
|
- `components/studio/workflow.module.css`
|
||||||
|
- `components/studio/publication-flow.module.css`
|
||||||
|
- `pretendard/dist/web/variable/pretendardvariable.css`
|
||||||
|
- `@fontsource/ibm-plex-mono/400.css`
|
||||||
|
- `@fontsource/ibm-plex-mono/500.css`
|
||||||
|
- `public/favicon.svg`
|
||||||
|
- `public/media/fetch-strategy-boundary.svg`
|
||||||
|
|
||||||
|
대상 `theme.css`의 Tailwind import와 플랫폼 token은 부트 오류 같은 플랫폼 표면을 위해 유지한다. TechLog 전역 스타일은 그 뒤에 unlayered CSS로 한 번만 로드한다. 원본 `globals.css`의 중복 `@import "tailwindcss"`만 제외하며 그 뒤 rule 순서와 선언값은 유지한다.
|
||||||
|
|
||||||
|
다음 값은 재해석하거나 대상 token 값으로 치환하지 않는다.
|
||||||
|
|
||||||
|
- `--canvas`, `--paper`, `--ink`, `--muted`, `--faint`, `--signal` 등 원본 color token
|
||||||
|
- `--shell: 1180px`, `--body-copy: 42rem`
|
||||||
|
- font size, weight, line-height와 letter-spacing
|
||||||
|
- border, radius, shadow와 transition
|
||||||
|
- `1179`, `1050`, `1024`, `980`, `900`, `767`, `420px` breakpoint
|
||||||
|
- reduced-motion 동작과 최소 `44px` interaction target
|
||||||
|
|
||||||
|
Public과 Studio의 최종 computed style은 원본이 기준이다. 자산은 내용 변경 없이 복사하고 build가 제공하는 동일-origin URL을 사용한다.
|
||||||
|
|
||||||
|
## Public 데이터와 query
|
||||||
|
|
||||||
|
Public 콘텐츠는 immutable static catalog adapter가 소유한다. Application query는 catalog port만 사용해 다음 결과를 파생한다.
|
||||||
|
|
||||||
|
- 홈 focus와 latest index
|
||||||
|
- 유형·topic·project filter
|
||||||
|
- 제목·요약·topic·project 검색
|
||||||
|
- Project별 record, decision, activity
|
||||||
|
- 문서 relation과 related content
|
||||||
|
- Release와 Profile
|
||||||
|
|
||||||
|
페이지 컴포넌트는 URL params와 query를 route codec으로 검증한 뒤 application query를 호출한다. 잘못된 filter 값은 원본의 canonical 상태로 정규화하고 검색 query는 URL에 보존한다. 존재하지 않는 slug와 version은 Public 404로 보낸다.
|
||||||
|
|
||||||
|
## Content Format과 공유 renderer
|
||||||
|
|
||||||
|
Case 본문의 Content Format v1 parser, serializer와 `PublicRenderModel` 판별 union을 유지한다. 지원 block은 heading, paragraph, blockquote, ordered/unordered list, code block, data table, callout와 evidence figure다. 지원 inline은 text, emphasis, strong, inline code, link와 status다.
|
||||||
|
|
||||||
|
Public 문서와 Studio 즉시 preview, 검증 preview, publication snapshot은 같은 typed renderer를 사용한다. raw HTML, script, `javascript:` URL과 임의 asset URL은 계속 거절한다.
|
||||||
|
|
||||||
|
## Studio port, adapter와 상태
|
||||||
|
|
||||||
|
Presentation은 `StudioGateway` port만 사용한다. 모든 method는 `Promise`를 반환하고 선택적인 `AbortSignal`을 받는다. 예상 가능한 실패는 RFC 9457 본문, status, code와 retryable 정보를 가진 `StudioGatewayError`로 정규화한다. `AbortError`만 조용히 무시하고 프로그래밍 오류는 runtime boundary로 전달한다.
|
||||||
|
|
||||||
|
Mock adapter는 원본 seed data, cursor, conflict fixture, preview expiry, validation, publication aggregate와 idempotency 동작을 보존한다.
|
||||||
|
|
||||||
|
Studio 상태 축은 다음 값을 유지한다.
|
||||||
|
|
||||||
|
- Editor: `CLEAN`, `DIRTY`, `SAVING`, `CONFLICT`
|
||||||
|
- Validation result: `NOT_RUN`, `INVALID`, `WARNINGS`, `VALID`
|
||||||
|
- Validation freshness: `NONE`, `CURRENT`, `STALE`
|
||||||
|
- Preview: `NONE`, `CURRENT`, `STALE`, `EXPIRED`
|
||||||
|
- Publication: `NEVER_PUBLISHED`, `PUBLISHED`, `UNPUBLISHED`
|
||||||
|
|
||||||
|
저장은 불완전 draft를 허용하고 version을 증가시킨다. 저장 뒤 validation은 `NOT_RUN`과 `NONE`, 기존 preview는 `STALE`이 된다. 게시 준비 검증과 다음 행동 우선순위, warning acknowledgement, publication idempotency와 unpublish 규칙은 원본 계약을 유지한다.
|
||||||
|
|
||||||
|
Studio 세션은 layout이 유지되는 동안만 살아 있다. 새로고침, Public 전체 이동과 `pageshow.persisted === true`에서 새 Mock adapter를 만들고 draft, request와 dialog를 초기화한다. 고정 fixture ID는 seed 상태로 돌아가고 세션 생성 ID는 Studio 전용 찾을 수 없음 상태가 된다.
|
||||||
|
|
||||||
|
## 오류와 빈 상태
|
||||||
|
|
||||||
|
- Boot failure는 기존 `BootErrorShell`이 담당한다.
|
||||||
|
- Public query·render 실패는 Public shell 안의 원본 fatal error surface를 사용한다.
|
||||||
|
- Public empty, no-result와 not-found는 서로 다른 원본 상태를 유지한다.
|
||||||
|
- Studio request failure는 `StudioGatewayError`의 code와 retryable을 기준으로 원본 메시지와 action을 표시한다.
|
||||||
|
- Studio render failure는 `StudioRuntimeBoundary`가 담당한다.
|
||||||
|
- save conflict는 현재 입력을 보존하고 gateway 최신본과 field path를 비교한다.
|
||||||
|
- auto merge와 auto overwrite를 추가하지 않는다.
|
||||||
|
- dirty 내부 이동은 머무르기, 변경 버리기와 저장 후 이동을 제공하고 trigger focus를 복원한다.
|
||||||
|
|
||||||
|
## 인증 유보
|
||||||
|
|
||||||
|
Studio는 인증이 필요한 제품 영역이지만 이번 이식에서는 인증 구현을 범위 밖으로 둔다.
|
||||||
|
|
||||||
|
- Studio route group을 별도로 유지한다.
|
||||||
|
- access policy 전환 지점을 route contract에 둔다.
|
||||||
|
- 현재는 직접 URL 접근을 허용한다.
|
||||||
|
- 가짜 로그인, 임시 계정과 인증된 것처럼 보이는 UI를 추가하지 않는다.
|
||||||
|
- 후속 인증 작업은 Studio 화면 DOM, URL과 gateway contract를 변경하지 않고 route guard와 session adapter를 연결하는 방식으로 수행한다.
|
||||||
|
|
||||||
|
## GitFlow와 전달 브랜치
|
||||||
|
|
||||||
|
저장소는 `main`을 production 브랜치, `develop`을 integration 브랜치로 사용하는 GitFlow로 초기화한다. 이 설계 문서와 선행 template 동기화가 포함된 현재 `main`에서 `develop`을 만든다.
|
||||||
|
|
||||||
|
- feature prefix는 `feature/`를 사용한다.
|
||||||
|
- 화면 이식 작업은 `develop`에서 시작한 `feature/techlog-ui-migration`에서만 수행한다.
|
||||||
|
- 구현 계획, 테스트, source port, 자산과 검증 문서는 같은 feature 브랜치에 커밋한다.
|
||||||
|
- `main`과 `develop`에는 화면 이식 production code를 직접 커밋하지 않는다.
|
||||||
|
- feature 통합은 구현·검증 완료 뒤 사용자가 선택한 방식으로 수행한다.
|
||||||
|
- 원격 push, remote branch 생성과 GitFlow feature finish는 별도 사용자 요청 전에는 수행하지 않는다.
|
||||||
|
|
||||||
|
## 테스트 전략
|
||||||
|
|
||||||
|
모든 production 동작 변경은 TDD로 진행한다. 각 slice는 기대 동작을 표현하는 실패 테스트를 먼저 추가하고 예상한 이유로 실패하는 것을 확인한 다음 최소 구현을 추가한다.
|
||||||
|
|
||||||
|
### 구조·계약 테스트
|
||||||
|
|
||||||
|
- 모든 Public·Studio route ID, path, layout group과 runtime module mapping
|
||||||
|
- Public/Studio layout 수명과 Studio gateway 단일 instance
|
||||||
|
- 원본 heading, landmark, class, element 순서와 ARIA 관계
|
||||||
|
- source CSS color, typography, width, breakpoint와 touch target 계약
|
||||||
|
- Public content graph의 유효한 내부 링크와 404 경계
|
||||||
|
- Content Format parser·serializer round trip과 unsafe input 거절
|
||||||
|
|
||||||
|
### 상호작용·상태 테스트
|
||||||
|
|
||||||
|
- 홈 focus tab과 URL 정규화
|
||||||
|
- 탐색 filter, reset, empty와 no-result
|
||||||
|
- 검색 dialog open/close, focus trap·restore와 query 보존
|
||||||
|
- document TOC, code copy와 evidence dialog
|
||||||
|
- Studio document 생성, 편집, 저장, validation, preview, publish와 unpublish
|
||||||
|
- conflict, stale·expired preview, warning acknowledgement와 idempotency
|
||||||
|
- dirty navigation dialog와 native `beforeunload`
|
||||||
|
- 새로고침·Public 이동·bfcache 복원 뒤 세션 reset
|
||||||
|
|
||||||
|
### 접근성·반응형·시각 검증
|
||||||
|
|
||||||
|
동일한 Chromium, font와 reduced-motion 조건에서 원본과 대상을 캡처한다.
|
||||||
|
|
||||||
|
- 고정 fixture로 접근 가능한 모든 canonical Public·Studio route: `360px`, `1440px`
|
||||||
|
- breakpoint 대표 화면: `390`, `768`, `820`, `1024`, `1180px`
|
||||||
|
- 열린 검색 dialog와 mobile menu
|
||||||
|
- Studio editor, 즉시 preview, warning과 dirty-leave dialog
|
||||||
|
- viewport 전체의 예상하지 않은 가로 overflow
|
||||||
|
- axe, keyboard navigation, focus visibility, single H1과 `aria-current`
|
||||||
|
|
||||||
|
동적 timestamp, caret와 animation을 고정한 뒤 pixel difference는 원칙적으로 `0`을 요구한다. 차이는 개선 여부가 아니라 원본과 동일한지로 판정한다. 브라우저 rasterization처럼 통제할 수 없는 차이가 발견되면 원인을 기록하고 사용자의 별도 승인을 받기 전에는 baseline을 갱신하지 않는다.
|
||||||
|
|
||||||
|
CI는 커밋된 target snapshot과 계약 테스트를 사용하며 외부 원본 경로에 의존하지 않는다. 구현 중 로컬 one-time source/target 비교로 baseline을 만들고 이후 target regression test로 고정한다.
|
||||||
|
|
||||||
|
## 검증 명령과 완료 기준
|
||||||
|
|
||||||
|
구현 완료 전에 다음 범주를 모두 실행한다.
|
||||||
|
|
||||||
|
- TechLog route·component·integration test
|
||||||
|
- TechLog Studio gateway·state·publication test
|
||||||
|
- Playwright visual·accessibility test
|
||||||
|
- TypeScript 전체 project 검사
|
||||||
|
- ESLint
|
||||||
|
- architecture, route registry, design-system과 i18n contract 검사
|
||||||
|
- production build
|
||||||
|
- repository full test suite
|
||||||
|
|
||||||
|
현재 저장소에 이미 기록된 RLIMIT, EMFILE, umask와 `/tmp` 관련 19개 환경 의존 실패는 known baseline으로 분리한다. 그 외 새 실패를 허용하지 않으며 TechLog 이식으로 추가된 테스트는 모두 통과해야 한다. 병합 전에는 known baseline과 새 실패를 구분한 결과를 사용자에게 보고한다.
|
||||||
|
|
||||||
|
완료 조건은 다음과 같다.
|
||||||
|
|
||||||
|
1. 원본 Public과 Studio canonical URL이 대상에서 모두 열리고 정의되지 않은 경로가 올바른 404를 반환한다.
|
||||||
|
2. 원본의 표시 콘텐츠, DOM·ARIA 구조, CSS 계산값, font와 asset이 유지된다.
|
||||||
|
3. Public 검색·탐색·문서 연결과 Studio 전체 Mock workflow가 원본과 동일하게 동작한다.
|
||||||
|
4. Studio 내부 이동 동안 상태가 유지되고 전체 문서 이동·새로고침·bfcache에서 초기화된다.
|
||||||
|
5. 대상의 clean architecture, route registry, 부트·진단·서비스 워커 계약이 유지된다.
|
||||||
|
6. 승인되지 않은 시각 diff와 새 test failure가 없다.
|
||||||
|
|
||||||
|
## 범위 밖
|
||||||
|
|
||||||
|
- Studio 인증과 권한
|
||||||
|
- 실제 HTTP Studio adapter와 백엔드 연결
|
||||||
|
- PostgreSQL, Cloudflare D1·R2와 파일 업로드
|
||||||
|
- Public 콘텐츠 CMS화
|
||||||
|
- 디자인 개선, 문구 수정과 정보 구조 재해석
|
||||||
|
- 원본에 없는 화면이나 기능 추가
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
# TechLog Backend 정합 설계
|
||||||
|
|
||||||
|
## 상태
|
||||||
|
|
||||||
|
- 승인일: 2026-08-17
|
||||||
|
- 기준선: `tech-log-frontend` `main` (UI 이식 완료 상태)
|
||||||
|
- 원본 요구: `/home/donghyeon/workspace/tech-log-alignment-design/01-tech-log-frontend-alignment-design.md`
|
||||||
|
- Canonical 계약: `/home/donghyeon/workspace/tech-log-design-package/contracts/openapi/studio-v1.yaml`
|
||||||
|
- Specification Version `2.0.0`
|
||||||
|
- **digest·revision의 단일 기록처는 `src/features/tech-log/contracts/studio/canonical-source.json`이다.**
|
||||||
|
이 문서는 그 값을 복제하지 않는다. 승인 시점에 여기 적혀 있던
|
||||||
|
`sha256:85a65004…` / revision `0ec5582`은 구현 중 canonical yaml이 갱신되면서
|
||||||
|
무효가 됐고, 실제로 vendor·고정된 값은 `canonical-source.json`이 기록한
|
||||||
|
`sha256:99f54f56…` / revision `ce2e748`이다(Task 12 표 3행의 `ce2e748`과 동일).
|
||||||
|
- 계약 기여(`tech-log-studio-contract-contribution.ts`)는 그 파일을 import해서
|
||||||
|
`EXTERNAL_PACKAGE` provenance를 채우고, `check:tech-log-contract`가 vendor
|
||||||
|
사본·`generated.ts`와의 일치를 검증한다. 두 곳이 다시 갈라질 수 있는 지점은
|
||||||
|
없다.
|
||||||
|
- 결정: 현재 Public/Studio UI 기준선을 고정하고, Studio 계약·전송 경계와 Asset capability를 canonical 계약에 정합시킨다. Public 조회의 HTTP 전환은 이 사이클에서 제외한다.
|
||||||
|
|
||||||
|
### Task 12 완료 상태 (2026-08-18)
|
||||||
|
|
||||||
|
12개 Task 전부 `feature/techlog-backend-alignment`에 커밋됐다. 아래는 §완료 조건의 12개 항목을 Task 12 게이트 실행(전체 로그는
|
||||||
|
`.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-12-report.md`)과 Task 1–11이 기록한 구현 상태를 근거로 판정한 결과다.
|
||||||
|
|
||||||
|
| # | 완료 조건 | 판정 | 근거 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | 현재 Public UI·라우트가 변경되지 않는다 | 충족 | Public 화면 테스트(`public-document-screens.test.tsx` 등) 무변경 통과; `case-body-renderer.tsx`/`evidence-figure.tsx`는 `9e5fbd1..HEAD` 사이 diff가 비어 있어 렌더러 코드에 변경이 없다(`check:architecture`/`check:registries`는 import 그래프·레지스트리 정합만 보고 Public 렌더 출력을 관찰하지 않으므로 이 판정의 근거가 아니다). `test:visual`의 Public 스냅샷 실패는 이 브랜치가 아니라 `main`의 `79e9aa8`에서 물려받은 것이다(아래 §Task 12 참고) |
|
||||||
|
| 2 | 현재 Studio 작업 흐름이 변경되지 않는다 | 충족 | 기본 `MOCK`에서 `test:tech-log`(36 files/303 tests) 전부 PASS; `tech-log-studio-workflow.spec.ts` chromium 2/2 PASS |
|
||||||
|
| 3 | Studio 계약이 canonical에서 생성되고 digest 고정·drift 게이트 동작 | 충족 | `check:tech-log-contract`: "in sync: @tech-log/studio-contract@2.0.0 (ce2e748), 19 operations". 최종 fix wave에서 이 명령을 `config/ci/gates.json`의 FE-GATE-010과 `test:all`에 연결했다 — 그전까지는 손으로 칠 때만 실행돼 drift 게이트가 실질적으로 비어 있었다 |
|
||||||
|
| 4 | `StudioGateway` 전체 operation이 HTTP 어댑터로 구현·MSW 검증 | 충족 | `test:unit`/`test:integration`의 HTTP·MSW 계약 스위트 PASS (환경 요인 실패 1건 제외, 아래 참고) |
|
||||||
|
| 5 | WorkingCopy 저장이 Public Projection을 변경하지 않는다 | 충족 | `studio-publication-flow.test.tsx`, `public-document-screens.test.tsx` PASS |
|
||||||
|
| 6 | Validation/Preview/Publish가 version·dependency revision으로 묶인다 | 충족 | `studio-validation-preview.test.tsx` PASS |
|
||||||
|
| 7 | Publication Event/Snapshot 조회 가능, 과거 Snapshot 불변 | 충족 | `studio-publication-flow.test.tsx` PASS |
|
||||||
|
| 8 | Image/SVG 업로드 + Asset 기반 evidence 삽입 | 충족 | Task 10/11 Asset Picker·업로드 다이얼로그·Asset Library; `test:tech-log` 내 asset 관련 스위트 PASS |
|
||||||
|
| 9 | `READY` Asset만 Preview/Publish에 사용, `QUARANTINED` 미노출 | 충족 | Task 6/9가 구현; 관련 렌더러·게이트 테스트 PASS |
|
||||||
|
| 10 | 23개 오류 코드 + idempotency/version 충돌 구분 | 충족 | `error-classification.test.ts` 등 PASS |
|
||||||
|
| 11 | 프론트가 Backend 도메인 Aggregate를 복제하지 않는다 | 충족 | `check:architecture` PASS (415 modules, 전 import 해석, 12개 회귀 fixture PASS) |
|
||||||
|
| 12 | 런타임 스위치 `MOCK`/`HTTP` 전환, 기본 `MOCK`에서 기존 parity 스위트 전부 통과 | 충족 | 위 1·2 근거 + 수동 확인: `TECH_LOG_STUDIO_SOURCE=HTTP`에서 Backend 부재 시 Studio 쉘은 정상 렌더되고 패널은 "작업 흐름을 불러오지 못했습니다" 인라인 오류로 우아하게 저하됨(백지·미처리 예외 없음) |
|
||||||
|
|
||||||
|
명시적 비완료 항목 — 계획대로 이번 사이클에 포함되지 않는다:
|
||||||
|
|
||||||
|
- **실행 중 Backend와의 실응답 대조**: 이 환경에 Backend가 없다. Task 12 수동 확인은 "Backend 부재 시 우아한 오류 상태"까지만 검증했고, 실제 Backend 응답과의 대조는 Backend Studio 구현 완료 후 별도로 수행한다.
|
||||||
|
- **Public 조회의 HTTP 전환**: 범위에서 명시적으로 제외됐다(§범위 "제외 — Public 조회의 HTTP 전환"). `adapters/static/public-query.ts`는 이번 사이클에서 손대지 않았고, `public-v1.yaml` 기준 별도 spec/plan 사이클로 수행한다.
|
||||||
|
- **성공 payload의 런타임 계약 검증**: `tech-log-studio-contract-contribution.ts`의 18개 operation은 전부 `passthrough`(`z.unknown()`) `inputValidator`/`outputValidator`를 쓴다. 의도된 선택이고 코드에도 주석으로 남아 있다 — canonical 계약이 payload를 소유하고 `generated.ts`가 컴파일 시점 계약이며, 런타임 재검증은 계약 갱신 때마다 두 곳을 고치게 만든다. `problemValidator`는 그대로 엄격하다(23개 코드 enum + 필드 제약). **결과적으로 성공 응답의 shape 불일치로는 `CONTRACT_VIOLATION`이 발생할 수 없다.** 이 한계는 바로 위 "실행 중 Backend와의 실응답 대조 없음"과 같은 종류의 위험이다: Backend가 없으므로 MSW는 테스트 작성자가 적은 것을 그대로 돌려주고, 그것을 canonical 스키마와 대조하는 주체가 없다. 즉 shape 회귀를 잡을 수 있는 층이 지금은 컴파일 타임 한 겹뿐이다. Backend 대조 사이클에서 (a) 실서버 응답 대조로 대체할지 (b) canonical에서 생성한 런타임 스키마로 `outputValidator`를 채울지 함께 판단한다.
|
||||||
|
|
||||||
|
완료 조건 자체는 아니지만, 게이트 실행 중 확인된 사전 존재(pre-existing) 또는 환경적(environmental) 이슈:
|
||||||
|
|
||||||
|
- `tests/unit/ci-artifact-contract.test.ts` 16개 테스트가 `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted` 샌드박스 제약으로 실패한다. 브랜치 분기점 `9e5fbd1`에서도 동일하게 재현되는 환경 문제이며 이번 작업과 무관하다.
|
||||||
|
- `test:coverage`는 위 환경 실패 때문에 vitest가 non-zero로 종료해 `check-risk-coverage.ts`까지 도달하지 못한다(vitest 기본값 `coverage.reportOnFailure: false`). 그 파일만 제외한 진단 실행에서는 `src/application/policies/compatibility.ts`(re-export전용, 계측 가능한 statement 0개)와 `reference-http-gateway.ts`(statements 86.95%/branches 85%, 임계값 90%) 2건이 걸리는데, 둘 다 병합 지점(`9e5fbd1`) 이후 이 브랜치가 건드리지 않은 파일이다.
|
||||||
|
- `test:visual`(chromium): **최종 fix wave에서 130개 중 13개 실패로 정리했고, 남은 13개는 전부 merge-base `9e5fbd1`에서 동일하게 실패한다** — 이 브랜치가 만든 시각 회귀는 0건이다.
|
||||||
|
- **판정 근거는 추론이 아니라 실측이다.** merge-base `9e5fbd1`을 detached checkout해 `test:visual`을 그대로 실행했고(13 failed / 117 passed), 두 실행이 남긴 `-actual.png`를 SHA-256으로 대조했다. 13개는 merge-base와 HEAD의 실제 렌더 결과가 **바이트 동일**했다 — 즉 이 브랜치의 코드와 무관하다.
|
||||||
|
- **물려받은 13개(갱신하지 않음)**: `TECH_LOG_CASE` 1440, `known Public fixture /cases/collection-fetch-join-pagination` 1440, `TECH_LOG_STUDIO_DOCUMENT_PREVIEW` 1440, `TECH_LOG_STUDIO_PUBLICATION_PREVIEW` 1440, `Studio current-preview` 1440, `Studio publication-snapshot` 1440, `Studio immediate preview` 1440(이상 7개는 높이가 **줄었다**), `TECH_LOG_STUDIO_DOCUMENTS` 360/1440, `Studio document-list` 1440, `TECH_LOG_STUDIO_DOCUMENT_NEW` 1440, `Studio new-document` 1440(이상 5개는 크기 변화 없는 픽셀 차이), `TECH_LOG_STUDIO_DOCUMENT_NEW` 360(360×1000 → 360×1130). 높이가 줄어든 7개의 원인은 `main`에 이미 있고 merge-base `9e5fbd1`의 조상인 `79e9aa8`("fix: align TechLog article content widths")로, `.evidence-figure`의 CSS 폭을 `min(61rem, calc(100% + 15rem))`(≈912px)에서 `min(var(--body-copy), 100%)`(672px, 비율 73.6%)로 바꿨다. golden PNG는 `79e9aa8`보다 앞선 `3a7c5de`에서 마지막으로 기록됐다. 갱신은 이 브랜치가 아니라 `main`의 `79e9aa8`에 대해 기록해야 한다.
|
||||||
|
- **이전 기록의 정정 2건**: (1) "나머지 5개는 크기 변화 없는 픽셀 차이(추가 조사하지 않음)"로 남겨뒀던 항목은 조사 결과 **전부 물려받은 것**이다. (2) `TECH_LOG_STUDIO_DOCUMENT_NEW` 360(360×1000 → 360×1130)을 Asset Picker 때문이라고 원인 B로 분류했었는데, merge-base에서 **동일한 픽셀 수(28,413)로 동일하게 실패**한다 — 물려받은 것이다. 따라서 이 브랜치가 만든 실패는 6개가 아니라 5개다.
|
||||||
|
- **이 브랜치가 만들어 갱신한 5개**: `TECH_LOG_STUDIO_DOCUMENT_EDIT` 360(360×3313 → 360×3627), `TECH_LOG_STUDIO_DOCUMENT_EDIT` 1440·`Studio case-editor` 1440·`Studio conflict-editor` 1440·`Studio dirty-leave dialog` 1440(전부 1440×2706 → 1440×2999). 전부 Case 편집기 화면이고, 늘어난 293px 영역은 Task 10이 추가한 "EVIDENCE / 본문에 Asset 삽입" 패널(업로드 종류 select + `Asset 업로드` 버튼 + Picker 빈 상태)임을 갱신본에서 직접 확인했다. `--update-snapshots`는 이 5개 테스트에만 `--grep`으로 한정해 실행했다 — 일괄 갱신은 물려받은 13개까지 조용히 흡수해 이 구분을 없애기 때문이다.
|
||||||
|
- `test:e2e`/`test:a11y`는 chromium에서 전부 통과하고, firefox/webkit 실패는 이 환경의 브라우저 의존성 문제다(firefox: Pretendard 폰트의 "name records not sorted" 경고를 strict 콘솔 검사가 실패로 잡음; webkit: 호스트에 필요한 시스템 라이브러리 없음 — `playwright install-deps` 필요).
|
||||||
|
|
||||||
|
전체 명령·원문 출력은 `.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-12-report.md`에 기록했다.
|
||||||
|
|
||||||
|
## 목적
|
||||||
|
|
||||||
|
현재 TechLog 프론트엔드는 Studio를 세션 수명 `MockStudioGateway`로, Public을 정적 동기 catalog로 구동한다. 이 설계는 다음을 달성한다.
|
||||||
|
|
||||||
|
1. Studio HTTP 계약을 canonical `studio-v1.yaml` 단일 출처에서 생성하고, 두 계약이 다시 갈라지지 못하게 빌드로 막는다.
|
||||||
|
2. `StudioGateway`의 모든 operation을 canonical 계약 기준 HTTP 어댑터로 구현한다.
|
||||||
|
3. Backend에 이미 존재하는 Asset/Image/SVG capability를 프론트 편집 흐름에 연결한다.
|
||||||
|
4. 위 전부를 실행 중인 Backend 없이 완료하고, Backend가 완성되면 런타임 스위치만으로 대조할 수 있게 한다.
|
||||||
|
|
||||||
|
현재 Public UI 라우트·화면 구성과 Studio의 `작업본 → 편집 → 저장 → 검증 → Public Preview → 게시/재게시 → 게시 기록/Snapshot` 흐름은 변경하지 않는다.
|
||||||
|
|
||||||
|
## Source of Truth 우선순위
|
||||||
|
|
||||||
|
| 영역 | Source of Truth |
|
||||||
|
|---|---|
|
||||||
|
| Public 화면·라우트·사용 흐름 | 현재 `tech-log-frontend` |
|
||||||
|
| Studio 화면 구조·사용 흐름 | 현재 `tech-log-frontend` |
|
||||||
|
| Studio HTTP 계약 | `studio-v1.yaml` (단일 canonical) |
|
||||||
|
| Asset lifecycle·불변 조건 | `studio-v1.yaml` |
|
||||||
|
| 전송·오류·재시도·관측 규약 | 현재 프론트 플랫폼 (`src/contracts/external-contract-runtime.ts`) |
|
||||||
|
|
||||||
|
UI는 Backend 도메인 구조를 그대로 노출하지 않는다. 프론트의 `WorkingCopy`는 Backend Aggregate가 아니라 **편집 계약**이다.
|
||||||
|
|
||||||
|
## 확인된 사실과 정정
|
||||||
|
|
||||||
|
착수 전 검증에서 원본 요구 문서 및 설계 패키지 README와 실제 코드가 어긋나는 지점을 확인했다. 아래는 구현 기준으로 채택하는 정정이다.
|
||||||
|
|
||||||
|
### 이미 충족된 항목
|
||||||
|
|
||||||
|
- 설계 패키지 README는 프론트 격차로 "`RecordKind`에 `PROJECT_DECISION` 없음"을 든다. **이미 충족돼 있다** — `contracts/studio/studio-api.openapi.yaml:236`, `contracts/studio/generated.ts:200`. README가 지칭하는 대상은 구 `techlog-studio-frontend` 저장소다.
|
||||||
|
- `application/ports/studio-gateway.ts`의 operation 집합·필터·`IdempotentOptions`는 원본 요구 §4와 이미 일치한다. 포트 재설계가 아니라 구현체 교체가 필요하다.
|
||||||
|
|
||||||
|
### 실재하는 격차
|
||||||
|
|
||||||
|
- 프론트 계약에 `X-CSRF-TOKEN` 선언이 없다 (canonical 6회 참조, 프론트 0회). canonical은 모든 mutating operation에 CSRF를 요구한다.
|
||||||
|
- 프론트 계약의 `Idempotency-Key` 선언이 1회로, canonical(2회, 공용 parameter)과 구조가 다르다.
|
||||||
|
- canonical에는 프론트 계약에 없는 operation이 6개 있다: `getStudioSession`, `listStudioAssets`, `uploadStudioAsset`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset`. canonical 총 operation은 **19개**(프론트 현재 13개).
|
||||||
|
- canonical 오류 코드는 **23개**로, 원본 요구 §10의 21개에 `IDEMPOTENCY_KEY_REUSED`와 `WARNING_ACKNOWLEDGEMENT_REQUIRED`가 추가된다. 두 코드 모두 필요하다 — 전자는 §10이 요구하는 "version conflict와 idempotency replay 구분"의 실제 코드이고, 후자는 이미 존재하는 `warning-acknowledgements.tsx`가 처리해야 하는 거절 사유다. **23개 전부를 채택한다.**
|
||||||
|
- 원본 요구는 세션/CSRF를 다루지 않는다. canonical은 CSRF 토큰을 `getStudioSession`이 발급한다고 정한다. 아래 §"세션과 CSRF"에서 경계를 정한다.
|
||||||
|
|
||||||
|
### 플랫폼 제약
|
||||||
|
|
||||||
|
- `ContractContributionSource`의 `TEMPLATE_FIXTURE`는 타입상 `fixtureId: "REFERENCE_FEATURE_V1"`로 닫혀 있다(`external-contract-runtime.ts:186-190`). TechLog는 `EXTERNAL_PACKAGE`만 사용할 수 있다.
|
||||||
|
- 플랫폼 계약 런타임은 `requestBody: "NONE" | "JSON"`만 허용하고, 그 외를 구성 시점에 거절한다(`external-contract-runtime.ts:145`, `:407`). 저수준 `client.ts`도 본문을 `JSON.stringify`로 고정한다(`client.ts:719`). **`multipart/form-data`를 표현할 수 없다.**
|
||||||
|
- 기존 `browser-transfer` capability는 presigned `GET`/`PUT`과 resumable part 업로드용이다(`PresignedTransferMethod = "GET" | "PUT"`). canonical의 `POST /assets` multipart MVP 계약에 그대로 맞지 않는다.
|
||||||
|
- 설계 패키지의 `MANIFEST.sha256`은 stale하다(yaml은 2026-08-17 갱신, manifest는 08-11 기준이며 `preview-v1.yaml`을 아직 나열한다). 계약 고정은 manifest가 아니라 위 §상태에 기록한 실측 digest를 기준으로 한다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
| | 범위 |
|
||||||
|
|---|---|
|
||||||
|
| **A. 계약 정합** | canonical `studio-v1.yaml` 도입, 타입 생성 자동화, digest 고정, drift 게이트 |
|
||||||
|
| **B. Studio 전송** | 계약 기여(JSON 18개 전체) + `StudioGateway` HTTP 어댑터(14개 소비) + 세션/CSRF + 오류 매핑 + 런타임 스위치 |
|
||||||
|
| **C. Asset** | `StudioAssetGateway` 포트, 5개 operation(JSON 4 + multipart 1), Asset Library/Picker/Upload UI, `/studio/assets` 라우트, evidence directive 연결, alt/decorative 규칙 정정 |
|
||||||
|
|
||||||
|
### 제외 — Public 조회의 HTTP 전환
|
||||||
|
|
||||||
|
원본 요구 §11은 `adapters/static/public-query.ts`를 HTTP로 교체하도록 나열한다. 이 사이클에서 제외하고 별도 사이클로 분리한다.
|
||||||
|
|
||||||
|
근거:
|
||||||
|
|
||||||
|
- `PublicContentQueries`는 전 메서드가 **동기**이고, 프레젠테이션 19개 파일 31개 호출 지점이 렌더 중 직접 호출한다. async 전환은 전 Public 화면에 loading/error/empty 상태를 도입하는 작업이다.
|
||||||
|
- 방금 완료한 UI 이식의 시각·접근성 parity 기준선을 광범위하게 흔든다.
|
||||||
|
- 실행 중인 Public Backend가 없어 지금 전환해도 검증할 대상이 없고 사용자 가치도 없다.
|
||||||
|
- canonical `public-v1.yaml`(1,765줄)은 준비돼 있으므로, Public API가 실제로 서비스될 때 자체 spec/plan 사이클로 수행한다.
|
||||||
|
|
||||||
|
이 제외는 범위 축소가 아니라 순서 결정이다. 완료 조건에서 해당 항목을 별도로 명시한다.
|
||||||
|
|
||||||
|
## 선택한 접근
|
||||||
|
|
||||||
|
### A. 계약 정합 — digest로 고정된 단일 출처
|
||||||
|
|
||||||
|
canonical yaml을 저장소에 vendor하고, 타입을 생성하고, **계약 기여의 `EXTERNAL_PACKAGE` provenance로 canonical revision에 암호학적으로 고정**한다.
|
||||||
|
|
||||||
|
`InstalledContractPackageIdentity`(`external-contract-runtime.ts:173`)는 이미 이 목적에 맞는 필드를 요구한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
packageId @tech-log/studio-contract
|
||||||
|
version 2.0.0 (canonical info.version, exact SemVer)
|
||||||
|
digest canonical studio-v1.yaml의 SHA-256
|
||||||
|
runtimeProtocolVersion 1
|
||||||
|
sourceRevision 설계 패키지 git revision
|
||||||
|
```
|
||||||
|
|
||||||
|
`digest`·`sourceRevision`의 실제 값은 이 문서가 아니라
|
||||||
|
`contracts/studio/canonical-source.json`에 기록한다(§상태 참고). 계약 기여가 그
|
||||||
|
파일을 직접 읽으므로, 문서에 값을 복제하면 갱신을 한쪽에서만 하다가 어긋난다 —
|
||||||
|
승인본이 실제로 그렇게 어긋났다.
|
||||||
|
|
||||||
|
`assertPackageIdentity`는 shape을 검증하므로 npm 레지스트리 없이 지금 사용할 수 있다. 실제 패키지 배포로 승격할 때 같은 필드를 그대로 채운다.
|
||||||
|
|
||||||
|
drift 방지는 저장소 관례(`generate:*` / `check:*`)를 따르는 스크립트 한 쌍으로 강제한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
generate:tech-log-contract canonical yaml → vendor 사본 + generated.ts + digest 기록
|
||||||
|
check:tech-log-contract 재생성 결과가 커밋 내용과 바이트 동일한지, digest가 계약 기여의
|
||||||
|
선언과 일치하는지 검증. 불일치 시 실패.
|
||||||
|
```
|
||||||
|
|
||||||
|
`openapi-typescript`를 devDependency로 고정한다. 현재 `generated.ts`는 이 도구로 만들어졌으나 도구도 스크립트도 저장소에 없어 재생성이 불가능하다 — 이것이 두 계약이 갈라진 근본 원인이다.
|
||||||
|
|
||||||
|
`X-CSRF-TOKEN`과 공용 `Idempotency-Key` parameter는 생성 결과에 자동 반영된다. 프론트 yaml을 손으로 고치지 않는다.
|
||||||
|
|
||||||
|
### B. Studio 전송 — 플랫폼 계약 런타임 사용
|
||||||
|
|
||||||
|
계약 기여는 서비스 패키지당 하나이므로, `features/tech-log/contracts/tech-log-studio-contract-contribution.ts` 한 파일에 canonical의 **JSON operation 18개 전부**(19개 중 multipart 업로드 제외)를 선언한다. Asset의 JSON operation 4개도 같은 기여에 속한다 — 같은 `studio-v1` 패키지이기 때문이다. 이 중 `StudioGateway`가 14개를, `StudioAssetGateway`가 4개를 소비한다.
|
||||||
|
|
||||||
|
선언 형식은 `reference-feature-contract-contribution.ts`가 확립한 것을 따른다: operation별 `inputValidator`/`outputValidator`/`problemValidator`, `acceptedStatuses`, `retrySemantics`, `commandRecovery`, `commandEffect`, `projectRequest`, 그리고 byte limit·deadline·retry budget·diagnostics 이름.
|
||||||
|
|
||||||
|
`retrySemantics`는 canonical의 안전성 구분을 그대로 반영한다. 조회는 `SAFE`, mutating operation은 `KEYED`이며 `commandRecovery.mode = "IDEMPOTENCY_REPLAY"`, retry budget은 0이다 — 발신된 KEYED 명령의 자동 재시도는 금지된다.
|
||||||
|
|
||||||
|
`StudioGateway` 구현체는 `contractOperations` executor 위에 얹고, 포트 시그니처는 변경하지 않는다. UI는 어댑터가 mock인지 HTTP인지 알지 못한다.
|
||||||
|
|
||||||
|
#### 세션과 CSRF
|
||||||
|
|
||||||
|
CSRF는 전송 관심사이며 UI 관심사가 아니다. 현재 Studio에는 인증 UI가 없고(이식 시 유보), 이 사이클에서도 추가하지 않는다.
|
||||||
|
|
||||||
|
따라서 `getStudioSession`을 포트로 노출하지 않는다. HTTP 어댑터 내부가 첫 mutating 요청 전에 세션을 조회해 CSRF 토큰을 캐시하고, `csrfHeaderName`으로 헤더를 붙인다. `401`/`403`은 기존 오류 경로로 흘려보낸다. `displayName`/`roles`는 이 사이클에서 소비하지 않는다.
|
||||||
|
|
||||||
|
이 결정으로 완료 조건 "현재 Studio 작업 흐름이 변경되지 않는다"가 유지된다.
|
||||||
|
|
||||||
|
#### 런타임 스위치
|
||||||
|
|
||||||
|
`config/runtime/*.json`에 스위치를 추가한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
TECH_LOG_STUDIO_SOURCE: "MOCK" | "HTTP"
|
||||||
|
|
||||||
|
local, development → MOCK (기본값)
|
||||||
|
staging, production → HTTP
|
||||||
|
```
|
||||||
|
|
||||||
|
`createTechLogFeatureInstalledInput`이 이 값으로 gateway factory를 고른다. mock은 삭제하지 않고 test fixture 겸 fallback으로 유지한다. Backend 완성 시 `local.json` 한 줄로 대조를 시작한다.
|
||||||
|
|
||||||
|
기본값을 `MOCK`으로 두는 이유는 현재 앱과 이식 parity 테스트가 그대로 통과해야 하기 때문이다.
|
||||||
|
|
||||||
|
### C. Asset — 포트 분리와 업로드 전송 경계
|
||||||
|
|
||||||
|
`StudioAssetGateway`를 `StudioGateway`와 별도 포트로 둔다. 파일 전송과 JSON orchestration의 실패 모델이 다르고, 향후 presigned/resumable 교체가 이 포트 뒤에서 끝나야 한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
StudioAssetGateway
|
||||||
|
listAssets(query, options) GET /api/v1/studio/assets
|
||||||
|
uploadAsset(form, options) POST /api/v1/studio/assets (multipart)
|
||||||
|
getAsset(assetId, options) GET /api/v1/studio/assets/{assetId}
|
||||||
|
updateAssetMetadata(assetId, cmd, o) PUT /api/v1/studio/assets/{assetId}
|
||||||
|
deleteAsset(assetId, options) DELETE /api/v1/studio/assets/{assetId}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 업로드 전송
|
||||||
|
|
||||||
|
플랫폼 계약 런타임이 multipart를 표현할 수 없으므로, 업로드 한 operation만 전용 전송 seam으로 분리한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
StudioAssetUploadTransport (좁은 인터페이스: form + 헤더 → 결과)
|
||||||
|
└ fetch + FormData 구현체
|
||||||
|
런타임 config의 API_BASE_URL·타임아웃 재사용
|
||||||
|
CSRF·Idempotency-Key 헤더는 B와 동일 경로로 획득
|
||||||
|
오류는 B와 동일한 코드 매핑 테이블 사용
|
||||||
|
```
|
||||||
|
|
||||||
|
나머지 4개 JSON operation은 A/B와 같은 계약 런타임을 통과한다. 즉 계약 런타임을 우회하는 것은 **19개 중 1개**다.
|
||||||
|
|
||||||
|
이 경계를 명시적 seam으로 두는 이유는 §7.4의 교체 가능성 요구를 만족시키기 위해서다. 플랫폼에 `MULTIPART` 모드가 생기거나 presigned로 옮길 때 구현체 한 파일만 바뀐다. 플랫폼 파일(`external-contract-runtime.ts`, `client.ts`)은 template 동기화 대상이므로 이 사이클에서 수정하지 않는다.
|
||||||
|
|
||||||
|
결정 사유와 우회 범위는 `docs/reviews/adapters/`에 기록한다.
|
||||||
|
|
||||||
|
#### UI 접근점
|
||||||
|
|
||||||
|
Studio primary navigation을 Asset 중심 CMS로 되돌리지 않는다. 두 접근점을 둔다.
|
||||||
|
|
||||||
|
1. **Editor contextual Asset Picker** — 작성 흐름의 기본 진입점. 선택 시 evidence directive를 삽입한다.
|
||||||
|
2. **`/studio/assets` Asset Library** — 검색·메타데이터·사용처·정리용 보조 화면. navigation에는 secondary utility link로만 노출한다.
|
||||||
|
|
||||||
|
`/studio/assets`는 `tech-log-route-contract.ts`에 `layoutGroup: "STUDIO"`로 추가한다(현재 27개 → 28개). 라우트 registry 거버넌스 기준선을 함께 갱신한다.
|
||||||
|
|
||||||
|
#### Evidence Figure와 Asset 연결
|
||||||
|
|
||||||
|
현재 content format directive를 유지한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
:::evidence key="asset-key" alt="설명" caption="캡션" zoom="true"
|
||||||
|
```
|
||||||
|
|
||||||
|
변경점:
|
||||||
|
|
||||||
|
- 정적 `evidenceAssets` 레지스트리(`adapters/static/evidence-assets.ts`) 대신 Backend Asset의 `assetKey`를 사용한다. `assetKey`는 canonical에서 immutable이며 공개 이력 이후 재사용이 금지된 안정 key다.
|
||||||
|
- Asset Picker 선택 시 directive를 자동 삽입한다. 사용자가 raw object-storage URL을 Markdown에 직접 넣지 않게 한다.
|
||||||
|
- 렌더러는 API가 제공한 Asset descriptor를 resolver로 주입받는다. 렌더러 경계는 변경하지 않는다.
|
||||||
|
- `QUARANTINED` Asset은 Public/Preview에 렌더링하지 않는다.
|
||||||
|
- 사용자 업로드 SVG 원문을 `innerHTML`로 주입하지 않는다. 검증된 `publicPath`를 `<img src>`로 렌더링한다.
|
||||||
|
|
||||||
|
정적 `evidenceAssets`는 기존 하드코딩 Case 화면의 parity 유지를 위해 fixture로 남기고, Asset 기반 경로와 공존시킨다.
|
||||||
|
|
||||||
|
#### alt / decorative 규칙 정정
|
||||||
|
|
||||||
|
현재 parser는 빈 alt를 syntax error로 차단한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
parse-case-content.ts:505
|
||||||
|
if (!attributes.alt) invalid(node, "evidence alt text is required");
|
||||||
|
```
|
||||||
|
|
||||||
|
Asset capability와 연결하면 이 위치에서는 판단할 수 없다 — 필요 여부가 Asset의 `decorative`에 달려 있다. 규칙을 옮긴다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
parser 빈 alt를 문법 오류로 차단하지 않는다 (구조만 검증)
|
||||||
|
publish 검증 Asset decorative=false + 사용 위치 alt 비어 있음 → ERROR
|
||||||
|
Asset decorative=true → alt="" 허용
|
||||||
|
```
|
||||||
|
|
||||||
|
이는 content format의 **의미 변경**이므로 parser/serializer round-trip 픽스처와 기존 검증 테스트를 함께 갱신한다.
|
||||||
|
|
||||||
|
## 대상 아키텍처
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/features/tech-log/
|
||||||
|
├── contracts/
|
||||||
|
│ ├── studio/
|
||||||
|
│ │ ├── studio-api.openapi.yaml canonical vendor 사본 (생성물, 수동 편집 금지)
|
||||||
|
│ │ ├── generated.ts 생성물
|
||||||
|
│ │ ├── contract.ts 타입 alias (Asset 계열 추가)
|
||||||
|
│ │ └── canonical-source.json digest·revision 기록
|
||||||
|
│ └── tech-log-studio-contract-contribution.ts JSON operation 18개 + EXTERNAL_PACKAGE provenance
|
||||||
|
├── application/ports/
|
||||||
|
│ ├── studio-gateway.ts 변경 없음
|
||||||
|
│ └── studio-asset-gateway.ts 신규
|
||||||
|
├── adapters/
|
||||||
|
│ ├── http/
|
||||||
|
│ │ ├── http-studio-gateway.ts contractOperations 위 구현
|
||||||
|
│ │ ├── http-studio-asset-gateway.ts JSON 4 + 업로드 위임
|
||||||
|
│ │ ├── asset-upload-transport.ts multipart seam
|
||||||
|
│ │ ├── studio-session-csrf.ts CSRF 토큰 획득·캐시
|
||||||
|
│ │ └── studio-error-mapping.ts 23 코드 → 플랫폼 FailureKind
|
||||||
|
│ ├── mock/ 유지 (fixture·fallback)
|
||||||
|
│ └── static/ 유지 (Public, 이 사이클 범위 외)
|
||||||
|
└── presentation/studio/
|
||||||
|
├── pages/assets-page.tsx 신규
|
||||||
|
└── components/
|
||||||
|
├── asset-library.tsx 신규
|
||||||
|
├── asset-picker.tsx 신규
|
||||||
|
└── asset-upload-dialog.tsx 신규
|
||||||
|
```
|
||||||
|
|
||||||
|
기존 유지 대상: `presentation/public/**`, `presentation/studio/pages/**`, `document-*`, `validation-*`, `public-preview-screen`, `publish-screen`, `publication-*`, `presentation/shared/public-render/**`.
|
||||||
|
|
||||||
|
## 오류·동시성 계약
|
||||||
|
|
||||||
|
23개 canonical 코드를 전부 처리한다. 매핑은 `studio-error-mapping.ts` 한 곳에 둔다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
AUTHENTICATION_REQUIRED STUDIO_ACCESS_DENIED
|
||||||
|
DOCUMENT_NOT_FOUND VERSION_CONFLICT
|
||||||
|
REQUEST_VALIDATION_FAILED VALIDATION_FAILED
|
||||||
|
VALIDATION_STALE PREVIEW_NOT_FOUND
|
||||||
|
PREVIEW_STALE PREVIEW_EXPIRED
|
||||||
|
PUBLICATION_NOT_FOUND PUBLICATION_CONFLICT
|
||||||
|
PUBLICATION_EVENT_NOT_FOUND PUBLICATION_SNAPSHOT_NOT_FOUND
|
||||||
|
IDEMPOTENCY_KEY_REUSED WARNING_ACKNOWLEDGEMENT_REQUIRED
|
||||||
|
ASSET_NOT_FOUND ASSET_NOT_READY
|
||||||
|
ASSET_IN_USE ASSET_QUARANTINED
|
||||||
|
PAYLOAD_TOO_LARGE UNSUPPORTED_MEDIA_TYPE
|
||||||
|
STUDIO_UNAVAILABLE
|
||||||
|
```
|
||||||
|
|
||||||
|
규칙:
|
||||||
|
|
||||||
|
- 모든 mutation은 `Idempotency-Key`를 보낸다. 명령 하나당 새 key를 만들고, 그 명령의 안전한 재시도에만 같은 key를 재사용한다.
|
||||||
|
- `VERSION_CONFLICT`(낙관적 잠금 실패)와 `IDEMPOTENCY_KEY_REUSED`(같은 key·다른 요청)를 혼동하지 않는다. 전자는 사용자 데이터 손실 없는 충돌 화면으로, 후자는 클라이언트 결함으로 다룬다.
|
||||||
|
- `Idempotency-Replayed` 응답 헤더를 replay 판별에 사용한다.
|
||||||
|
- workflow 상태(`publicationStatus`, `hasUnpublishedChanges`, `nextAction`)는 서버가 계산한 값을 그대로 신뢰한다. 프론트에서 재계산하지 않는다.
|
||||||
|
- 업로드 상태는 `선택 실패 / 업로드 중 / 전송 실패 / READY / REJECTED / QUARANTINED / 크기 초과 / 미지원 형식`을 구분한다. 업로드 전송 성공과 서버 검증 성공을 분리한다.
|
||||||
|
- Asset은 `READY`일 때만 Preview/Publish에 사용한다.
|
||||||
|
|
||||||
|
## 테스트 전략
|
||||||
|
|
||||||
|
TDD로 진행한다. 각 단위는 red → green → 게이트 순서를 지킨다.
|
||||||
|
|
||||||
|
**계약(A)**
|
||||||
|
|
||||||
|
- `check:tech-log-contract`가 vendor 사본·`generated.ts`·digest 불일치를 잡는다 (의도적 변조로 red 확인).
|
||||||
|
- canonical 19개 operationId가 전부 덮이는지 parity 검증: 18개는 계약 기여에, `uploadStudioAsset`은 업로드 전송 seam에 존재해야 한다. 어느 쪽에도 없는 operationId가 있으면 실패한다.
|
||||||
|
- `EXTERNAL_PACKAGE` identity가 `assertPackageIdentity`를 통과하고 `contractSet`에 나타나는지 확인.
|
||||||
|
|
||||||
|
**전송(B)** — MSW로 canonical 응답·오류를 재현
|
||||||
|
|
||||||
|
- 불완전 draft 저장 성공, `expectedVersion` 충돌 → `VERSION_CONFLICT`.
|
||||||
|
- idempotency replay(`Idempotency-Replayed: true`)와 `IDEMPOTENCY_KEY_REUSED` 구분.
|
||||||
|
- workflow 전이: `INVALID → FIX_VALIDATION`, `VALID/WARNINGS → CREATE_PREVIEW`, `CURRENT → PUBLISH`, 게시 후 `NONE`, 편집 후 `VALIDATE` 복귀, expired preview 재생성.
|
||||||
|
- Publication: 최초 `PUBLISHED`, 재게시 `REPUBLISHED`, 취소 `UNPUBLISHED`, 과거 Snapshot 불변성.
|
||||||
|
- 23개 코드 전부의 UI 관측 가능한 처리.
|
||||||
|
- CSRF 토큰 획득 실패·만료 경로.
|
||||||
|
- 런타임 스위치: `MOCK`/`HTTP` 각각에서 gateway 종류가 선택되는지.
|
||||||
|
|
||||||
|
**Asset(C)**
|
||||||
|
|
||||||
|
- PNG/JPEG/WebP/SVG 업로드 성공, 미지원 형식 `UNSUPPORTED_MEDIA_TYPE`, 크기 초과 `PAYLOAD_TOO_LARGE`.
|
||||||
|
- `decorative=false` + 빈 alt → publish ERROR / `decorative=true` + `alt=""` 허용.
|
||||||
|
- `QUARANTINED` Asset의 Public·Preview 렌더링 차단.
|
||||||
|
- 사용 중 Asset hard delete 차단 → `ASSET_IN_USE`, `ARCHIVED` 전환 경로.
|
||||||
|
- Asset Picker가 evidence directive를 정확한 문법으로 삽입.
|
||||||
|
- parser가 빈 alt를 더 이상 syntax error로 차단하지 않음 + round-trip 픽스처 갱신.
|
||||||
|
|
||||||
|
**렌더러 불변**
|
||||||
|
|
||||||
|
동일 픽스처에 대해 `Instant Preview`, `Server Public Preview`, `Published Public`, `Publication Snapshot`의 semantic output이 동일해야 한다. Asset resolver도 같은 렌더러 경계로 주입한다.
|
||||||
|
|
||||||
|
**회귀**
|
||||||
|
|
||||||
|
기존 이식 parity 스위트(시각·접근성·라우트·아키텍처 경계)가 전부 통과해야 한다. 기본 스위치가 `MOCK`이므로 이 스위트는 영향받지 않아야 한다.
|
||||||
|
|
||||||
|
## 위험과 완화
|
||||||
|
|
||||||
|
| 위험 | 완화 |
|
||||||
|
|---|---|
|
||||||
|
| 실행 중 Backend 없음 → 실응답 미검증 | canonical 계약 기준 MSW 검증. 스위치로 대조 지점을 남긴다. 완료 조건에 "실서버 대조 미포함"을 명시한다. |
|
||||||
|
| canonical yaml이 계속 변경 중 (오늘도 수정됨) | digest·revision을 커밋에 고정하고 drift 게이트로 감지. canonical 갱신은 의도적 재생성 커밋으로만 반영. |
|
||||||
|
| 계약 런타임 우회(업로드 1개)가 거버넌스 위반으로 보일 수 있음 | 좁은 seam으로 격리, 사유·범위를 adapter review 문서에 기록, 나머지 18개는 런타임 통과. |
|
||||||
|
| content format 의미 변경(alt)이 기존 픽스처를 깨뜨림 | parser 변경과 픽스처·검증 갱신을 한 단위로 묶어 red→green으로 수행. |
|
||||||
|
| 라우트 1개 추가가 registry 기준선을 깨뜨림 | 거버넌스 기준선 갱신을 같은 단위에 포함. |
|
||||||
|
| `openapi-typescript` 도입이 생성물 diff를 크게 만듦 | 첫 생성 결과를 별도 커밋으로 분리해 리뷰 가능하게 한다. |
|
||||||
|
|
||||||
|
## 완료 조건
|
||||||
|
|
||||||
|
1. 현재 Public UI·라우트가 변경되지 않는다.
|
||||||
|
2. 현재 Studio 작업 흐름이 변경되지 않는다.
|
||||||
|
3. Studio 계약이 canonical `studio-v1.yaml`에서 생성되고, digest 고정과 drift 게이트가 동작한다.
|
||||||
|
4. `StudioGateway`의 모든 operation이 HTTP 어댑터로 구현되고 MSW 계약 테스트로 검증된다.
|
||||||
|
5. WorkingCopy 저장이 Public Projection을 변경하지 않는다.
|
||||||
|
6. Validation/Preview/Publish가 version과 dependency revision으로 묶인다.
|
||||||
|
7. Publication Event와 Snapshot을 현재 UI에서 조회할 수 있고 과거 Snapshot이 불변이다.
|
||||||
|
8. Image/SVG를 업로드하고 Case content에 Asset 기반 evidence로 삽입할 수 있다.
|
||||||
|
9. Asset은 `READY`일 때만 Preview/Publish에 사용된다. `QUARANTINED`는 렌더링되지 않는다.
|
||||||
|
10. 23개 오류 코드와 idempotency/version 충돌 구분이 처리된다.
|
||||||
|
11. 프론트가 Backend 도메인 Aggregate를 복제하지 않는다.
|
||||||
|
12. 런타임 스위치로 `MOCK`/`HTTP`를 전환할 수 있고, 기본 `MOCK`에서 기존 parity 스위트가 전부 통과한다.
|
||||||
|
|
||||||
|
명시적 비완료 항목:
|
||||||
|
|
||||||
|
- 실행 중 Backend와의 실응답 대조. Backend Studio 구현 완료 후 별도로 수행한다.
|
||||||
|
- Public 조회의 HTTP 전환. `public-v1.yaml` 기준 별도 spec/plan 사이클로 수행한다.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# TechLog 본문·콘텐츠 자료 너비 정렬 설계
|
||||||
|
|
||||||
|
## 상태
|
||||||
|
|
||||||
|
- 승인일: 2026-08-17
|
||||||
|
- 우선순위: Decision 작성 기능보다 먼저 적용
|
||||||
|
- 결정: 코드 블록, 데이터 표, SVG·이미지 evidence를 본문과 같은 `42rem` 너비에 맞춘다.
|
||||||
|
|
||||||
|
## 문제
|
||||||
|
|
||||||
|
Public 문서 본문은 `--body-copy: 42rem`이지만 코드 블록과 데이터 표는 최대 `58rem`, evidence figure는 최대 `61rem`으로 가운데 돌출된다. 이 때문에 본문 문장과 자료의 좌우 경계가 달라지고 문서를 읽을 때 시선축이 흔들린다.
|
||||||
|
|
||||||
|
## 선택한 접근
|
||||||
|
|
||||||
|
본문의 읽기 너비는 유지하고 자료 쪽을 본문 너비에 맞춘다.
|
||||||
|
|
||||||
|
- `.code-block`, `.data-table-wrap`, `.evidence-figure`의 기본 너비를 `min(var(--body-copy), 100%)`로 통일한다.
|
||||||
|
- 세 요소의 좌우 중앙 정렬은 일반 `margin-inline: auto`로 표현하고, 폭을 넓히기 위한 `50%` 이동과 `translateX`를 제거한다.
|
||||||
|
- 긴 코드는 기존처럼 `pre` 내부에서 가로 스크롤한다.
|
||||||
|
- 넓은 표는 기존처럼 wrapper 내부에서 가로 스크롤한다.
|
||||||
|
- SVG·이미지는 비율을 유지해 컨테이너 너비에 맞추고 기존 확대 dialog를 유지한다.
|
||||||
|
- 작은 화면에서는 세 자료가 계속 `width: 100%`를 사용한다.
|
||||||
|
|
||||||
|
본문 자체를 `58rem` 이상으로 넓히는 접근은 긴 문장의 가독성을 바꾸므로 사용하지 않는다. 코드·이미지만 계속 돌출시키는 접근도 이번 문제를 유지하므로 사용하지 않는다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
다음 표면에 동일하게 적용한다.
|
||||||
|
|
||||||
|
- Case 공개 문서
|
||||||
|
- Studio 즉시 미리보기
|
||||||
|
- 저장·검증된 Public preview
|
||||||
|
- 게시 snapshot preview
|
||||||
|
- 공유 Public renderer를 사용하는 모든 코드, 표, evidence figure
|
||||||
|
|
||||||
|
다음은 이번 변경에 포함하지 않는다.
|
||||||
|
|
||||||
|
- 본문 글꼴, 행간, `--body-copy` 값 변경
|
||||||
|
- 이미지 업로드와 evidence catalog 자동 등록
|
||||||
|
- TOC 위치와 문서 전체 shell 너비 변경
|
||||||
|
- Decision 작성 기능
|
||||||
|
|
||||||
|
## 반응형·접근성
|
||||||
|
|
||||||
|
- 데스크톱에서 본문과 자료의 좌우 경계가 같아야 한다.
|
||||||
|
- 모바일에서는 viewport를 넘지 않아야 한다.
|
||||||
|
- 코드와 표의 가로 스크롤 가능성을 유지한다.
|
||||||
|
- evidence 확대 버튼과 keyboard focus 동작을 유지한다.
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
먼저 style contract에 본문, 코드, 표, evidence figure의 계산된 너비 규칙이 모두 `min(var(--body-copy), 100%)`인지 확인하는 실패 테스트를 추가한다. 그 후 최소 CSS 변경으로 통과시킨다.
|
||||||
|
|
||||||
|
공유 renderer 회귀 테스트로 코드의 `pre` overflow, 표 wrapper overflow, evidence 확대 control이 그대로 존재하는지 확인한다. 마지막으로 Case 문서를 데스크톱과 모바일에서 확인해 자료가 본문 경계와 정렬되고 viewport overflow가 없는지 검증한다.
|
||||||
@@ -382,6 +382,194 @@ const browserDataBoundaryPlugin = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DOM element rendered by React carries `__reactFiber$*` / `__reactProps$*`
|
||||||
|
* as *own enumerable* properties. `node:assert` builds its `AssertionError`
|
||||||
|
* eagerly, running `util.inspect` over both operands with `depth: 1000`,
|
||||||
|
* `getters: true` and `maxArrayLength: Infinity`; the fiber graph re-expands
|
||||||
|
* once per traversal path, so inspecting one rendered element allocates
|
||||||
|
* gigabytes and the worker dies before any `AssertionError` is ever thrown.
|
||||||
|
* A genuine regression then reports as an OOM or an opaque timeout instead of
|
||||||
|
* a failed assertion. Measured on this repo's Asset Library heading:
|
||||||
|
* depth 6 = 1.3MB, depth 8 = 7.8MB, depth 10 = 36MB, depth 12 = 135MB.
|
||||||
|
*
|
||||||
|
* Equality assertions only inspect their operands on failure, so an unsafe
|
||||||
|
* comparison stays invisible while green and detonates the day the behaviour
|
||||||
|
* it guards regresses -- which is exactly when the diagnosis is needed.
|
||||||
|
*
|
||||||
|
* `expect` is not affected: vitest prints and diffs through pretty-format's
|
||||||
|
* DOM plugin, which reads tag/attributes/children and never touches the fiber.
|
||||||
|
* So the safe form is always an `expect` matcher -- `toHaveFocus()`,
|
||||||
|
* `not.toBeInTheDocument()`, `toBe(element)` -- and this rule only forbids
|
||||||
|
* handing a DOM element to `node:assert`.
|
||||||
|
*/
|
||||||
|
const TESTING_LIBRARY_QUERY =
|
||||||
|
/^(get|query|find)(All)?By(Role|Text|LabelText|PlaceholderText|AltText|Title|DisplayValue|TestId)$/u;
|
||||||
|
const domQueryMethods = new Set([
|
||||||
|
"querySelector",
|
||||||
|
"querySelectorAll",
|
||||||
|
"getElementById",
|
||||||
|
"closest",
|
||||||
|
]);
|
||||||
|
const domElementProperties = new Set([
|
||||||
|
"activeElement",
|
||||||
|
"parentElement",
|
||||||
|
"firstElementChild",
|
||||||
|
"lastElementChild",
|
||||||
|
"nextElementSibling",
|
||||||
|
"previousElementSibling",
|
||||||
|
"offsetParent",
|
||||||
|
]);
|
||||||
|
// Fail when the operands *differ*, so on failure at least one element is
|
||||||
|
// still there to be inspected.
|
||||||
|
const positiveAssertEqualities = new Set([
|
||||||
|
"equal",
|
||||||
|
"strictEqual",
|
||||||
|
"deepEqual",
|
||||||
|
"deepStrictEqual",
|
||||||
|
]);
|
||||||
|
// Fail when the operands *match*. `assert.notEqual(element, null)` can only
|
||||||
|
// fail with `null` on both sides, so a nullish literal operand makes these
|
||||||
|
// safe; anything else leaves an element to inspect.
|
||||||
|
const negativeAssertEqualities = new Set([
|
||||||
|
"notEqual",
|
||||||
|
"notStrictEqual",
|
||||||
|
"notDeepEqual",
|
||||||
|
"notDeepStrictEqual",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const noElementOperandEqualityRule: Rule.RuleModule = {
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
unbounded:
|
||||||
|
"node:assert inspects both operands at depth 1000 to build its failure message, and a React-rendered element's __reactFiber$* graph exhausts the worker heap there, so the regression reports as an OOM instead of an assertion. Use an expect matcher instead -- expect(el).toHaveFocus(), expect(el).not.toBeInTheDocument(), expect(actual).toBe(expected) -- which prints DOM nodes through pretty-format's DOM plugin.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const sourceCode = context.sourceCode;
|
||||||
|
|
||||||
|
const unwrap = (input: any): any => {
|
||||||
|
let node = input;
|
||||||
|
while (
|
||||||
|
node &&
|
||||||
|
[
|
||||||
|
"AwaitExpression",
|
||||||
|
"ChainExpression",
|
||||||
|
"TSAsExpression",
|
||||||
|
"TSNonNullExpression",
|
||||||
|
"TSSatisfiesExpression",
|
||||||
|
"TSTypeAssertion",
|
||||||
|
].includes(node.type)
|
||||||
|
) {
|
||||||
|
node = node.type === "AwaitExpression" ? node.argument : node.expression;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const memberName = (node: any): string | null => {
|
||||||
|
if (!node.computed && node.property?.type === "Identifier") {
|
||||||
|
return node.property.name;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
node.computed &&
|
||||||
|
(node.property?.type === "Literal" ||
|
||||||
|
node.property?.type === "StringLiteral") &&
|
||||||
|
typeof node.property.value === "string"
|
||||||
|
) {
|
||||||
|
return node.property.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveInit = (node: any): any => {
|
||||||
|
const scope = sourceCode.getScope(node);
|
||||||
|
let current: any = scope;
|
||||||
|
while (current) {
|
||||||
|
const variable = current.variables.find(
|
||||||
|
(entry: any) => entry.name === node.name,
|
||||||
|
);
|
||||||
|
if (variable) {
|
||||||
|
const definition = variable.defs.at(-1);
|
||||||
|
return definition?.node?.type === "VariableDeclarator"
|
||||||
|
? definition.node.init
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
current = current.upper;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isElementValued = (input: any, seen = new Set<any>()): boolean => {
|
||||||
|
const node = unwrap(input);
|
||||||
|
if (!node || seen.has(node)) return false;
|
||||||
|
seen.add(node);
|
||||||
|
if (node.type === "MemberExpression") {
|
||||||
|
const name = memberName(node);
|
||||||
|
return name !== null && domElementProperties.has(name);
|
||||||
|
}
|
||||||
|
if (node.type === "CallExpression") {
|
||||||
|
const callee = unwrap(node.callee);
|
||||||
|
if (callee?.type !== "MemberExpression") return false;
|
||||||
|
const name = memberName(callee);
|
||||||
|
return (
|
||||||
|
name !== null &&
|
||||||
|
(TESTING_LIBRARY_QUERY.test(name) || domQueryMethods.has(name))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (node.type === "Identifier") {
|
||||||
|
return isElementValued(resolveInit(node), seen);
|
||||||
|
}
|
||||||
|
if (node.type === "ConditionalExpression") {
|
||||||
|
return (
|
||||||
|
isElementValued(node.consequent, seen) ||
|
||||||
|
isElementValued(node.alternate, seen)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNullish = (input: any): boolean => {
|
||||||
|
const node = unwrap(input);
|
||||||
|
if (!node) return false;
|
||||||
|
return (
|
||||||
|
(node.type === "Literal" && node.value === null) ||
|
||||||
|
(node.type === "Identifier" && node.name === "undefined")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
CallExpression(node: any) {
|
||||||
|
const callee = unwrap(node.callee);
|
||||||
|
if (callee?.type !== "MemberExpression") return;
|
||||||
|
const object = unwrap(callee.object);
|
||||||
|
if (object?.type !== "Identifier" || object.name !== "assert") return;
|
||||||
|
|
||||||
|
const name = memberName(callee);
|
||||||
|
if (name === null) return;
|
||||||
|
const positive = positiveAssertEqualities.has(name);
|
||||||
|
if (!positive && !negativeAssertEqualities.has(name)) return;
|
||||||
|
|
||||||
|
const operands = (node.arguments ?? []).slice(0, 2);
|
||||||
|
if (!positive && operands.some((argument: any) => isNullish(argument))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const operand = operands.find((argument: any) =>
|
||||||
|
isElementValued(argument),
|
||||||
|
);
|
||||||
|
if (operand) context.report({ node: operand, messageId: "unbounded" });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const testAssertionBoundaryPlugin = {
|
||||||
|
rules: {
|
||||||
|
"no-element-operand-equality": noElementOperandEqualityRule,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const commonLanguageOptions = {
|
const commonLanguageOptions = {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
@@ -406,6 +594,20 @@ const commonSecurityRules = {
|
|||||||
"CallExpression[callee.object.name='document'][callee.property.name='createElement'][arguments.0.value='script']",
|
"CallExpression[callee.object.name='document'][callee.property.name='createElement'][arguments.0.value='script']",
|
||||||
message: "Runtime script construction is prohibited by FE-OC-019.",
|
message: "Runtime script construction is prohibited by FE-OC-019.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
setState 업데이터는 핸들러가 끝난 뒤, 다음 렌더에 실행된다. 그때 React 는 이미
|
||||||
|
`event.currentTarget` 을 null 로 되돌려 놓았으므로 업데이터 안에서 그것을 읽으면
|
||||||
|
"Cannot read properties of null" 로 화면이 통째로 죽는다.
|
||||||
|
|
||||||
|
타입 검사도 lint 도 잡지 못했고, 첫 입력에서야 드러났다 — 값은 핸들러가 도는 동안
|
||||||
|
지역 변수로 꺼내 두고 업데이터에는 그 값을 넘긴다.
|
||||||
|
*/
|
||||||
|
selector:
|
||||||
|
"CallExpression[callee.name=/^set[A-Z]/] > ArrowFunctionExpression MemberExpression[property.name='currentTarget']",
|
||||||
|
message:
|
||||||
|
"Read event.currentTarget before the setState updater runs — it is null by the time the updater is called.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -811,6 +1013,12 @@ export default [
|
|||||||
...globals.node,
|
...globals.node,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
"test-assertion-boundary": testAssertionBoundaryPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"test-assertion-boundary/no-element-operand-equality": "error",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [`tests/support/browser/**/*.${sourceExtensions}`],
|
files: [`tests/support/browser/**/*.${sourceExtensions}`],
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Tech Log frontend" />
|
<meta name="description" content="Tech Log frontend" />
|
||||||
<title>Tech Log</title>
|
<title>Tech Log</title>
|
||||||
|
<!-- public/favicon.svg 는 빌드가 dist 루트로 복사하고 nginx 도 서빙하지만,
|
||||||
|
참조가 없어 브라우저는 /favicon.ico 를 찾다가 404 를 받고 기본 아이콘을
|
||||||
|
띄우고 있었다. %BASE_URL% 은 Vite 가 base 로 치환한다 — 경로 프리픽스
|
||||||
|
배포(/dev/)에서도 같은 파일을 가리키게 하려면 절대경로여선 안 된다. -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon.svg" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+17
-3
@@ -11,8 +11,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "node scripts/build-frontend.ts",
|
"build": "node scripts/build-frontend.ts",
|
||||||
|
"build:profile": "node scripts/generate-runtime-config.ts",
|
||||||
"build:release-candidate": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security && corepack pnpm verify:release && node scripts/verify-supply-chain-artifacts.ts && node scripts/create-release-candidate.ts",
|
"build:release-candidate": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security && corepack pnpm verify:release && node scripts/verify-supply-chain-artifacts.ts && node scripts/create-release-candidate.ts",
|
||||||
"preview": "vite preview",
|
"preview": "node dist/server.mjs",
|
||||||
|
"verify:tech-log-source-parity": "node tests/support/browser/verify-tech-log-source-parity.ts",
|
||||||
"lint": "eslint src scripts tests recipes .storybook vite.config.ts vitest.config.ts playwright*.config.ts --max-warnings=0",
|
"lint": "eslint src scripts tests recipes .storybook vite.config.ts vitest.config.ts playwright*.config.ts --max-warnings=0",
|
||||||
"check:architecture": "node scripts/check-architecture.ts",
|
"check:architecture": "node scripts/check-architecture.ts",
|
||||||
"check:design-system": "node scripts/check-design-system.ts",
|
"check:design-system": "node scripts/check-design-system.ts",
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
"check:i18n:fixture": "node scripts/check-i18n.ts --fixture",
|
"check:i18n:fixture": "node scripts/check-i18n.ts --fixture",
|
||||||
"check:adapter-inventory": "node scripts/check-adapter-inventory.ts",
|
"check:adapter-inventory": "node scripts/check-adapter-inventory.ts",
|
||||||
"check:remediation-ledger": "node scripts/check-remediation-ledger.ts",
|
"check:remediation-ledger": "node scripts/check-remediation-ledger.ts",
|
||||||
|
"check:release-admission": "node scripts/check-release-admission.ts",
|
||||||
"check:diagnostics": "node scripts/check-diagnostics.ts",
|
"check:diagnostics": "node scripts/check-diagnostics.ts",
|
||||||
"check:diagnostics:fixture": "node scripts/check-diagnostics.ts --fixture",
|
"check:diagnostics:fixture": "node scripts/check-diagnostics.ts --fixture",
|
||||||
"check:types": "corepack pnpm check:types:app && corepack pnpm check:types:node && corepack pnpm check:types:test && corepack pnpm check:types:recipes && corepack pnpm check:types:web-worker && corepack pnpm check:types:service-worker",
|
"check:types": "corepack pnpm check:types:app && corepack pnpm check:types:node && corepack pnpm check:types:test && corepack pnpm check:types:recipes && corepack pnpm check:types:web-worker && corepack pnpm check:types:service-worker",
|
||||||
@@ -73,14 +76,19 @@
|
|||||||
"test:browser-file-storage-removal": "node scripts/test-browser-file-storage-runtime-removal.ts",
|
"test:browser-file-storage-removal": "node scripts/test-browser-file-storage-runtime-removal.ts",
|
||||||
"test:realtime-removal": "node scripts/test-realtime-runtime-removal.ts",
|
"test:realtime-removal": "node scripts/test-realtime-runtime-removal.ts",
|
||||||
"test:reference-feature": "vitest run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests",
|
"test:reference-feature": "vitest run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests",
|
||||||
|
"test:tech-log": "vitest run tests/features/tech-log --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/tech-log.xml",
|
||||||
"check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts",
|
"check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts",
|
||||||
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts",
|
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature tests/features/tech-log --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts",
|
||||||
"check:coverage:fixture": "node scripts/check-risk-coverage.ts --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json",
|
"check:coverage:fixture": "node scripts/check-risk-coverage.ts --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json",
|
||||||
"test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:recipes",
|
"test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm check:tech-log-contract && corepack pnpm test:tech-log && corepack pnpm test:recipes",
|
||||||
"verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts",
|
"verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts",
|
||||||
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
|
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
|
||||||
"generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts",
|
"generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts",
|
||||||
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
||||||
|
"generate:tech-log-contract": "node scripts/generate-tech-log-contract.ts",
|
||||||
|
"check:tech-log-contract": "node scripts/generate-tech-log-contract.ts --check",
|
||||||
|
"generate:dev-release-manifest": "node scripts/check-dev-release-manifest.ts --write",
|
||||||
|
"check:dev-release-manifest": "node scripts/check-dev-release-manifest.ts",
|
||||||
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
|
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
|
||||||
"verify:local-evidence": "node scripts/verify-release-candidate.ts && node scripts/verify-release.ts && node scripts/verify-supply-chain-artifacts.ts && node scripts/verify-archived-local-evidence.ts && node scripts/verify-release-candidate.ts",
|
"verify:local-evidence": "node scripts/verify-release-candidate.ts && node scripts/verify-release.ts && node scripts/verify-supply-chain-artifacts.ts && node scripts/verify-archived-local-evidence.ts && node scripts/verify-release-candidate.ts",
|
||||||
"verify:promotion": "node scripts/verify-exact-promotion-bundle.ts",
|
"verify:promotion": "node scripts/verify-exact-promotion-bundle.ts",
|
||||||
@@ -123,11 +131,17 @@
|
|||||||
"check:types:service-worker": "tsc --project tsconfig.service-worker.json"
|
"check:types:service-worker": "tsc --project tsconfig.service-worker.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/ibm-plex-mono": "5.3.0",
|
||||||
"@tanstack/react-query": "5.101.4",
|
"@tanstack/react-query": "5.101.4",
|
||||||
"lucide-react": "1.25.0",
|
"lucide-react": "1.25.0",
|
||||||
|
"pretendard": "1.3.9",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-router-dom": "7.18.1",
|
"react-router-dom": "7.18.1",
|
||||||
|
"remark-directive": "4.0.0",
|
||||||
|
"remark-gfm": "4.0.1",
|
||||||
|
"remark-parse": "11.0.0",
|
||||||
|
"unified": "11.0.5",
|
||||||
"zod": "4.4.3"
|
"zod": "4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -9,35 +9,35 @@ export default defineConfig({
|
|||||||
["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }],
|
["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }],
|
||||||
],
|
],
|
||||||
use: {
|
use: {
|
||||||
baseURL: "http://127.0.0.1:4173",
|
baseURL: "http://127.0.0.1:4273",
|
||||||
|
colorScheme: "light",
|
||||||
|
locale: "ko-KR",
|
||||||
|
timezoneId: "Asia/Seoul",
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
screenshot: "only-on-failure",
|
screenshot: "only-on-failure",
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command:
|
command:
|
||||||
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4173",
|
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4273",
|
||||||
url: "http://127.0.0.1:4173",
|
url: "http://127.0.0.1:4273",
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "chromium",
|
||||||
testIgnore: "**/compact-smoke.spec.ts",
|
|
||||||
use: { ...devices["Desktop Chrome"] },
|
use: { ...devices["Desktop Chrome"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "firefox",
|
name: "firefox",
|
||||||
testIgnore: "**/compact-smoke.spec.ts",
|
|
||||||
use: { ...devices["Desktop Firefox"] },
|
use: { ...devices["Desktop Firefox"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "webkit",
|
name: "webkit",
|
||||||
testIgnore: "**/compact-smoke.spec.ts",
|
|
||||||
use: { ...devices["Desktop Safari"] },
|
use: { ...devices["Desktop Safari"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "chromium-compact",
|
name: "chromium-compact",
|
||||||
testMatch: "**/compact-smoke.spec.ts",
|
testMatch: "**/tech-log-responsive.spec.ts",
|
||||||
use: {
|
use: {
|
||||||
...devices["Desktop Chrome"],
|
...devices["Desktop Chrome"],
|
||||||
viewport: { width: 390, height: 844 },
|
viewport: { width: 390, height: 844 },
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ export default defineConfig({
|
|||||||
toHaveScreenshot: {
|
toHaveScreenshot: {
|
||||||
animations: "disabled",
|
animations: "disabled",
|
||||||
caret: "hide",
|
caret: "hide",
|
||||||
maxDiffPixelRatio: 0.002,
|
maxDiffPixels: 0,
|
||||||
scale: "css",
|
maxDiffPixelRatio: 0,
|
||||||
|
scale: "device",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
use: {
|
use: {
|
||||||
...devices["Desktop Chrome"],
|
...devices["Desktop Chrome"],
|
||||||
baseURL: "http://127.0.0.1:4174",
|
baseURL: "http://127.0.0.1:4174",
|
||||||
colorScheme: "light",
|
colorScheme: "light",
|
||||||
locale: "en-US",
|
locale: "ko-KR",
|
||||||
|
timezoneId: "Asia/Seoul",
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
@@ -33,5 +35,10 @@ export default defineConfig({
|
|||||||
url: "http://127.0.0.1:4174",
|
url: "http://127.0.0.1:4174",
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
},
|
},
|
||||||
projects: [{ name: "chromium-visual" }],
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"], deviceScaleFactor: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+729
File diff suppressed because it is too large
Load Diff
@@ -14,5 +14,8 @@
|
|||||||
"WEB_WORKER": "DEFAULT",
|
"WEB_WORKER": "DEFAULT",
|
||||||
"SERVICE_WORKER": "DEFAULT",
|
"SERVICE_WORKER": "DEFAULT",
|
||||||
"OFFLINE_COMMANDS": "DEFAULT"
|
"OFFLINE_COMMANDS": "DEFAULT"
|
||||||
|
},
|
||||||
|
"FEATURE_OVERRIDES": {
|
||||||
|
"reference-feature": "DEFAULT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22 19.2727C22 20.779 20.779 22 19.2727 22H14.7273C13.221 22 12 20.779 12 19.2727V12H19.2727C20.779 12 22 13.221 22 14.7273V19.2727Z" fill="#68C4FF"/>
|
||||||
|
<path d="M20 2C21.1046 2 22 2.89543 22 4V7C22 8.10457 21.1046 9 20 9H17C15.8954 9 15 8.10457 15 7V4C15 2.89543 15.8954 2 17 2H20Z" fill="#0C79D8"/>
|
||||||
|
<path d="M7 15C8.10457 15 9 15.8954 9 17V20C9 21.1046 8.10457 22 7 22H4C2.89543 22 2 21.1046 2 20V17C2 15.8954 2.89543 15 4 15H7Z" fill="#0C79D8"/>
|
||||||
|
<path d="M12 12H4.72727C3.22104 12 2 10.779 2 9.27273V4.72727C2 3.22104 3.22104 2 4.72727 2H9.27273C10.779 2 12 3.22104 12 4.72727V12Z" fill="#2E9EFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 712 B |
@@ -0,0 +1,57 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="420" viewBox="0 0 1080 420">
|
||||||
|
<title>Fetch Join과 Batch Fetch의 페이징 경계</title>
|
||||||
|
<desc>Fetch Join은 전체 조인 결과를 읽은 뒤 메모리에서 20개를 고른다. Batch Fetch는 부모 20개를 먼저 고른 뒤 해당 ID의 컬렉션만 조회한다.</desc>
|
||||||
|
<rect width="1080" height="420" rx="18" fill="#FCFCFB"/>
|
||||||
|
<style>
|
||||||
|
.label { font: 600 18px Pretendard, sans-serif; fill: #17181B; }
|
||||||
|
.text { font: 500 15px Pretendard, sans-serif; fill: #3F4249; }
|
||||||
|
.muted { font: 500 13px Pretendard, sans-serif; fill: #686B72; }
|
||||||
|
.box { fill: #F7F7F5; stroke: #D9DBDE; stroke-width: 1.5; }
|
||||||
|
.bad { fill: #FFF4E8; stroke: #D99A4E; stroke-width: 1.5; }
|
||||||
|
.good { fill: #ECF6F2; stroke: #4F927F; stroke-width: 1.5; }
|
||||||
|
.arrow { stroke: #A5A8AE; stroke-width: 2; fill: none; marker-end: url(#arrow); }
|
||||||
|
</style>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||||
|
<path d="M0 0 L8 4 L0 8 Z" fill="#A5A8AE"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<text class="label" x="40" y="54">Collection Fetch Join</text>
|
||||||
|
<rect class="box" x="40" y="80" width="190" height="76" rx="10"/>
|
||||||
|
<text class="text" x="66" y="112">목록 + 컬렉션 JOIN</text>
|
||||||
|
<text class="muted" x="66" y="136">부모 × 자식 행</text>
|
||||||
|
<path class="arrow" d="M230 118 H286"/>
|
||||||
|
<rect class="bad" x="286" y="80" width="230" height="76" rx="10"/>
|
||||||
|
<text class="text" x="312" y="112">전체 Join 결과 로드</text>
|
||||||
|
<text class="muted" x="312" y="136">DB LIMIT 없음 · 1,961행</text>
|
||||||
|
<path class="arrow" d="M516 118 H572"/>
|
||||||
|
<rect class="box" x="572" y="80" width="220" height="76" rx="10"/>
|
||||||
|
<text class="text" x="598" y="112">부모 엔티티 복원</text>
|
||||||
|
<text class="muted" x="598" y="136">중복 부모 정리</text>
|
||||||
|
<path class="arrow" d="M792 118 H848"/>
|
||||||
|
<rect class="bad" x="848" y="80" width="192" height="76" rx="10"/>
|
||||||
|
<text class="text" x="874" y="112">메모리에서 20개</text>
|
||||||
|
<text class="muted" x="874" y="136">페이지 경계가 뒤에 있음</text>
|
||||||
|
|
||||||
|
<line x1="40" y1="210" x2="1040" y2="210" stroke="#E2E3E1"/>
|
||||||
|
|
||||||
|
<text class="label" x="40" y="258">Parent Paging + Batch Fetch</text>
|
||||||
|
<rect class="good" x="40" y="284" width="230" height="76" rx="10"/>
|
||||||
|
<text class="text" x="66" y="316">부모 목록 LIMIT 20</text>
|
||||||
|
<text class="muted" x="66" y="340">정렬과 페이지 경계 확정</text>
|
||||||
|
<path class="arrow" d="M270 322 H342"/>
|
||||||
|
<rect class="box" x="342" y="284" width="210" height="76" rx="10"/>
|
||||||
|
<text class="text" x="368" y="316">부모 ID 20개</text>
|
||||||
|
<text class="muted" x="368" y="340">현재 페이지 집합</text>
|
||||||
|
<path class="arrow" d="M552 322 H624"/>
|
||||||
|
<rect class="good" x="624" y="284" width="220" height="76" rx="10"/>
|
||||||
|
<text class="text" x="650" y="316">컬렉션 IN 조회</text>
|
||||||
|
<text class="muted" x="650" y="340">현재 부모만 로드</text>
|
||||||
|
<path class="arrow" d="M844 322 H900"/>
|
||||||
|
<rect class="box" x="900" y="284" width="140" height="76" rx="10"/>
|
||||||
|
<text class="text" x="926" y="316">화면 조립</text>
|
||||||
|
<text class="muted" x="926" y="340">비용 상한 설명 가능</text>
|
||||||
|
|
||||||
|
<text class="muted" x="40" y="396">관찰: 페이징이 적용되는 지점이 부모 조회 앞으로 이동한다.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -8,20 +8,59 @@
|
|||||||
"releaseId": "local-release",
|
"releaseId": "local-release",
|
||||||
"builtAt": "1970-01-01T00:00:00.000Z",
|
"builtAt": "1970-01-01T00:00:00.000Z",
|
||||||
"routeChunks": {
|
"routeChunks": {
|
||||||
"route-home": "src/presentation/pages/home-page.tsx",
|
"route-tech-log-home": "src/features/tech-log/presentation/public/pages/home-page.tsx",
|
||||||
"route-examples-platform": "src/presentation/examples/platform-overview-page.tsx",
|
"route-tech-log-explore": "src/features/tech-log/presentation/public/pages/explore-page.tsx",
|
||||||
"route-examples-ui": "src/presentation/examples/ui-gallery-page.tsx",
|
"route-tech-log-explore-kind": "src/features/tech-log/presentation/public/pages/explore-kind-page.tsx",
|
||||||
"route-examples-states": "src/presentation/examples/state-gallery-page.tsx",
|
"route-tech-log-case": "src/features/tech-log/presentation/public/pages/case-page.tsx",
|
||||||
"route-examples-auth": "src/presentation/examples/auth-example-page.tsx",
|
"route-tech-log-reference": "src/features/tech-log/presentation/public/pages/reference-page.tsx",
|
||||||
"route-reference-resources": "src/features/reference-feature/presentation/reference-resource-page.tsx",
|
"route-tech-log-question": "src/features/tech-log/presentation/public/pages/question-page.tsx",
|
||||||
"route-reference-resource-detail": "src/features/reference-feature/presentation/reference-resource-detail-page.tsx",
|
"route-tech-log-topic": "src/features/tech-log/presentation/public/pages/topic-page.tsx",
|
||||||
"route-reference-resource-form": "src/features/reference-feature/presentation/reference-resource-form-page.tsx",
|
"route-tech-log-projects": "src/features/tech-log/presentation/public/pages/projects-page.tsx",
|
||||||
"route-reference-resource-status": "src/features/reference-feature/presentation/reference-resource-status-page.tsx",
|
"route-tech-log-project": "src/features/tech-log/presentation/public/pages/project-overview-page.tsx",
|
||||||
"route-not-found": "src/presentation/pages/not-found-page.tsx"
|
"route-tech-log-project-records": "src/features/tech-log/presentation/public/pages/project-records-page.tsx",
|
||||||
|
"route-tech-log-project-decisions": "src/features/tech-log/presentation/public/pages/project-decisions-page.tsx",
|
||||||
|
"route-tech-log-project-activity": "src/features/tech-log/presentation/public/pages/project-activity-page.tsx",
|
||||||
|
"route-tech-log-releases": "src/features/tech-log/presentation/public/pages/releases-page.tsx",
|
||||||
|
"route-tech-log-release": "src/features/tech-log/presentation/public/pages/release-page.tsx",
|
||||||
|
"route-tech-log-profile": "src/features/tech-log/presentation/public/pages/profile-page.tsx",
|
||||||
|
"route-tech-log-search": "src/features/tech-log/presentation/public/pages/search-page.tsx",
|
||||||
|
"route-tech-log-studio-home": "src/features/tech-log/presentation/studio/pages/studio-home-page.tsx",
|
||||||
|
"route-tech-log-studio-documents": "src/features/tech-log/presentation/studio/pages/documents-page.tsx",
|
||||||
|
"route-tech-log-studio-document-new": "src/features/tech-log/presentation/studio/pages/new-document-page.tsx",
|
||||||
|
"route-tech-log-studio-document-edit": "src/features/tech-log/presentation/studio/pages/document-edit-page.tsx",
|
||||||
|
"route-tech-log-studio-document-validation": "src/features/tech-log/presentation/studio/pages/document-validation-page.tsx",
|
||||||
|
"route-tech-log-studio-document-preview": "src/features/tech-log/presentation/studio/pages/document-preview-page.tsx",
|
||||||
|
"route-tech-log-studio-document-publish": "src/features/tech-log/presentation/studio/pages/document-publish-page.tsx",
|
||||||
|
"route-tech-log-studio-publications": "src/features/tech-log/presentation/studio/pages/publications-page.tsx",
|
||||||
|
"route-tech-log-studio-publication-preview": "src/features/tech-log/presentation/studio/pages/publication-preview-page.tsx",
|
||||||
|
"route-tech-log-studio-not-found": "src/features/tech-log/presentation/studio/pages/studio-not-found-page.tsx",
|
||||||
|
"route-not-found": "src/features/tech-log/presentation/public/pages/public-not-found-page.tsx"
|
||||||
},
|
},
|
||||||
"contractSet": {
|
"contractSet": {
|
||||||
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
||||||
"setDigest": "sha256:ad6aab71fea6a9ff87cbd170b984b339965afc90d85bb57f87801c9e0c020da2",
|
"setDigest": "sha256:cdcfb628a502d71596f1162726eb395aad0f5f92cf05fd77d304f8e51c81b2fc",
|
||||||
"packages": []
|
"packages": [
|
||||||
|
{
|
||||||
|
"packageId": "@tech-log/management-contract",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55",
|
||||||
|
"runtimeProtocolVersion": 1,
|
||||||
|
"sourceRevision": "ef49d3a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"packageId": "@tech-log/public-contract",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"digest": "sha256:7eb668e39e279e49767306dd36e1dd51302071c39d78495d21307bbd9676220e",
|
||||||
|
"runtimeProtocolVersion": 1,
|
||||||
|
"sourceRevision": "ef49d3a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"packageId": "@tech-log/studio-contract",
|
||||||
|
"version": "3.1.0",
|
||||||
|
"digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd",
|
||||||
|
"runtimeProtocolVersion": 1,
|
||||||
|
"sourceRevision": "ef49d3a"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,20 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim
|
|||||||
* 1. clean dist and .generated/frontend-runtime
|
* 1. clean dist and .generated/frontend-runtime
|
||||||
* 2. generate contractSet and build-info source
|
* 2. generate contractSet and build-info source
|
||||||
* 3. Vite app build (emptyOutDir = true)
|
* 3. Vite app build (emptyOutDir = true)
|
||||||
* 4. scan app dist and generate the static asset source
|
* 4. materialize dist/config.json from the declared APP_PROFILE
|
||||||
* 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
|
* 5. scan app dist and generate the static asset source
|
||||||
* 6. generate Release Manifest V2 and the build manifest
|
* 6. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
|
||||||
|
* 7. generate the self-contained TechLog production serving boundary
|
||||||
|
* 8. generate Release Manifest V2 and the build manifest
|
||||||
*
|
*
|
||||||
* Steps 4 and 5 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
|
* Steps 5 and 6 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
|
||||||
* and `null`: those modes never run an active worker build.
|
* and `null`: those modes never run an active worker build.
|
||||||
|
*
|
||||||
|
* Step 4 has to follow the Vite build and precede the asset scan. Vite copies
|
||||||
|
* `public/` verbatim, so without it every build — including a production one —
|
||||||
|
* ships the local runtime document; and the Service Worker hashes the emitted
|
||||||
|
* `config.json`, so the profile must be in place before that inventory is
|
||||||
|
* taken.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const selection = INSTALLED_RUNTIME_CAPABILITIES.serviceWorker;
|
const selection = INSTALLED_RUNTIME_CAPABILITIES.serviceWorker;
|
||||||
@@ -45,10 +53,13 @@ run("node", ["scripts/generate-contract-set.ts"]);
|
|||||||
// 3. app build
|
// 3. app build
|
||||||
run("npx", ["vite", "build"]);
|
run("npx", ["vite", "build"]);
|
||||||
|
|
||||||
|
// 4. runtime config for the declared profile
|
||||||
|
run("node", ["scripts/generate-runtime-config.ts"]);
|
||||||
|
|
||||||
if (buildsActiveWorker) {
|
if (buildsActiveWorker) {
|
||||||
// 4. hashed asset inventory
|
// 5. hashed asset inventory
|
||||||
run("node", ["scripts/generate-service-worker-assets.ts", "dist"]);
|
run("node", ["scripts/generate-service-worker-assets.ts", "dist"]);
|
||||||
// 5. service worker build
|
// 6. service worker build
|
||||||
run("npx", ["vite", "build", "--config", "vite.service-worker.config.ts"]);
|
run("npx", ["vite", "build", "--config", "vite.service-worker.config.ts"]);
|
||||||
} else {
|
} else {
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
@@ -56,5 +67,8 @@ if (buildsActiveWorker) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. release + build manifest
|
// 7. production serving boundary
|
||||||
|
run("node", ["scripts/generate-tech-log-serving-artifact.ts"]);
|
||||||
|
|
||||||
|
// 8. release + build manifest
|
||||||
run("node", ["scripts/generate-build-manifest.ts"]);
|
run("node", ["scripts/generate-build-manifest.ts"]);
|
||||||
|
|||||||
@@ -723,9 +723,13 @@ function findArchitectureViolations(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const dependency of dependencies) {
|
for (const dependency of dependencies) {
|
||||||
|
const sourceGroups = rule.from?.path
|
||||||
|
? (new RegExp(rule.from.path, "u").exec(dependency.source)?.slice(1) ??
|
||||||
|
[])
|
||||||
|
: [];
|
||||||
if (
|
if (
|
||||||
matchesPath(dependency.source, rule.from) &&
|
matchesPath(dependency.source, rule.from) &&
|
||||||
matchesPath(dependency.target, rule.to)
|
matchesPath(dependency.target, rule.to, sourceGroups)
|
||||||
) {
|
) {
|
||||||
violations.push({
|
violations.push({
|
||||||
rule: rule.name,
|
rule: rule.name,
|
||||||
@@ -746,16 +750,52 @@ function findArchitectureViolations(
|
|||||||
function matchesPath(
|
function matchesPath(
|
||||||
modulePath: string,
|
modulePath: string,
|
||||||
criterion: PathRule | undefined,
|
criterion: PathRule | undefined,
|
||||||
|
sourceGroups: readonly string[] = [],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!criterion) return true;
|
if (!criterion) return true;
|
||||||
if (criterion.path && !new RegExp(criterion.path, "u").test(modulePath)) {
|
if (
|
||||||
|
criterion.path &&
|
||||||
|
!new RegExp(expandSourceGroups(criterion.path, sourceGroups), "u").test(
|
||||||
|
modulePath,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !(
|
return !(
|
||||||
criterion.pathNot && new RegExp(criterion.pathNot, "u").test(modulePath)
|
criterion.pathNot &&
|
||||||
|
new RegExp(expandSourceGroups(criterion.pathNot, sourceGroups), "u").test(
|
||||||
|
modulePath,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Substitutes `$1`..`$9` in a `to` pattern with the capture groups the `from`
|
||||||
|
* pattern matched on the importing module.
|
||||||
|
*
|
||||||
|
* Without it, "an adapter may not import a *different* adapter" cannot be
|
||||||
|
* written as one rule: the target pattern has to name the importer's own
|
||||||
|
* directory to exempt it. The alternative is one rule per adapter group, which
|
||||||
|
* silently stops covering a group the moment somebody adds one — exactly the
|
||||||
|
* gap that let `diagnostics` import `telemetry` while the documented rule said
|
||||||
|
* it could not.
|
||||||
|
*/
|
||||||
|
function expandSourceGroups(
|
||||||
|
pattern: string,
|
||||||
|
sourceGroups: readonly string[],
|
||||||
|
): string {
|
||||||
|
return pattern.replaceAll(/\$([1-9])/gu, (whole, index: string) => {
|
||||||
|
const captured = sourceGroups[Number(index) - 1];
|
||||||
|
// A `from` pattern that did not capture leaves the token literal rather
|
||||||
|
// than quietly matching everything.
|
||||||
|
return captured === undefined ? whole : escapeRegExp(captured);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(value: string): string {
|
||||||
|
return value.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
||||||
|
}
|
||||||
|
|
||||||
function validateArchitectureRules(rules: readonly ArchitectureRule[]): void {
|
function validateArchitectureRules(rules: readonly ArchitectureRule[]): void {
|
||||||
if (!rules.some((rule) => rule.to?.circular === true)) {
|
if (!rules.some((rule) => rule.to?.circular === true)) {
|
||||||
throw new Error("Architecture configuration must contain a circular rule");
|
throw new Error("Architecture configuration must contain a circular rule");
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ if (!architecture?.evidenceArtifactIds.some((id) => index.artifacts.get(id)?.pat
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expectedGateIds = Array.from(
|
const expectedGateIds = Array.from(
|
||||||
{ length: 26 },
|
{ length: 27 },
|
||||||
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
||||||
);
|
);
|
||||||
const passingResults: Record<string, GateResult> = Object.fromEntries(
|
const passingResults: Record<string, GateResult> = Object.fromEntries(
|
||||||
@@ -139,4 +139,4 @@ if (failures.length > 0) {
|
|||||||
process.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`);
|
process.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
process.stdout.write("CI contract: 26 gates, strict v2 graph and generated workflow model PASS\n");
|
process.stdout.write("CI contract: 27 gates, strict v2 graph and generated workflow model PASS\n");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { mkdir, readFile, readdir } from "node:fs/promises";
|
|||||||
|
|
||||||
import { designSystemReportArtifactSchema } from "./contracts/release-artifacts.ts";
|
import { designSystemReportArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||||
|
import { containsRawPaletteValue } from "./lib/design-system-source.ts";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { REQUIRED_COMPONENT_TOKENS, REQUIRED_PRIMITIVE_TOKENS, REQUIRED_SEMANTIC_TOKENS } from "../src/presentation/design-system/tokens/token-contract.ts";
|
import { REQUIRED_COMPONENT_TOKENS, REQUIRED_PRIMITIVE_TOKENS, REQUIRED_SEMANTIC_TOKENS } from "../src/presentation/design-system/tokens/token-contract.ts";
|
||||||
@@ -109,7 +110,7 @@ for (const file of sources) {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!file.includes("src/presentation/design-system/tokens/") &&
|
!file.includes("src/presentation/design-system/tokens/") &&
|
||||||
/(?:#[0-9a-f]{3,8}\b|oklch\(|rgba?\()/i.test(source)
|
containsRawPaletteValue(source)
|
||||||
) {
|
) {
|
||||||
failures.push(`raw palette value in ${file}`);
|
failures.push(`raw palette value in ${file}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Gate: the hand-maintained dev fixture `public/release-manifest.json` must
|
||||||
|
* declare the same `contractSet` the build compiles.
|
||||||
|
*
|
||||||
|
* `corepack pnpm dev` serves that file verbatim, and `verifyContractSet` runs
|
||||||
|
* unconditionally at boot, so a stale fixture is a hard boot failure of
|
||||||
|
* `pnpm dev` in the default `MOCK` mode — not an `HTTP`-mode caveat. Nothing
|
||||||
|
* else in the gate set reads `public/*.json`, which is how a completely broken
|
||||||
|
* `pnpm dev` shipped with every static check green.
|
||||||
|
*
|
||||||
|
* `--write` refreshes the block instead of failing; that is what
|
||||||
|
* `corepack pnpm generate:tech-log-contract` calls.
|
||||||
|
*/
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEV_RELEASE_MANIFEST_PATH,
|
||||||
|
checkDevReleaseManifestContractSet,
|
||||||
|
refreshDevReleaseManifestContractSet,
|
||||||
|
} from "./lib/dev-release-manifest.ts";
|
||||||
|
|
||||||
|
const write = process.argv.includes("--write");
|
||||||
|
|
||||||
|
if (write) {
|
||||||
|
const { changed, contractSet } = await refreshDevReleaseManifestContractSet();
|
||||||
|
process.stdout.write(
|
||||||
|
`${changed ? "Updated" : "Already in sync"}: ${DEV_RELEASE_MANIFEST_PATH} contractSet ` +
|
||||||
|
`(${contractSet.packages.length} package(s), ${contractSet.setDigest})\n`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const failures = await checkDevReleaseManifestContractSet();
|
||||||
|
if (failures.length > 0) {
|
||||||
|
process.stderr.write(
|
||||||
|
`dev release manifest drift:\n- ${failures.join("\n- ")}\n` +
|
||||||
|
`Run: corepack pnpm generate:dev-release-manifest\n`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.stdout.write(
|
||||||
|
`${DEV_RELEASE_MANIFEST_PATH} contractSet matches the compiled contract set.\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEPLOYMENT_TARGETS,
|
||||||
|
findAdmissionViolations,
|
||||||
|
isDeploymentTarget,
|
||||||
|
type AdmissionInput,
|
||||||
|
} from "../src/contracts/deployment-admission.ts";
|
||||||
|
import { parseRuntimeConfigArtifact } from "../src/contracts/release-artifacts.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* §6.4 / FE-GATE-027. Refuses to admit an artifact to an environment it was not
|
||||||
|
* built for.
|
||||||
|
*
|
||||||
|
* Release coherence already proves the artifacts agree with each other. It
|
||||||
|
* cannot prove they belong in production, because a local build is coherent
|
||||||
|
* with itself: `APP_ENV: local`, `AUTH_MODE: demo` and a loopback API pass
|
||||||
|
* every existing gate. This gate closes that by making the destination an
|
||||||
|
* explicit, declared input and refusing anything that does not match it.
|
||||||
|
*
|
||||||
|
* It fails closed in both directions. An undeclared destination is a refusal,
|
||||||
|
* not a default, so an artifact can never be admitted by omission; and every
|
||||||
|
* rule is stated as a reason to refuse, so an unreadable field cannot pass.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RUNTIME_CONFIG_PATH = "dist/config.json";
|
||||||
|
const RECORD_PATH = "artifacts/release/deployment-admission.json";
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const declared = process.env["RELEASE_TARGET"];
|
||||||
|
if (!isDeploymentTarget(declared)) {
|
||||||
|
process.stderr.write(
|
||||||
|
"release admission refused: RELEASE_TARGET must be declared as one of " +
|
||||||
|
`${DEPLOYMENT_TARGETS.join(", ")}; received ${
|
||||||
|
declared === undefined ? "nothing" : declared
|
||||||
|
}.\n` +
|
||||||
|
"An artifact is never admitted by default — name the environment it is for.\n",
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let document: unknown;
|
||||||
|
try {
|
||||||
|
document = JSON.parse(await readFile(RUNTIME_CONFIG_PATH, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(
|
||||||
|
`release admission refused: ${RUNTIME_CONFIG_PATH} is unreadable: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: AdmissionInput;
|
||||||
|
try {
|
||||||
|
config = parseRuntimeConfigArtifact(document) as AdmissionInput;
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(
|
||||||
|
`release admission refused: ${RUNTIME_CONFIG_PATH} is not a valid runtime config: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const violations = findAdmissionViolations(declared, config);
|
||||||
|
await mkdir("artifacts/release", { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
RECORD_PATH,
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
target: declared,
|
||||||
|
appEnv: config.APP_ENV,
|
||||||
|
authMode: config.AUTH_MODE,
|
||||||
|
apiBaseUrl: config.API_BASE_URL,
|
||||||
|
buildId: config.BUILD_ID ?? null,
|
||||||
|
releaseId: config.RELEASE_ID ?? null,
|
||||||
|
status: violations.length === 0 ? "ADMITTED" : "REFUSED",
|
||||||
|
violations,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
process.stderr.write(
|
||||||
|
`release admission refused for ${declared}:\n${violations
|
||||||
|
.map((violation) => ` ${violation.field}: ${violation.reason}`)
|
||||||
|
.join("\n")}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.stdout.write(
|
||||||
|
`release admission: ${declared} ADMITTED ` +
|
||||||
|
`(APP_ENV=${config.APP_ENV}, AUTH_MODE=${config.AUTH_MODE}, ` +
|
||||||
|
`API=${config.API_BASE_URL}); record at ${RECORD_PATH}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
+127
-13
@@ -5,6 +5,7 @@ import path from "node:path";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts";
|
import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts";
|
||||||
|
import { MANUAL_A11Y_ROUTE_IDS } from "../lib/manual-a11y-evidence.ts";
|
||||||
import {
|
import {
|
||||||
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
||||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||||
@@ -247,6 +248,7 @@ const artifactSchemaSchema = z.discriminatedUnion("kind", [
|
|||||||
"provider-provenance",
|
"provider-provenance",
|
||||||
"provider-verification",
|
"provider-verification",
|
||||||
"ci-contract-report",
|
"ci-contract-report",
|
||||||
|
"deployment-admission",
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
@@ -442,7 +444,36 @@ export type LoadCiGateContractOptions = Readonly<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
const CANONICAL_GATE_SHAPE_SHA256 =
|
const CANONICAL_GATE_SHAPE_SHA256 =
|
||||||
"a4a963d0b9deffb7a0a3d755bbbcb979d72610eb74751c3a2e5eca55251e12d4";
|
// Template merge. Both sides carried a digest of their own gate set; neither
|
||||||
|
// describes the merged one. Recomputed from the merged config/ci/gates.json.
|
||||||
|
// Task 11: recomputed again after FE-GATE-009 gained the
|
||||||
|
// TECH_LOG_STUDIO_ASSETS manual accessibility evidence artifact.
|
||||||
|
// Final fix wave item 1: recomputed again after FE-GATE-010 gained
|
||||||
|
// `check-tech-log-contract`. Recomputed with `canonicalGateShapeSha256`
|
||||||
|
// below, verified by first reproducing the previous constant from the
|
||||||
|
// previous `config/ci/gates.json` before hashing the new one.
|
||||||
|
// Alignment follow-up item 2: recomputed again after FE-GATE-007 gained
|
||||||
|
// `test-tech-log` and its junit evidence, by the same method — the previous
|
||||||
|
// constant f3cc9075… was reproduced from the previous gates.json first, so
|
||||||
|
// the transcription that produced this value is known to be the real one.
|
||||||
|
// Dev release manifest drift fix, item 2: recomputed again after FE-GATE-010
|
||||||
|
// gained `check-dev-release-manifest`. Same method — 98d19911… was first
|
||||||
|
// reproduced from the previous gates.json before this value was hashed.
|
||||||
|
// Taxonomy route: recomputed again after FE-GATE-009 gained the
|
||||||
|
// TECH_LOG_STUDIO_TAXONOMY manual accessibility evidence artifact. The gate
|
||||||
|
// lists one evidence artifact per installed route and refuses a set that does
|
||||||
|
// not match the route scope exactly, so adding a route necessarily moves this
|
||||||
|
// digest — that is the point of pinning it.
|
||||||
|
// Release authoring: recomputed again after FE-GATE-009 gained the
|
||||||
|
// TECH_LOG_STUDIO_RELEASES evidence artifact, by the same method — 8c73d447…
|
||||||
|
// was first reproduced from the previous gates.json, so the computation that
|
||||||
|
// produced this value is known to be the one the constant was pinned under.
|
||||||
|
// 프로젝트 편집 화면: FE-GATE-009 가 TECH_LOG_STUDIO_PROJECT_EDIT 증거 아티팩트를
|
||||||
|
// 얻어 다시 계산했다. 같은 방법이다 — 187dbd96… 을 이전 gates.json 에서 먼저 재현해,
|
||||||
|
// 이 값을 만든 계산이 상수가 고정될 때 쓰인 그 계산임을 확인했다.
|
||||||
|
// 릴리즈 편집 화면: 같은 방법으로 다시 계산했다. f9e7e521… 을 이전 gates.json 에서 먼저
|
||||||
|
// 재현했다.
|
||||||
|
"fb138e7c51fdf969f755cd8ff32cf627f1750b966d212c33ee996c8c578db0e3";
|
||||||
|
|
||||||
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
||||||
const normalized = gates.map(
|
const normalized = gates.map(
|
||||||
@@ -473,16 +504,37 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[]
|
|||||||
(total, gate) => total + gate.commandIds.length,
|
(total, gate) => total + gate.commandIds.length,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
if (contract.gates.length !== 26) {
|
if (contract.gates.length !== 27) {
|
||||||
failures.push(`gate authority baseline must contain exactly 26 gates; received ${contract.gates.length}`);
|
failures.push(`gate authority baseline must contain exactly 27 gates; received ${contract.gates.length}`);
|
||||||
}
|
}
|
||||||
if (contract.commands.length !== 81 || commandReferenceCount !== 93) {
|
// Final fix wave, item 1: FE-GATE-010 gained `check-tech-log-contract`, the
|
||||||
|
// drift gate that pins the vendored canonical Studio contract to its digest.
|
||||||
|
// Until it was referenced by a gate it ran only when typed by hand.
|
||||||
|
// Alignment follow-up, item 2: FE-GATE-007 gained `test-tech-log`. The suite
|
||||||
|
// already ran inside `test:coverage`'s combined vitest invocation, so a
|
||||||
|
// TechLog failure was reported as a coverage-gate failure with no junit of
|
||||||
|
// its own to name it.
|
||||||
|
// Dev release manifest drift fix, item 2: FE-GATE-010 gained
|
||||||
|
// `check-dev-release-manifest`. No gate read `public/*.json` at all, so a
|
||||||
|
// fixture that did not declare the compiled contract set broke `pnpm dev`
|
||||||
|
// outright while every static gate stayed green.
|
||||||
|
if (contract.commands.length !== 85 || commandReferenceCount !== 97) {
|
||||||
failures.push(
|
failures.push(
|
||||||
`command authority baseline must contain exactly 81 definitions and 93 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`,
|
`command authority baseline must contain exactly 85 definitions and 97 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (contract.artifacts.length !== 105) {
|
// Template merge. 126 product artifacts plus the two the template added.
|
||||||
failures.push(`artifact authority baseline must contain exactly 105 artifacts; received ${contract.artifacts.length}`);
|
// Task 11 added one more: the TECH_LOG_STUDIO_ASSETS manual a11y evidence file.
|
||||||
|
// Alignment follow-up, item 2 added the TechLog junit report.
|
||||||
|
// The taxonomy route added its own manual a11y evidence file — every installed
|
||||||
|
// route carries one, and the gate checks that the two sets match exactly.
|
||||||
|
// The project edit route did the same: it is what finally lets a project carry
|
||||||
|
// a purpose, a current objective, and a next step, so the public screens that
|
||||||
|
// read those fields stop rendering blanks.
|
||||||
|
// The release edit route followed: the editor used to open below the release
|
||||||
|
// list, so editing meant scrolling past every release to reach it.
|
||||||
|
if (contract.artifacts.length !== 134) {
|
||||||
|
failures.push(`artifact authority baseline must contain exactly 134 artifacts; received ${contract.artifacts.length}`);
|
||||||
}
|
}
|
||||||
if (contract.stages.length !== 5) {
|
if (contract.stages.length !== 5) {
|
||||||
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
|
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
|
||||||
@@ -511,7 +563,7 @@ export function parseCiGateContract(
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
throw new TypeError(`CI gate contract invalid:\n${diagnostic}`);
|
throw new TypeError(`CI gate contract invalid:\n${diagnostic}`);
|
||||||
}
|
}
|
||||||
if ((options.mode ?? "canonical") === "canonical") {
|
if ((options.mode ?? defaultCiContractMode()) === "canonical") {
|
||||||
const failures = canonicalAuthorityBaselineFailures(result.data);
|
const failures = canonicalAuthorityBaselineFailures(result.data);
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`);
|
throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`);
|
||||||
@@ -520,11 +572,29 @@ export function parseCiGateContract(
|
|||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A removal fixture runs the whole suite against a deliberately *reduced* CI
|
||||||
|
* contract: the removed capability's gates, commands and artifacts are pruned.
|
||||||
|
* Loading that contract in canonical mode re-imposes the full exact-count
|
||||||
|
* authority on it, so the fixture failed on the very reduction it exists to
|
||||||
|
* prove. `runRemovalFixturePnpm` marks those runs, and this is where the mark
|
||||||
|
* is honoured.
|
||||||
|
*/
|
||||||
|
export function defaultCiContractMode(): "canonical" | "removal-fixture" {
|
||||||
|
return process.env.CI_CONTRACT_MODE === "removal-fixture"
|
||||||
|
? "removal-fixture"
|
||||||
|
: "canonical";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReducedCiContractRun(): boolean {
|
||||||
|
return defaultCiContractMode() === "removal-fixture";
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadCiGateContract(
|
export async function loadCiGateContract(
|
||||||
root = process.cwd(),
|
root = process.cwd(),
|
||||||
options: LoadCiGateContractOptions = {},
|
options: LoadCiGateContractOptions = {},
|
||||||
): Promise<CiGateContract> {
|
): Promise<CiGateContract> {
|
||||||
const mode = options.mode ?? "canonical";
|
const mode = options.mode ?? defaultCiContractMode();
|
||||||
const [rawContract, rawPackage] = await Promise.all([
|
const [rawContract, rawPackage] = await Promise.all([
|
||||||
readFile(path.join(root, "config/ci/gates.json"), "utf8"),
|
readFile(path.join(root, "config/ci/gates.json"), "utf8"),
|
||||||
readFile(path.join(root, "package.json"), "utf8"),
|
readFile(path.join(root, "package.json"), "utf8"),
|
||||||
@@ -723,6 +793,44 @@ function validateContractSemantics(
|
|||||||
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
|
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessibilityGate = contract.gates.find(
|
||||||
|
({ id: gateId }) => gateId === "FE-GATE-009",
|
||||||
|
);
|
||||||
|
const manualAccessibilityArtifacts = MANUAL_A11Y_ROUTE_IDS.map((routeId) => ({
|
||||||
|
id: `artifact-artifacts-tests-a11y-manual-${routeId.replaceAll("_", "-")}-md`,
|
||||||
|
path: `artifacts/tests/a11y-manual/${routeId}.md`,
|
||||||
|
}));
|
||||||
|
const expectedAccessibilityEvidenceArtifactIds = [
|
||||||
|
"artifact-artifacts-tests-a11y-json",
|
||||||
|
...manualAccessibilityArtifacts.map(({ id: artifactId }) => artifactId),
|
||||||
|
"artifact-artifacts-tests-a11y-manual-report-json",
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
!accessibilityGate ||
|
||||||
|
JSON.stringify(accessibilityGate.evidenceArtifactIds) !==
|
||||||
|
JSON.stringify(expectedAccessibilityEvidenceArtifactIds)
|
||||||
|
) {
|
||||||
|
issue(
|
||||||
|
"FE-GATE-009 manual accessibility evidence must exactly match the installed route scope",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const expected of manualAccessibilityArtifacts) {
|
||||||
|
const artifact = contract.artifacts.find(
|
||||||
|
({ id: artifactId }) => artifactId === expected.id,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!artifact ||
|
||||||
|
artifact.path !== expected.path ||
|
||||||
|
artifact.schemaId !== "markdown" ||
|
||||||
|
artifact.production !== "source-controlled"
|
||||||
|
) {
|
||||||
|
issue(
|
||||||
|
`FE-GATE-009 manual accessibility artifact registration is invalid: ${expected.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const referencedRetentionClasses = new Set(
|
const referencedRetentionClasses = new Set(
|
||||||
contract.gates.map(({ retentionClassId }) => retentionClassId),
|
contract.gates.map(({ retentionClassId }) => retentionClassId),
|
||||||
);
|
);
|
||||||
@@ -759,11 +867,11 @@ function validateContractSemantics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expectedGateIds = Array.from(
|
const expectedGateIds = Array.from(
|
||||||
{ length: 26 },
|
{ length: 27 },
|
||||||
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
||||||
);
|
);
|
||||||
if (JSON.stringify(contract.gates.map(({ id }) => id)) !== JSON.stringify(expectedGateIds)) {
|
if (JSON.stringify(contract.gates.map(({ id }) => id)) !== JSON.stringify(expectedGateIds)) {
|
||||||
issue("gate registry must contain FE-GATE-001..026 in canonical order");
|
issue("gate registry must contain FE-GATE-001..027 in canonical order");
|
||||||
}
|
}
|
||||||
const expectedStages: ReadonlyArray<readonly [string, string, readonly string[], readonly string[]]> = [
|
const expectedStages: ReadonlyArray<readonly [string, string, readonly string[], readonly string[]]> = [
|
||||||
["merge", "MERGE_READY", [], PROMOTION_FORMULA.MERGE_READY],
|
["merge", "MERGE_READY", [], PROMOTION_FORMULA.MERGE_READY],
|
||||||
@@ -834,7 +942,7 @@ function validateContractSemantics(
|
|||||||
const expectedJobOwnership: Readonly<Record<string, readonly string[]>> = {
|
const expectedJobOwnership: Readonly<Record<string, readonly string[]>> = {
|
||||||
merge_gate: ["FE-GATE-001", "FE-GATE-002", "FE-GATE-003", "FE-GATE-004", "FE-GATE-005", "FE-GATE-006", "FE-GATE-007", "FE-GATE-008", "FE-GATE-009", "FE-GATE-010", "FE-GATE-011", "FE-GATE-013", "FE-GATE-020"],
|
merge_gate: ["FE-GATE-001", "FE-GATE-002", "FE-GATE-003", "FE-GATE-004", "FE-GATE-005", "FE-GATE-006", "FE-GATE-007", "FE-GATE-008", "FE-GATE-009", "FE-GATE-010", "FE-GATE-011", "FE-GATE-013", "FE-GATE-020"],
|
||||||
release_gate: ["FE-GATE-012", "FE-GATE-014", "FE-GATE-019", "FE-GATE-026"],
|
release_gate: ["FE-GATE-012", "FE-GATE-014", "FE-GATE-019", "FE-GATE-026"],
|
||||||
immutable_build: ["FE-GATE-015"],
|
immutable_build: ["FE-GATE-015", "FE-GATE-027"],
|
||||||
vulnerability_provider: [],
|
vulnerability_provider: [],
|
||||||
provenance_provider: [],
|
provenance_provider: [],
|
||||||
promotion: [],
|
promotion: [],
|
||||||
@@ -888,7 +996,13 @@ function validateContractSemantics(
|
|||||||
const expectedEnvironmentBindings: Readonly<Record<string, readonly Readonly<{ name: string; value: string }> []>> = {
|
const expectedEnvironmentBindings: Readonly<Record<string, readonly Readonly<{ name: string; value: string }> []>> = {
|
||||||
merge_gate: [],
|
merge_gate: [],
|
||||||
release_gate: [{ name: "HOSTING_BASE_URL", value: "${{ vars.HOSTING_BASE_URL }}" }],
|
release_gate: [{ name: "HOSTING_BASE_URL", value: "${{ vars.HOSTING_BASE_URL }}" }],
|
||||||
immutable_build: [],
|
immutable_build: [
|
||||||
|
// FE-GATE-027 admits the built artifact to a named environment, so both
|
||||||
|
// the profile it was built from and the destination it is claimed for are
|
||||||
|
// declared inputs. An absent RELEASE_TARGET is a refusal, not a default.
|
||||||
|
{ name: "APP_PROFILE", value: "${{ vars.APP_PROFILE }}" },
|
||||||
|
{ name: "RELEASE_TARGET", value: "${{ vars.RELEASE_TARGET }}" },
|
||||||
|
],
|
||||||
vulnerability_provider: [
|
vulnerability_provider: [
|
||||||
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
|
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
|
||||||
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
|
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
|
||||||
|
|||||||
@@ -670,6 +670,27 @@ export const labPerformanceArtifactSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FE-GATE-027. The record of which environment an artifact was admitted to, and
|
||||||
|
* every reason it was refused. Refusals are kept in the artifact so a rejected
|
||||||
|
* promotion leaves evidence rather than only a non-zero exit code.
|
||||||
|
*/
|
||||||
|
export const deploymentAdmissionArtifactSchema = z
|
||||||
|
.object({
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
target: z.enum(["local", "development", "staging", "production"]),
|
||||||
|
appEnv: z.enum(["local", "development", "staging", "production"]),
|
||||||
|
authMode: z.enum(["external", "demo"]),
|
||||||
|
apiBaseUrl: nonEmptyString,
|
||||||
|
buildId: nonEmptyString.nullable(),
|
||||||
|
releaseId: nonEmptyString.nullable(),
|
||||||
|
status: z.enum(["ADMITTED", "REFUSED"]),
|
||||||
|
violations: z.array(
|
||||||
|
z.object({ field: nonEmptyString, reason: nonEmptyString }).strict(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const releaseVerificationArtifactSchema = z
|
export const releaseVerificationArtifactSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(1),
|
schemaVersion: z.literal(1),
|
||||||
@@ -1321,9 +1342,28 @@ export const documentationReviewArtifactSchema = z
|
|||||||
reviewer: z.literal("wiki-diagram-reviewer"),
|
reviewer: z.literal("wiki-diagram-reviewer"),
|
||||||
standard: z.literal("rules/diagram-standards.md v2"),
|
standard: z.literal("rules/diagram-standards.md v2"),
|
||||||
evidenceReport: z
|
evidenceReport: z
|
||||||
.object({ repoPath: nonEmptyString, canonicalPath: nonEmptyString, canonicalSha256: sha256 })
|
.object({
|
||||||
|
repoPath: nonEmptyString,
|
||||||
|
upstreamCanonicalPath: nonEmptyString,
|
||||||
|
canonicalSha256: sha256,
|
||||||
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
reportDigestValid: z.boolean(),
|
reportDigestValid: z.boolean(),
|
||||||
|
/**
|
||||||
|
* The declared review scope, derived from the installed route registry
|
||||||
|
* rather than read off a sentence. Both scope documents claimed six routes
|
||||||
|
* while ten were registered.
|
||||||
|
*/
|
||||||
|
routeScope: z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
path: nonEmptyString,
|
||||||
|
missingRouteIds: z.array(nonEmptyString),
|
||||||
|
documented: z.boolean(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
).min(1),
|
||||||
|
routeScopeDocumented: z.boolean(),
|
||||||
results: z.array(
|
results: z.array(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
@@ -1350,8 +1390,21 @@ export const documentationReviewArtifactSchema = z
|
|||||||
context.addIssue({ code: "custom", path: ["results", index, "passed"], message: "must agree with review evidence" });
|
context.addIssue({ code: "custom", path: ["results", index, "passed"], message: "must agree with review evidence" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (artifact.passed !== (artifact.reportDigestValid && artifact.results.every(({ passed }) => passed))) {
|
artifact.routeScope.forEach((entry, index) => {
|
||||||
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with report digest and review results" });
|
if (entry.documented !== (entry.missingRouteIds.length === 0)) {
|
||||||
|
context.addIssue({ code: "custom", path: ["routeScope", index, "documented"], message: "must agree with the missing route list" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (artifact.routeScopeDocumented !== artifact.routeScope.every(({ documented }) => documented)) {
|
||||||
|
context.addIssue({ code: "custom", path: ["routeScopeDocumented"], message: "must agree with every scope document" });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
artifact.passed !==
|
||||||
|
(artifact.reportDigestValid &&
|
||||||
|
artifact.routeScopeDocumented &&
|
||||||
|
artifact.results.every(({ passed }) => passed))
|
||||||
|
) {
|
||||||
|
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with report digest, documented scope and review results" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ import {
|
|||||||
} from "./lib/build-environment.ts";
|
} from "./lib/build-environment.ts";
|
||||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||||
import { CANONICAL_VITE_MANIFEST_PATH } from "./lib/build-manifest-outputs.ts";
|
import { CANONICAL_VITE_MANIFEST_PATH } from "./lib/build-manifest-outputs.ts";
|
||||||
|
import {
|
||||||
|
findViteDynamicRouteChunk,
|
||||||
|
type ViteManifestRouteEntry,
|
||||||
|
} from "./lib/vite-route-chunks.ts";
|
||||||
|
|
||||||
assertCiBuildEnvironment(process.env);
|
assertCiBuildEnvironment(process.env);
|
||||||
type ViteManifestEntry = Readonly<{
|
|
||||||
file: string;
|
|
||||||
name?: string;
|
|
||||||
isDynamicEntry?: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const packageJson = parsePackageMetadata(
|
const packageJson = parsePackageMetadata(
|
||||||
JSON.parse(await readFile("package.json", "utf8")),
|
JSON.parse(await readFile("package.json", "utf8")),
|
||||||
@@ -63,9 +62,9 @@ const runtimeContracts: Readonly<Record<string, { moduleId: string }>> =
|
|||||||
ROUTE_RUNTIME_CONTRACT;
|
ROUTE_RUNTIME_CONTRACT;
|
||||||
for (const definition of Object.values(ROUTE_REGISTRY)) {
|
for (const definition of Object.values(ROUTE_REGISTRY)) {
|
||||||
const runtime = runtimeContracts[definition.routeId];
|
const runtime = runtimeContracts[definition.routeId];
|
||||||
const asset = Object.values(viteManifestObject).find(
|
const asset = runtime
|
||||||
(entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry,
|
? findViteDynamicRouteChunk(viteManifestObject, runtime.moduleId)
|
||||||
);
|
: undefined;
|
||||||
if (!runtime || !asset?.file) {
|
if (!runtime || !asset?.file) {
|
||||||
throw new Error(`Missing built route chunk: ${definition.routeId}`);
|
throw new Error(`Missing built route chunk: ${definition.routeId}`);
|
||||||
}
|
}
|
||||||
@@ -163,9 +162,9 @@ function parsePackageMetadata(value: unknown): Readonly<{
|
|||||||
|
|
||||||
function parseViteManifest(
|
function parseViteManifest(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): Readonly<Record<string, ViteManifestEntry>> {
|
): Readonly<Record<string, ViteManifestRouteEntry>> {
|
||||||
if (!isRecord(value)) throw new TypeError("Vite manifest must be an object");
|
if (!isRecord(value)) throw new TypeError("Vite manifest must be an object");
|
||||||
const entries: Record<string, ViteManifestEntry> = {};
|
const entries: Record<string, ViteManifestRouteEntry> = {};
|
||||||
for (const [key, candidate] of Object.entries(value)) {
|
for (const [key, candidate] of Object.entries(value)) {
|
||||||
if (!isRecord(candidate) || typeof candidate.file !== "string") {
|
if (!isRecord(candidate) || typeof candidate.file !== "string") {
|
||||||
throw new TypeError(`Invalid Vite manifest entry: ${key}`);
|
throw new TypeError(`Invalid Vite manifest entry: ${key}`);
|
||||||
@@ -176,6 +175,10 @@ function parseViteManifest(
|
|||||||
...(typeof candidate.isDynamicEntry === "boolean"
|
...(typeof candidate.isDynamicEntry === "boolean"
|
||||||
? { isDynamicEntry: candidate.isDynamicEntry }
|
? { isDynamicEntry: candidate.isDynamicEntry }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(Array.isArray(candidate.dynamicImports) &&
|
||||||
|
candidate.dynamicImports.every((item) => typeof item === "string")
|
||||||
|
? { dynamicImports: candidate.dynamicImports as string[] }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the nginx configuration the deployed frontend is served with.
|
||||||
|
*
|
||||||
|
* Generated rather than hand-written because three files already decide what it
|
||||||
|
* must say, and a copy of them would drift: `dist/tech-log-serving-contract.json`
|
||||||
|
* (which paths are SPA routes and what a miss answers with),
|
||||||
|
* `config/hosting/security-headers.json`, and `config/hosting/cache-policy.json`.
|
||||||
|
* The repository had no frontend deployment artifact at all — no Dockerfile, no
|
||||||
|
* server config — so those two hosting files described a contract nothing
|
||||||
|
* fulfilled: `dist/server.mjs` applies neither, answering `no-cache` for hashed
|
||||||
|
* assets and sending no security headers.
|
||||||
|
*
|
||||||
|
* This serves static files only. TLS and the BFF paths belong to the edge: the
|
||||||
|
* deployment's own nginx terminates HTTPS and sends `/api`, the OIDC redirect
|
||||||
|
* chain and the identity provider to the backend directly (in Kubernetes,
|
||||||
|
* Traefik does). A second proxy hop here would only add a place for the two
|
||||||
|
* routing tables to disagree.
|
||||||
|
*
|
||||||
|
* The base path comes from `VITE_ROUTER_BASE_PATH`, the same value the bundle is
|
||||||
|
* built with: served under a prefix, every route and asset lives under it too.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DIST = "dist";
|
||||||
|
const OUT = path.join(DIST, "nginx.conf");
|
||||||
|
|
||||||
|
type ServingContract = Readonly<{
|
||||||
|
publicSpaPathPatterns: readonly string[];
|
||||||
|
studioPathPrefix: string;
|
||||||
|
studioSpaPathPatterns: readonly string[];
|
||||||
|
notFound: Readonly<{ status: number; contentType: string; body: string }>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type HostingHeaders = Readonly<{ headers: Readonly<Record<string, string>> }>;
|
||||||
|
|
||||||
|
type CachePolicy = Readonly<{
|
||||||
|
surfaces: Readonly<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
Readonly<{
|
||||||
|
path?: string;
|
||||||
|
pathPattern?: string;
|
||||||
|
cacheControl?: string;
|
||||||
|
securityHeaders?: boolean;
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
async function readJson<T>(file: string): Promise<T> {
|
||||||
|
return JSON.parse(await readFile(file, "utf8")) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** nginx location matching is not regex-escaped for us; only `=` exact paths are literal. */
|
||||||
|
function exactLocation(pathname: string): string {
|
||||||
|
return pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JS regex from the contract translated for nginx. Both use PCRE-ish syntax
|
||||||
|
* for what the contract uses (`^`, `$`, `[^/]+`, alternation), so the pattern
|
||||||
|
* carries over unchanged — asserted rather than assumed, because a pattern that
|
||||||
|
* silently failed to translate would open a Studio route to the 404 branch.
|
||||||
|
*/
|
||||||
|
function studioRegex(pattern: string): string {
|
||||||
|
// nginx uses PCRE, so anchors, character classes, alternation and plain groups
|
||||||
|
// carry over as written. Lookaround and backreferences do not translate the
|
||||||
|
// same way and would silently change which paths match, so they are refused.
|
||||||
|
if (/\(\?[=!<]|\\[1-9]/.test(pattern)) {
|
||||||
|
throw new Error(
|
||||||
|
`studio SPA pattern uses a construct this generator does not translate: ${pattern}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headerDirectives(
|
||||||
|
headers: Readonly<Record<string, string>>,
|
||||||
|
indent: string,
|
||||||
|
): string {
|
||||||
|
return Object.entries(headers)
|
||||||
|
.map(([name, value]) => `${indent}add_header ${name} "${value}" always;`)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const contract = await readJson<ServingContract>(
|
||||||
|
path.join(DIST, "tech-log-serving-contract.json"),
|
||||||
|
);
|
||||||
|
const security = await readJson<HostingHeaders>(
|
||||||
|
"config/hosting/security-headers.json",
|
||||||
|
);
|
||||||
|
const cache = await readJson<CachePolicy>("config/hosting/cache-policy.json");
|
||||||
|
|
||||||
|
const surfaces = cache.surfaces;
|
||||||
|
const indexCache = surfaces["index"]?.cacheControl ?? "no-cache";
|
||||||
|
const configCache = surfaces["runtimeConfig"]?.cacheControl ?? "no-store";
|
||||||
|
const manifestCache = surfaces["releaseManifest"]?.cacheControl ?? "no-store";
|
||||||
|
const assetCache = surfaces["hashedAsset"]?.cacheControl ?? "no-cache";
|
||||||
|
|
||||||
|
const secure = headerDirectives(security.headers, " ");
|
||||||
|
|
||||||
|
// The bundle's own base path. `/` for a deployment at the domain root, `/dev/`
|
||||||
|
// for one served under a prefix — the routes below have to carry it or nginx
|
||||||
|
// matches paths the browser never asks for.
|
||||||
|
const rawBase = process.env["VITE_ROUTER_BASE_PATH"] ?? "/";
|
||||||
|
const basePath = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
|
||||||
|
|
||||||
|
const [notFoundType, notFoundCharsetParam] = contract.notFound.contentType
|
||||||
|
.split(";")
|
||||||
|
.map((part) => part.trim());
|
||||||
|
const notFoundCharset = (notFoundCharsetParam ?? "charset=utf-8")
|
||||||
|
.replace(/^charset=/i, "")
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
// Regex locations now, matching the Studio half: the contract declares which
|
||||||
|
// paths the router serves, not which ones the fixture happened to contain, so
|
||||||
|
// a record published after this build is served instead of 404ed at the edge.
|
||||||
|
const publicLocations = contract.publicSpaPathPatterns
|
||||||
|
.map(
|
||||||
|
(pattern: string) => ` location ~ ^${basePath}${studioRegex(pattern).slice(1)} {
|
||||||
|
${secure}
|
||||||
|
add_header Cache-Control "${indexCache}" always;
|
||||||
|
try_files /index.html =404;
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const studioLocations = contract.studioSpaPathPatterns
|
||||||
|
.map(
|
||||||
|
(pattern) => ` location ~ ^${basePath}${studioRegex(pattern).slice(1)} {
|
||||||
|
${secure}
|
||||||
|
add_header Cache-Control "${indexCache}" always;
|
||||||
|
try_files /index.html =404;
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const conf = `# Generated by scripts/generate-nginx-config.ts — do not edit.
|
||||||
|
# Sources: dist/tech-log-serving-contract.json, config/hosting/security-headers.json,
|
||||||
|
# config/hosting/cache-policy.json
|
||||||
|
#
|
||||||
|
# Plain HTTP on purpose: the edge terminates TLS and this container is only ever
|
||||||
|
# reached from inside the deployment network.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
|
||||||
|
# The bundle is small and already compressed at rest by the build; gzip here
|
||||||
|
# covers the JSON surfaces and index.html.
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript text/javascript application/json;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
|
||||||
|
# Static surfaces, per config/hosting/cache-policy.json
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
location = ${basePath}/config.json {
|
||||||
|
alias /usr/share/nginx/html/config.json;
|
||||||
|
${secure}
|
||||||
|
add_header Cache-Control "${configCache}" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = ${basePath}/release-manifest.json {
|
||||||
|
alias /usr/share/nginx/html/release-manifest.json;
|
||||||
|
${secure}
|
||||||
|
add_header Cache-Control "${manifestCache}" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Content-hashed filenames, so the long TTL is safe and revalidation is waste.
|
||||||
|
location ${basePath}/assets/ {
|
||||||
|
# alias, not root + URI: under a base path the request is /dev/assets/x.js
|
||||||
|
# while the file is dist/assets/x.js, so root would look for
|
||||||
|
# dist/dev/assets/x.js and answer 404 for every script on the page.
|
||||||
|
alias /usr/share/nginx/html/assets/;
|
||||||
|
add_header Cache-Control "${assetCache}" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Source maps are not published (cache-policy sourceMap.public = false).
|
||||||
|
location ~ \\.map$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SPA routes. Enumerated from the serving contract rather than a catch-all:
|
||||||
|
# a path that is not a real route answers 404 instead of a 200 shell, which is
|
||||||
|
# what tells a crawler the difference.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
${publicLocations}
|
||||||
|
|
||||||
|
${studioLocations}
|
||||||
|
|
||||||
|
location = ${basePath}/favicon.svg {
|
||||||
|
alias /usr/share/nginx/html/favicon.svg;
|
||||||
|
add_header Cache-Control "${assetCache}" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = ${basePath}/media/ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ${basePath}/media/ {
|
||||||
|
alias /usr/share/nginx/html/media/;
|
||||||
|
add_header Cache-Control "${assetCache}" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Anything else is not a route this deployment serves.
|
||||||
|
location / {
|
||||||
|
# The contract states the content type with its charset attached
|
||||||
|
# (text/plain;charset=UTF-8), but nginx takes the two separately —
|
||||||
|
# default_type rejects a parameter outright.
|
||||||
|
default_type ${notFoundType};
|
||||||
|
charset ${notFoundCharset};
|
||||||
|
return ${contract.notFound.status} "${contract.notFound.body}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
await writeFile(OUT, conf, "utf8");
|
||||||
|
process.stdout.write(
|
||||||
|
`nginx config: ${OUT} (${contract.publicSpaPathPatterns.length} public routes, ` +
|
||||||
|
`${contract.studioSpaPathPatterns.length} studio patterns)\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user