test: prove TechLog UI migration parity
@@ -187,6 +187,7 @@ jobs:
|
||||
scripts/lib/secret-scan.ts \
|
||||
scripts/lib/supply-chain.ts \
|
||||
scripts/lib/validated-json-artifact.ts \
|
||||
scripts/lib/vite-route-chunks.ts \
|
||||
src/contracts/release-artifacts.ts \
|
||||
src/features/installed-contract-contributions.ts \
|
||||
src/features/installed-feature-contracts.ts \
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# Task 14 report: integrated TechLog parity and release verification
|
||||
|
||||
## Status
|
||||
|
||||
`DONE_WITH_CONCERNS`. The migrated UI, committed browser evidence, source-to-target
|
||||
comparison, sample-removal gate, focused suites, static gates, and production
|
||||
build pass. The only concern is the repository's pre-existing restricted-runner
|
||||
`test:all` baseline: child-process-heavy tests cannot spawn in the managed
|
||||
sandbox, and 19 provider-environment cases remain red even when isolated with
|
||||
the required process scope. 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. Task 14 is committed by this report's commit,
|
||||
`test: prove TechLog UI migration parity`.
|
||||
|
||||
## Evidence files
|
||||
|
||||
Added or replaced product evidence:
|
||||
|
||||
- `tests/visual/tech-log.visual.spec.ts` and 111 target-only PNG snapshots. The
|
||||
suite has 112 cases because canonical and state coverage for the 1440-pixel
|
||||
`/studio/publications` screen deliberately share the same reviewed image.
|
||||
- `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` and the external, manually run
|
||||
`tests/support/browser/verify-tech-log-source-parity.ts` evidence harness.
|
||||
- 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 and target Vite preview 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, product-subtree
|
||||
tag/class/text/ARIA projection, layout diagnostics, and console/page/request
|
||||
failure collection.
|
||||
|
||||
The final external comparison command was:
|
||||
|
||||
```bash
|
||||
TECH_LOG_SOURCE_URL=http://127.0.0.1:4275 TECH_LOG_TARGET_URL=http://127.0.0.1:4274 corepack pnpm exec tsx tests/support/browser/verify-tech-log-source-parity.ts
|
||||
```
|
||||
|
||||
Result: **112/112 passed**, 0 failed, `totalDifferentPixels=0`, every DOM/class/
|
||||
text/ARIA projection 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 112/112 with
|
||||
`maxDiffPixels=0` and `maxDiffPixelRatio=0`.
|
||||
|
||||
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`, with the exact nine-byte body `Not Found`. Curl and
|
||||
Chromium regression evidence therefore fixes the target to that shell-free
|
||||
response. 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 112-case matrix is 54 canonical-route 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**.
|
||||
- Exact target visual command: **112/112 passed** in 2.5 minutes with no masks
|
||||
and zero pixel tolerance.
|
||||
- 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/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 (383 modules/1,173 dependencies, 12 graph checks,
|
||||
9 forbidden fixtures); 11 registry checks; runtime schema 3 files/40 tests;
|
||||
unit 115/1,247; component 18/124; integration 8/74; recipes 2/17; coverage 164
|
||||
files/1,653 tests at 77.41% statements, 73.26% branches, 83.89% functions, and
|
||||
80.03% lines; risk coverage 382/382 with 76 thresholds; source evidence 198
|
||||
files/111 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 test:visual` | 112 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` | 392 modules, 1,215 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,352 modules transformed; build and manifest completed |
|
||||
| `git diff --check` | passed |
|
||||
|
||||
The exact managed-workspace `corepack pnpm test:all` passed runtime schema 3/40,
|
||||
then its unit phase passed 117 files/1,738 tests and failed 50 tests in only
|
||||
three child-process-heavy files due to `spawnSync ... EPERM`: 39
|
||||
`ci-artifact-contract`, 2 `ci-workflow-generation`, and 9
|
||||
`http-scenario-evidence`. No TechLog test failed. The exact isolated 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; the remaining 19 pre-existing CI-artifact provider/cgroup,
|
||||
RLIMIT/EMFILE, umask, `/tmp`, timing, and identity cases failed. 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.
|
||||
|
||||
Before the Task 14 commit, the full migration range contains the 21 prior
|
||||
commits from baseline establishment through `c5c8b94` and changes 352 files
|
||||
(26,025 insertions, 3,467 deletions). This report's commit adds the integrated
|
||||
parity evidence and fixes with the exact message
|
||||
`test: prove TechLog UI migration parity`. Final status and range are checked
|
||||
again after the commit; integration remains explicitly deferred.
|
||||
@@ -4,9 +4,9 @@ Initialized from `clean-architecture-frontend-template` revision
|
||||
`4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is
|
||||
recorded in `template.lock.json`.
|
||||
|
||||
A React/Vite reference implementation where architecture boundaries,
|
||||
integration behavior, release coherence, accessibility, performance, and
|
||||
operations are executable contracts rather than conventions.
|
||||
A React/Vite TechLog application where architecture boundaries, integration
|
||||
behavior, release coherence, accessibility, performance, and operations are
|
||||
executable contracts rather than conventions.
|
||||
|
||||
## Start locally
|
||||
|
||||
@@ -28,26 +28,20 @@ corepack pnpm dev
|
||||
Runtime-public settings live in `public/config.json` and are validated before
|
||||
the product tree mounts. Client secrets are forbidden.
|
||||
|
||||
## Included starter experience
|
||||
## TechLog experience
|
||||
|
||||
The default build mounts a domain-neutral application shell with a header,
|
||||
responsive sidebar, route focus management, session integration status, and a
|
||||
persistent `system` / `light` / `dark` theme selector.
|
||||
The default build mounts the source-faithful TechLog Public and Studio
|
||||
experience. Public routes provide discovery, search, documents, topics,
|
||||
projects, releases, and profile content. `/studio` provides the session-scoped
|
||||
mock authoring workflow: create, edit, validate, preview, publish, unpublish,
|
||||
and immutable publication history.
|
||||
|
||||
| Route | Purpose |
|
||||
| --- | --- |
|
||||
| `/` | implementation readiness and starter links |
|
||||
| `/examples/ui` | buttons, fields, cards, alerts, badges, modal, and tokens |
|
||||
| `/examples/states` | loading, refresh, empty, error, auth, forbidden, and not-found states |
|
||||
| `/examples/auth` | reactive external-auth integration seam |
|
||||
| `/examples/reference-resources` | removable, session-required reference feature |
|
||||
|
||||
`AUTH_MODE=demo` is credential-free and accepted only in local/development
|
||||
environments. Deployments use `AUTH_MODE=external` and provide the opaque auth
|
||||
owner described in
|
||||
[`docs/architecture/starter-experience.md`](docs/architecture/starter-experience.md).
|
||||
The client route policy is user experience only; server authorization remains
|
||||
authoritative.
|
||||
The 27 canonical route definitions are divided into `PUBLIC` and `STUDIO`
|
||||
nested layouts. Studio authentication remains deliberately deferred; its mock
|
||||
gateway state lasts for one Studio shell session and resets on a full document
|
||||
load. The exact route, dependency, stylesheet, asset, and parity inventory is
|
||||
recorded in
|
||||
[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -61,11 +55,13 @@ contracts own cross-cutting registries
|
||||
```
|
||||
|
||||
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||
`docs/architecture/starter-experience.md`. The removable vertical slice is
|
||||
under `src/features/reference-feature`; its domain, application input, HTTP
|
||||
adapter, contracts, route runtime, and presentation are installed through the
|
||||
feature contribution files in `src/features`. The generic starter routes
|
||||
continue to typecheck, test, and build after that contribution is removed.
|
||||
`docs/architecture/starter-experience.md`. TechLog is one feature boundary
|
||||
under `src/features/tech-log`. Its immutable Public catalog and session-scoped
|
||||
Studio mock gateway are injected through the application feature input;
|
||||
Public and Studio presentation code shares the typed content renderer without
|
||||
importing concrete adapters. The retained reference feature remains a
|
||||
non-product platform contract fixture and can be removed without changing the
|
||||
TechLog route set.
|
||||
|
||||
### Platform capability review
|
||||
|
||||
@@ -117,6 +113,9 @@ corepack pnpm verify:release
|
||||
corepack pnpm check:registries
|
||||
corepack pnpm drill:runbooks
|
||||
corepack pnpm check:ci
|
||||
corepack pnpm exec vitest run tests/features/tech-log
|
||||
corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium
|
||||
corepack pnpm test:visual
|
||||
```
|
||||
|
||||
`check:types`는 source, Node scripts/config와 tests를 분리된 TypeScript
|
||||
@@ -144,7 +143,7 @@ corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
Two gates intentionally need external evidence:
|
||||
|
||||
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
|
||||
for all six registered routes.
|
||||
for the registered product routes.
|
||||
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
||||
eligible-sample threshold and 28 days of production data exist.
|
||||
|
||||
|
||||
@@ -2301,6 +2301,7 @@
|
||||
"scripts/lib/secret-scan.ts",
|
||||
"scripts/lib/supply-chain.ts",
|
||||
"scripts/lib/validated-json-artifact.ts",
|
||||
"scripts/lib/vite-route-chunks.ts",
|
||||
"src/contracts/release-artifacts.ts",
|
||||
"src/features/installed-contract-contributions.ts",
|
||||
"src/features/installed-feature-contracts.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8",
|
||||
"owner": "frontend-platform",
|
||||
"reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
|
||||
"approvedAt": "2026-08-01T15:15:45.537Z"
|
||||
"snapshotDigest": "428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b",
|
||||
"owner": "tech-log-frontend",
|
||||
"reason": "Install approved TechLog Public and Studio route contract",
|
||||
"approvedAt": "2026-08-15T16:32:32.042Z"
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
"registryId": "FE-REG-ROUTE",
|
||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||
"source": "src/features/installed-feature-contracts.ts",
|
||||
"rowCount": 10,
|
||||
"rowCount": 27,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"routeId",
|
||||
"path",
|
||||
"layoutGroup",
|
||||
"paramsSchema",
|
||||
"searchSchema",
|
||||
"access",
|
||||
@@ -23,6 +24,7 @@
|
||||
"fieldTypes": {
|
||||
"routeId": "string",
|
||||
"path": "string",
|
||||
"layoutGroup": "string",
|
||||
"paramsSchema": "string|null",
|
||||
"searchSchema": "string|null",
|
||||
"access": "string",
|
||||
@@ -43,14 +45,29 @@
|
||||
"public",
|
||||
"session-required"
|
||||
],
|
||||
"layoutGroup": [
|
||||
"PUBLIC",
|
||||
"STUDIO"
|
||||
],
|
||||
"paramsSchema": [
|
||||
null,
|
||||
"NotFoundSplat",
|
||||
"ReferenceResourceParams"
|
||||
"ReferenceResourceParams",
|
||||
"TechLogExploreKindParams",
|
||||
"TechLogSlugParams",
|
||||
"TechLogVersionParams",
|
||||
"TechLogDocumentIdParams",
|
||||
"TechLogPublicationEventIdParams",
|
||||
"TechLogStudioSplat"
|
||||
],
|
||||
"searchSchema": [
|
||||
null,
|
||||
"ReferenceResourceListQuery"
|
||||
"ReferenceResourceListQuery",
|
||||
"TechLogHomeSearch",
|
||||
"TechLogExploreSearch",
|
||||
"TechLogExploreKindSearch",
|
||||
"TechLogSearchQuery",
|
||||
"TechLogCaseStateSearch"
|
||||
],
|
||||
"loadingSurface": [
|
||||
"app-shell",
|
||||
@@ -88,6 +105,7 @@
|
||||
"breakingFields": [
|
||||
"routeId",
|
||||
"path",
|
||||
"layoutGroup",
|
||||
"paramsSchema",
|
||||
"searchSchema",
|
||||
"access",
|
||||
@@ -95,75 +113,11 @@
|
||||
]
|
||||
},
|
||||
"rows": {
|
||||
"APP_HOME": {
|
||||
"access": "public",
|
||||
"chunkId": "route-home",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "시작",
|
||||
"navigationOrder": 10,
|
||||
"paramsSchema": null,
|
||||
"path": "/",
|
||||
"routeId": "APP_HOME",
|
||||
"searchSchema": null,
|
||||
"title": "시작"
|
||||
},
|
||||
"EXAMPLES_AUTH": {
|
||||
"access": "public",
|
||||
"chunkId": "route-examples-auth",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "example-page",
|
||||
"navigationLabel": "인증 연동",
|
||||
"navigationOrder": 40,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/auth",
|
||||
"routeId": "EXAMPLES_AUTH",
|
||||
"searchSchema": null,
|
||||
"title": "인증 연동"
|
||||
},
|
||||
"EXAMPLES_PLATFORM": {
|
||||
"access": "public",
|
||||
"chunkId": "route-examples-platform",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "example-page",
|
||||
"navigationLabel": "플랫폼 구성",
|
||||
"navigationOrder": 15,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/platform",
|
||||
"routeId": "EXAMPLES_PLATFORM",
|
||||
"searchSchema": null,
|
||||
"title": "플랫폼 구성"
|
||||
},
|
||||
"EXAMPLES_STATES": {
|
||||
"access": "public",
|
||||
"chunkId": "route-examples-states",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "example-page",
|
||||
"navigationLabel": "화면 상태",
|
||||
"navigationOrder": 30,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/states",
|
||||
"routeId": "EXAMPLES_STATES",
|
||||
"searchSchema": null,
|
||||
"title": "화면 상태"
|
||||
},
|
||||
"EXAMPLES_UI": {
|
||||
"access": "public",
|
||||
"chunkId": "route-examples-ui",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "example-page",
|
||||
"navigationLabel": "UI 구성요소",
|
||||
"navigationOrder": 20,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/ui",
|
||||
"routeId": "EXAMPLES_UI",
|
||||
"searchSchema": null,
|
||||
"title": "UI 구성요소"
|
||||
},
|
||||
"NOT_FOUND": {
|
||||
"access": "public",
|
||||
"chunkId": "route-not-found",
|
||||
"errorSurface": "not-found",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "none",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
@@ -171,59 +125,371 @@
|
||||
"path": "*",
|
||||
"routeId": "NOT_FOUND",
|
||||
"searchSchema": null,
|
||||
"title": "페이지를 찾을 수 없음"
|
||||
"title": "페이지를 찾을 수 없습니다."
|
||||
},
|
||||
"REFERENCE_RESOURCE_DETAIL": {
|
||||
"access": "session-required",
|
||||
"chunkId": "route-reference-resource-detail",
|
||||
"TECH_LOG_CASE": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-case",
|
||||
"errorSurface": "feature-boundary",
|
||||
"loadingSurface": "reference-resource-detail",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "ReferenceResourceParams",
|
||||
"path": "/examples/reference-resources/:resourceId",
|
||||
"routeId": "REFERENCE_RESOURCE_DETAIL",
|
||||
"searchSchema": null,
|
||||
"title": "Reference detail"
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/cases/:slug",
|
||||
"routeId": "TECH_LOG_CASE",
|
||||
"searchSchema": "TechLogCaseStateSearch",
|
||||
"title": "Case"
|
||||
},
|
||||
"REFERENCE_RESOURCE_FORM": {
|
||||
"access": "session-required",
|
||||
"chunkId": "route-reference-resource-form",
|
||||
"TECH_LOG_EXPLORE": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-explore",
|
||||
"errorSurface": "feature-boundary",
|
||||
"loadingSurface": "reference-resource-form",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "탐색",
|
||||
"navigationOrder": 10,
|
||||
"paramsSchema": null,
|
||||
"path": "/explore",
|
||||
"routeId": "TECH_LOG_EXPLORE",
|
||||
"searchSchema": "TechLogExploreSearch",
|
||||
"title": "탐색"
|
||||
},
|
||||
"TECH_LOG_EXPLORE_KIND": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-explore-kind",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogExploreKindParams",
|
||||
"path": "/explore/:kind",
|
||||
"routeId": "TECH_LOG_EXPLORE_KIND",
|
||||
"searchSchema": "TechLogExploreKindSearch",
|
||||
"title": "유형별 탐색"
|
||||
},
|
||||
"TECH_LOG_HOME": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-home",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/reference-resources/new",
|
||||
"routeId": "REFERENCE_RESOURCE_FORM",
|
||||
"searchSchema": null,
|
||||
"title": "Reference form"
|
||||
"path": "/",
|
||||
"routeId": "TECH_LOG_HOME",
|
||||
"searchSchema": "TechLogHomeSearch",
|
||||
"title": "TechLog"
|
||||
},
|
||||
"REFERENCE_RESOURCE_LIST": {
|
||||
"access": "session-required",
|
||||
"chunkId": "route-reference-resources",
|
||||
"TECH_LOG_PROFILE": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-profile",
|
||||
"errorSurface": "feature-boundary",
|
||||
"loadingSurface": "reference-resource-list",
|
||||
"navigationLabel": "Reference feature",
|
||||
"navigationOrder": 50,
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "프로필",
|
||||
"navigationOrder": 40,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/reference-resources",
|
||||
"routeId": "REFERENCE_RESOURCE_LIST",
|
||||
"searchSchema": "ReferenceResourceListQuery",
|
||||
"title": "Reference feature"
|
||||
"path": "/profile",
|
||||
"routeId": "TECH_LOG_PROFILE",
|
||||
"searchSchema": null,
|
||||
"title": "프로필"
|
||||
},
|
||||
"REFERENCE_RESOURCE_STATUS": {
|
||||
"access": "session-required",
|
||||
"chunkId": "route-reference-resource-status",
|
||||
"TECH_LOG_PROJECT": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-project",
|
||||
"errorSurface": "feature-boundary",
|
||||
"loadingSurface": "reference-resource-status",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/projects/:slug",
|
||||
"routeId": "TECH_LOG_PROJECT",
|
||||
"searchSchema": null,
|
||||
"title": "프로젝트"
|
||||
},
|
||||
"TECH_LOG_PROJECT_ACTIVITY": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-project-activity",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/projects/:slug/activity",
|
||||
"routeId": "TECH_LOG_PROJECT_ACTIVITY",
|
||||
"searchSchema": null,
|
||||
"title": "프로젝트 활동"
|
||||
},
|
||||
"TECH_LOG_PROJECT_DECISIONS": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-project-decisions",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/projects/:slug/decisions",
|
||||
"routeId": "TECH_LOG_PROJECT_DECISIONS",
|
||||
"searchSchema": null,
|
||||
"title": "프로젝트 결정"
|
||||
},
|
||||
"TECH_LOG_PROJECT_RECORDS": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-project-records",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/projects/:slug/records",
|
||||
"routeId": "TECH_LOG_PROJECT_RECORDS",
|
||||
"searchSchema": null,
|
||||
"title": "프로젝트 기록"
|
||||
},
|
||||
"TECH_LOG_PROJECTS": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-projects",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "프로젝트",
|
||||
"navigationOrder": 20,
|
||||
"paramsSchema": null,
|
||||
"path": "/projects",
|
||||
"routeId": "TECH_LOG_PROJECTS",
|
||||
"searchSchema": null,
|
||||
"title": "프로젝트"
|
||||
},
|
||||
"TECH_LOG_QUESTION": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-question",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/questions/:slug",
|
||||
"routeId": "TECH_LOG_QUESTION",
|
||||
"searchSchema": null,
|
||||
"title": "Open Question"
|
||||
},
|
||||
"TECH_LOG_REFERENCE": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-reference",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/references/:slug",
|
||||
"routeId": "TECH_LOG_REFERENCE",
|
||||
"searchSchema": null,
|
||||
"title": "Reference"
|
||||
},
|
||||
"TECH_LOG_RELEASE": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-release",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogVersionParams",
|
||||
"path": "/releases/:version",
|
||||
"routeId": "TECH_LOG_RELEASE",
|
||||
"searchSchema": null,
|
||||
"title": "변경 기록"
|
||||
},
|
||||
"TECH_LOG_RELEASES": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-releases",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "변경 기록",
|
||||
"navigationOrder": 30,
|
||||
"paramsSchema": null,
|
||||
"path": "/releases",
|
||||
"routeId": "TECH_LOG_RELEASES",
|
||||
"searchSchema": null,
|
||||
"title": "변경 기록"
|
||||
},
|
||||
"TECH_LOG_SEARCH": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-search",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/reference-resources/status",
|
||||
"routeId": "REFERENCE_RESOURCE_STATUS",
|
||||
"path": "/search",
|
||||
"routeId": "TECH_LOG_SEARCH",
|
||||
"searchSchema": "TechLogSearchQuery",
|
||||
"title": "검색"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_EDIT": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-document-edit",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogDocumentIdParams",
|
||||
"path": "/studio/documents/:id/edit",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT",
|
||||
"searchSchema": null,
|
||||
"title": "Reference status"
|
||||
"title": "문서 편집"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_NEW": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-document-new",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "새 문서",
|
||||
"navigationOrder": 30,
|
||||
"paramsSchema": null,
|
||||
"path": "/studio/documents/new",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW",
|
||||
"searchSchema": null,
|
||||
"title": "새 문서"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_PREVIEW": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-document-preview",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogDocumentIdParams",
|
||||
"path": "/studio/documents/:id/preview",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW",
|
||||
"searchSchema": null,
|
||||
"title": "Public Preview"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_PUBLISH": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-document-publish",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogDocumentIdParams",
|
||||
"path": "/studio/documents/:id/publish",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH",
|
||||
"searchSchema": null,
|
||||
"title": "게시"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_VALIDATION": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-document-validation",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogDocumentIdParams",
|
||||
"path": "/studio/documents/:id/validation",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION",
|
||||
"searchSchema": null,
|
||||
"title": "문서 검증"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENTS": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-documents",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "작업본",
|
||||
"navigationOrder": 10,
|
||||
"paramsSchema": null,
|
||||
"path": "/studio/documents",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENTS",
|
||||
"searchSchema": null,
|
||||
"title": "작업본"
|
||||
},
|
||||
"TECH_LOG_STUDIO_HOME": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-home",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": null,
|
||||
"path": "/studio",
|
||||
"routeId": "TECH_LOG_STUDIO_HOME",
|
||||
"searchSchema": null,
|
||||
"title": "TechLog Studio"
|
||||
},
|
||||
"TECH_LOG_STUDIO_NOT_FOUND": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-not-found",
|
||||
"errorSurface": "not-found",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "none",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogStudioSplat",
|
||||
"path": "/studio/*",
|
||||
"routeId": "TECH_LOG_STUDIO_NOT_FOUND",
|
||||
"searchSchema": null,
|
||||
"title": "Studio 화면을 찾을 수 없습니다"
|
||||
},
|
||||
"TECH_LOG_STUDIO_PUBLICATION_PREVIEW": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-publication-preview",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogPublicationEventIdParams",
|
||||
"path": "/studio/publications/:publicationEventId/preview",
|
||||
"routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW",
|
||||
"searchSchema": null,
|
||||
"title": "게시 Snapshot"
|
||||
},
|
||||
"TECH_LOG_STUDIO_PUBLICATIONS": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-studio-publications",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "STUDIO",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "게시 기록",
|
||||
"navigationOrder": 20,
|
||||
"paramsSchema": null,
|
||||
"path": "/studio/publications",
|
||||
"routeId": "TECH_LOG_STUDIO_PUBLICATIONS",
|
||||
"searchSchema": null,
|
||||
"title": "게시 기록"
|
||||
},
|
||||
"TECH_LOG_TOPIC": {
|
||||
"access": "public",
|
||||
"chunkId": "route-tech-log-topic",
|
||||
"errorSurface": "feature-boundary",
|
||||
"layoutGroup": "PUBLIC",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "TechLogSlugParams",
|
||||
"path": "/topics/:slug",
|
||||
"routeId": "TECH_LOG_TOPIC",
|
||||
"searchSchema": null,
|
||||
"title": "Topic"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -231,7 +497,7 @@
|
||||
"registryId": "FE-REG-ROUTE-RUNTIME",
|
||||
"owner": "feature-frontend-routing-release-recovery-runtime",
|
||||
"source": "src/features/installed-feature-contracts.ts",
|
||||
"rowCount": 10,
|
||||
"rowCount": 27,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"routeId",
|
||||
@@ -276,64 +542,166 @@
|
||||
]
|
||||
},
|
||||
"rows": {
|
||||
"APP_HOME": {
|
||||
"moduleId": "home-page",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "APP_HOME",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"EXAMPLES_AUTH": {
|
||||
"moduleId": "auth-example-page",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "EXAMPLES_AUTH",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"EXAMPLES_PLATFORM": {
|
||||
"moduleId": "platform-overview-page",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "EXAMPLES_PLATFORM",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"EXAMPLES_STATES": {
|
||||
"moduleId": "state-gallery-page",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "EXAMPLES_STATES",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"EXAMPLES_UI": {
|
||||
"moduleId": "ui-gallery-page",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "EXAMPLES_UI",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"NOT_FOUND": {
|
||||
"moduleId": "not-found-page",
|
||||
"moduleId": "route-not-found",
|
||||
"paramsCodec": "NotFoundSplat",
|
||||
"routeId": "NOT_FOUND",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"REFERENCE_RESOURCE_DETAIL": {
|
||||
"moduleId": "reference-resource-detail-page",
|
||||
"paramsCodec": "ReferenceResourceParams",
|
||||
"routeId": "REFERENCE_RESOURCE_DETAIL",
|
||||
"TECH_LOG_CASE": {
|
||||
"moduleId": "route-tech-log-case",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_CASE",
|
||||
"searchCodec": "TechLogCaseStateSearch"
|
||||
},
|
||||
"TECH_LOG_EXPLORE": {
|
||||
"moduleId": "route-tech-log-explore",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_EXPLORE",
|
||||
"searchCodec": "TechLogExploreSearch"
|
||||
},
|
||||
"TECH_LOG_EXPLORE_KIND": {
|
||||
"moduleId": "route-tech-log-explore-kind",
|
||||
"paramsCodec": "TechLogExploreKindParams",
|
||||
"routeId": "TECH_LOG_EXPLORE_KIND",
|
||||
"searchCodec": "TechLogExploreKindSearch"
|
||||
},
|
||||
"TECH_LOG_HOME": {
|
||||
"moduleId": "route-tech-log-home",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_HOME",
|
||||
"searchCodec": "TechLogHomeSearch"
|
||||
},
|
||||
"TECH_LOG_PROFILE": {
|
||||
"moduleId": "route-tech-log-profile",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_PROFILE",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"REFERENCE_RESOURCE_FORM": {
|
||||
"moduleId": "reference-resource-form-page",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "REFERENCE_RESOURCE_FORM",
|
||||
"TECH_LOG_PROJECT": {
|
||||
"moduleId": "route-tech-log-project",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_PROJECT",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"REFERENCE_RESOURCE_LIST": {
|
||||
"moduleId": "reference-resource-page",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "REFERENCE_RESOURCE_LIST",
|
||||
"searchCodec": "ReferenceResourceListQuery"
|
||||
"TECH_LOG_PROJECT_ACTIVITY": {
|
||||
"moduleId": "route-tech-log-project-activity",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_PROJECT_ACTIVITY",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"REFERENCE_RESOURCE_STATUS": {
|
||||
"moduleId": "reference-resource-status-page",
|
||||
"TECH_LOG_PROJECT_DECISIONS": {
|
||||
"moduleId": "route-tech-log-project-decisions",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_PROJECT_DECISIONS",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_PROJECT_RECORDS": {
|
||||
"moduleId": "route-tech-log-project-records",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_PROJECT_RECORDS",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_PROJECTS": {
|
||||
"moduleId": "route-tech-log-projects",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "REFERENCE_RESOURCE_STATUS",
|
||||
"routeId": "TECH_LOG_PROJECTS",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_QUESTION": {
|
||||
"moduleId": "route-tech-log-question",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_QUESTION",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_REFERENCE": {
|
||||
"moduleId": "route-tech-log-reference",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_REFERENCE",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_RELEASE": {
|
||||
"moduleId": "route-tech-log-release",
|
||||
"paramsCodec": "TechLogVersionParams",
|
||||
"routeId": "TECH_LOG_RELEASE",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_RELEASES": {
|
||||
"moduleId": "route-tech-log-releases",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_RELEASES",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_SEARCH": {
|
||||
"moduleId": "route-tech-log-search",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_SEARCH",
|
||||
"searchCodec": "TechLogSearchQuery"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_EDIT": {
|
||||
"moduleId": "route-tech-log-studio-document-edit",
|
||||
"paramsCodec": "TechLogDocumentIdParams",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_NEW": {
|
||||
"moduleId": "route-tech-log-studio-document-new",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_PREVIEW": {
|
||||
"moduleId": "route-tech-log-studio-document-preview",
|
||||
"paramsCodec": "TechLogDocumentIdParams",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_PUBLISH": {
|
||||
"moduleId": "route-tech-log-studio-document-publish",
|
||||
"paramsCodec": "TechLogDocumentIdParams",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENT_VALIDATION": {
|
||||
"moduleId": "route-tech-log-studio-document-validation",
|
||||
"paramsCodec": "TechLogDocumentIdParams",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_DOCUMENTS": {
|
||||
"moduleId": "route-tech-log-studio-documents",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_STUDIO_DOCUMENTS",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_HOME": {
|
||||
"moduleId": "route-tech-log-studio-home",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_STUDIO_HOME",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_NOT_FOUND": {
|
||||
"moduleId": "route-tech-log-studio-not-found",
|
||||
"paramsCodec": "TechLogStudioSplat",
|
||||
"routeId": "TECH_LOG_STUDIO_NOT_FOUND",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_PUBLICATION_PREVIEW": {
|
||||
"moduleId": "route-tech-log-studio-publication-preview",
|
||||
"paramsCodec": "TechLogPublicationEventIdParams",
|
||||
"routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_STUDIO_PUBLICATIONS": {
|
||||
"moduleId": "route-tech-log-studio-publications",
|
||||
"paramsCodec": "none",
|
||||
"routeId": "TECH_LOG_STUDIO_PUBLICATIONS",
|
||||
"searchCodec": "none"
|
||||
},
|
||||
"TECH_LOG_TOPIC": {
|
||||
"moduleId": "route-tech-log-topic",
|
||||
"paramsCodec": "TechLogSlugParams",
|
||||
"routeId": "TECH_LOG_TOPIC",
|
||||
"searchCodec": "none"
|
||||
}
|
||||
}
|
||||
@@ -530,7 +898,7 @@
|
||||
"registryId": "FE-REG-SCHEMA",
|
||||
"owner": "feature-frontend-contract-schema-registry",
|
||||
"source": "src/features/installed-feature-contracts.ts",
|
||||
"rowCount": 8,
|
||||
"rowCount": 19,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"schemaId",
|
||||
@@ -633,6 +1001,105 @@
|
||||
"schemaId": "ReferenceResourcePayload",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
},
|
||||
"TechLogCaseStateSearch": {
|
||||
"boundary": "route-search",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogCaseStateSearch",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
},
|
||||
"TechLogDocumentIdParams": {
|
||||
"boundary": "route-params",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogDocumentIdParams",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"TechLogExploreKindParams": {
|
||||
"boundary": "route-params",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogExploreKindParams",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"TechLogExploreKindSearch": {
|
||||
"boundary": "route-search",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogExploreKindSearch",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
},
|
||||
"TechLogExploreSearch": {
|
||||
"boundary": "route-search",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogExploreSearch",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
},
|
||||
"TechLogHomeSearch": {
|
||||
"boundary": "route-search",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogHomeSearch",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
},
|
||||
"TechLogPublicationEventIdParams": {
|
||||
"boundary": "route-params",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogPublicationEventIdParams",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"TechLogSearchQuery": {
|
||||
"boundary": "route-search",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogSearchQuery",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "STRIP_UNKNOWN"
|
||||
},
|
||||
"TechLogSlugParams": {
|
||||
"boundary": "route-params",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogSlugParams",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"TechLogStudioSplat": {
|
||||
"boundary": "route-params",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogStudioSplat",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
},
|
||||
"TechLogVersionParams": {
|
||||
"boundary": "route-params",
|
||||
"direction": "REQUEST",
|
||||
"owner": "feature-tech-log",
|
||||
"runtime": "zod",
|
||||
"schemaId": "TechLogVersionParams",
|
||||
"schemaVersion": 1,
|
||||
"unknownFieldPolicy": "REJECT_UNKNOWN"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -766,7 +1233,7 @@
|
||||
"registryId": "FE-REG-STORAGE",
|
||||
"owner": "feature-frontend-storage-registry-contract",
|
||||
"source": "src/contracts/storage-keys.ts",
|
||||
"rowCount": 4,
|
||||
"rowCount": 5,
|
||||
"contract": {
|
||||
"requiredFields": [
|
||||
"logicalName",
|
||||
@@ -847,6 +1314,19 @@
|
||||
"ttl": null,
|
||||
"valueCodec": "none"
|
||||
},
|
||||
"CACHE_INVALIDATION_PULSE": {
|
||||
"backend": "localStorage",
|
||||
"classification": "opaque-cache",
|
||||
"logicalName": "CACHE_INVALIDATION_PULSE",
|
||||
"migration": "discard",
|
||||
"name": "pulse",
|
||||
"physicalKey": "ca-frontend:cache-invalidation:v1:pulse",
|
||||
"quotaFallback": "no-persist",
|
||||
"schemaVersion": 1,
|
||||
"scope": "cache-invalidation",
|
||||
"ttl": null,
|
||||
"valueCodec": "opaque-string-v1"
|
||||
},
|
||||
"CHUNK_RELOAD_GUARD": {
|
||||
"backend": "sessionStorage",
|
||||
"classification": "opaque-cache",
|
||||
|
||||
@@ -1,6 +1,190 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"changes": [
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:allowedValues:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:breakingFields:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:fieldTypes:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:$contract:requiredFields:contract-field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:APP_HOME:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_AUTH:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_PLATFORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_STATES:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:EXAMPLES_UI:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_DETAIL:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_FORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_LIST:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_STATUS:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:APP_HOME:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_AUTH:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_PLATFORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_STATES:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_UI:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:NOT_FOUND:moduleId:field-changed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_DETAIL:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_FORM:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_LIST:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_STATUS:*:removed",
|
||||
"versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.",
|
||||
"migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.",
|
||||
"rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ENV:API_CONTRACT_VERSION:*:removed",
|
||||
"versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.",
|
||||
|
||||
@@ -1,4 +1,47 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"changes": []
|
||||
"changes": [
|
||||
{
|
||||
"changeId": "add:@fontsource/ibm-plex-mono@5.3.0",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Preserve the source application's IBM Plex Mono typography and bundled font assets without a runtime font request.",
|
||||
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
|
||||
},
|
||||
{
|
||||
"changeId": "add:pretendard@1.3.9",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Preserve the source application's Pretendard Variable typography using its pinned bundled font asset.",
|
||||
"rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point."
|
||||
},
|
||||
{
|
||||
"changeId": "add:remark-directive@4.0.0",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Parse the source TechLog document directive syntax through the migrated deterministic content pipeline.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
},
|
||||
{
|
||||
"changeId": "add:remark-gfm@4.0.1",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Preserve source TechLog GitHub-flavored Markdown tables, task lists, and autolink parsing.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
},
|
||||
{
|
||||
"changeId": "add:remark-parse@11.0.0",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Parse the source TechLog Markdown records into the migrated typed public-render model.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
},
|
||||
{
|
||||
"changeId": "add:unified@11.0.5",
|
||||
"owner": "tech-log-frontend",
|
||||
"reviewer": "frontend-platform-security",
|
||||
"reason": "Compose the source-equivalent Markdown and directive parsing stages without framework coupling.",
|
||||
"rollback": "Revert the TechLog UI migration content parser and dependency installation together."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0"
|
||||
"MPL-2.0",
|
||||
"OFL-1.1"
|
||||
],
|
||||
"deniedLicensePatterns": [
|
||||
"(^|\\s)AGPL",
|
||||
|
||||
@@ -71,12 +71,107 @@ No Next.js, Vinext, or Cloudflare package is part of the migration baseline.
|
||||
|
||||
## Source stylesheet inventory
|
||||
|
||||
- `app/globals.css`
|
||||
- `app/studio.css`
|
||||
- `app/studio-editor.css`
|
||||
- `components/studio/workflow.module.css`
|
||||
- `components/studio/publication-flow.module.css`
|
||||
- `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 and the target Vite preview 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 product-subtree
|
||||
tag/class/text/ARIA projection and console/request failure comparison.
|
||||
|
||||
| Evidence group | Cases |
|
||||
| --- | ---: |
|
||||
| 27 canonical routes at 360 and 1440 pixels | 54 |
|
||||
| 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** | **112** |
|
||||
|
||||
Result: 112/112 passed, zero failed, zero different pixels, all DOM/class/text/
|
||||
ARIA projections equal, and zero unexplained source or target console/request
|
||||
errors. The target-only visual regression suite exercises the same 112 cases
|
||||
against 111 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 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`, and the exact
|
||||
nine-byte body `Not Found`. The target intentionally removes the Public shell
|
||||
for those paths and matches that response in curl and Chromium regressions;
|
||||
this is the observed production source contract, not a generic runtime error.
|
||||
|
||||
## 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 runtime-schema phase passed 3 files/40 tests. The unit phase passed 117
|
||||
files/1,738 tests and reported 50 failures in three child-process-heavy files:
|
||||
39 `ci-artifact-contract`, 2 `ci-workflow-generation`, and 9
|
||||
`http-scenario-evidence`. Every failure was rooted in `spawnSync ... EPERM` in
|
||||
the restricted process sandbox; no `tests/features/tech-log` test failed.
|
||||
|
||||
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 pre-existing 19 environment-only
|
||||
`ci-artifact-contract` cases failed. The 407 `ci-workflow-generation` and 14
|
||||
`http-scenario-evidence` tests all passed. The remaining exact 19 are the
|
||||
documented provider lifecycle/cgroup, RLIMIT/EMFILE, restrictive-umask, and
|
||||
`/tmp` timing/identity cases; they 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, and the package manager's frozen-lockfile verification.
|
||||
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.
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"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",
|
||||
"check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts",
|
||||
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts",
|
||||
"test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature tests/features/tech-log --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts",
|
||||
"check:coverage:fixture": "node scripts/check-risk-coverage.ts --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json",
|
||||
"test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:recipes",
|
||||
"verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts",
|
||||
|
||||
@@ -9,35 +9,35 @@ export default defineConfig({
|
||||
["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }],
|
||||
],
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:4173",
|
||||
baseURL: "http://127.0.0.1:4273",
|
||||
colorScheme: "light",
|
||||
locale: "ko-KR",
|
||||
timezoneId: "Asia/Seoul",
|
||||
trace: "retain-on-failure",
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
webServer: {
|
||||
command:
|
||||
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4173",
|
||||
url: "http://127.0.0.1:4173",
|
||||
"corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4273",
|
||||
url: "http://127.0.0.1:4273",
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
testIgnore: "**/compact-smoke.spec.ts",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
testIgnore: "**/compact-smoke.spec.ts",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
testIgnore: "**/compact-smoke.spec.ts",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
{
|
||||
name: "chromium-compact",
|
||||
testMatch: "**/compact-smoke.spec.ts",
|
||||
testMatch: "**/tech-log-responsive.spec.ts",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
viewport: { width: 390, height: 844 },
|
||||
|
||||
@@ -16,15 +16,17 @@ export default defineConfig({
|
||||
toHaveScreenshot: {
|
||||
animations: "disabled",
|
||||
caret: "hide",
|
||||
maxDiffPixelRatio: 0.002,
|
||||
scale: "css",
|
||||
maxDiffPixels: 0,
|
||||
maxDiffPixelRatio: 0,
|
||||
scale: "device",
|
||||
},
|
||||
},
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
baseURL: "http://127.0.0.1:4174",
|
||||
colorScheme: "light",
|
||||
locale: "en-US",
|
||||
locale: "ko-KR",
|
||||
timezoneId: "Asia/Seoul",
|
||||
trace: "retain-on-failure",
|
||||
},
|
||||
webServer: {
|
||||
@@ -33,5 +35,10 @@ export default defineConfig({
|
||||
url: "http://127.0.0.1:4174",
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
projects: [{ name: "chromium-visual" }],
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"], deviceScaleFactor: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mkdir, readFile, readdir } from "node:fs/promises";
|
||||
|
||||
import { designSystemReportArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
import { containsRawPaletteValue } from "./lib/design-system-source.ts";
|
||||
import path from "node:path";
|
||||
|
||||
import { REQUIRED_COMPONENT_TOKENS, REQUIRED_PRIMITIVE_TOKENS, REQUIRED_SEMANTIC_TOKENS } from "../src/presentation/design-system/tokens/token-contract.ts";
|
||||
@@ -109,7 +110,7 @@ for (const file of sources) {
|
||||
}
|
||||
if (
|
||||
!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}`);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,12 @@ import {
|
||||
} from "./lib/build-environment.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.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);
|
||||
type ViteManifestEntry = Readonly<{
|
||||
file: string;
|
||||
name?: string;
|
||||
isDynamicEntry?: boolean;
|
||||
}>;
|
||||
|
||||
const packageJson = parsePackageMetadata(
|
||||
JSON.parse(await readFile("package.json", "utf8")),
|
||||
@@ -63,9 +62,9 @@ const runtimeContracts: Readonly<Record<string, { moduleId: string }>> =
|
||||
ROUTE_RUNTIME_CONTRACT;
|
||||
for (const definition of Object.values(ROUTE_REGISTRY)) {
|
||||
const runtime = runtimeContracts[definition.routeId];
|
||||
const asset = Object.values(viteManifestObject).find(
|
||||
(entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry,
|
||||
);
|
||||
const asset = runtime
|
||||
? findViteDynamicRouteChunk(viteManifestObject, runtime.moduleId)
|
||||
: undefined;
|
||||
if (!runtime || !asset?.file) {
|
||||
throw new Error(`Missing built route chunk: ${definition.routeId}`);
|
||||
}
|
||||
@@ -163,9 +162,9 @@ function parsePackageMetadata(value: unknown): Readonly<{
|
||||
|
||||
function parseViteManifest(
|
||||
value: unknown,
|
||||
): Readonly<Record<string, ViteManifestEntry>> {
|
||||
): Readonly<Record<string, ViteManifestRouteEntry>> {
|
||||
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)) {
|
||||
if (!isRecord(candidate) || typeof candidate.file !== "string") {
|
||||
throw new TypeError(`Invalid Vite manifest entry: ${key}`);
|
||||
@@ -176,6 +175,10 @@ function parseViteManifest(
|
||||
...(typeof candidate.isDynamicEntry === "boolean"
|
||||
? { isDynamicEntry: candidate.isDynamicEntry }
|
||||
: {}),
|
||||
...(Array.isArray(candidate.dynamicImports) &&
|
||||
candidate.dynamicImports.every((item) => typeof item === "string")
|
||||
? { dynamicImports: candidate.dynamicImports as string[] }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return entries;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
const RAW_PALETTE_VALUE_PATTERN =
|
||||
/(?<![a-z0-9/_-])(?:#[0-9a-f]{3,8}\b|oklch\(|rgba?\()/i;
|
||||
|
||||
export function containsRawPaletteValue(source: string): boolean {
|
||||
return RAW_PALETTE_VALUE_PATTERN.test(source);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
type ReleaseCandidateManifest,
|
||||
} from "./release-candidate.ts";
|
||||
import { verifyReleaseRuntimeCoherence } from "./release-runtime-coherence.ts";
|
||||
import { findViteDynamicRouteChunk } from "./vite-route-chunks.ts";
|
||||
import { digestReleaseInputFiles } from "./release-input-evidence.ts";
|
||||
import {
|
||||
compareStoredDependencyEvidence,
|
||||
@@ -1256,12 +1257,27 @@ async function verifyReleaseOutputs(
|
||||
ROUTE_RUNTIME_CONTRACT;
|
||||
for (const definition of Object.values(ROUTE_REGISTRY)) {
|
||||
const runtime = runtimeContracts[definition.routeId];
|
||||
const viteEntry = Object.values(viteManifest).find(
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
entry.name === runtime?.moduleId &&
|
||||
entry.isDynamicEntry === true,
|
||||
);
|
||||
const viteEntry = runtime
|
||||
? findViteDynamicRouteChunk(
|
||||
Object.fromEntries(
|
||||
Object.entries(viteManifest).flatMap(([key, entry]) => {
|
||||
if (!isRecord(entry) || typeof entry.file !== "string") return [];
|
||||
return [[key, {
|
||||
file: entry.file,
|
||||
...(typeof entry.name === "string" ? { name: entry.name } : {}),
|
||||
...(typeof entry.isDynamicEntry === "boolean"
|
||||
? { isDynamicEntry: entry.isDynamicEntry }
|
||||
: {}),
|
||||
...(Array.isArray(entry.dynamicImports) &&
|
||||
entry.dynamicImports.every((item) => typeof item === "string")
|
||||
? { dynamicImports: entry.dynamicImports as string[] }
|
||||
: {}),
|
||||
}]];
|
||||
}),
|
||||
),
|
||||
runtime.moduleId,
|
||||
)
|
||||
: undefined;
|
||||
const file = isRecord(viteEntry) ? viteEntry.file : null;
|
||||
if (
|
||||
typeof file !== "string" ||
|
||||
|
||||
@@ -52,6 +52,7 @@ export const LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS = Object.freeze([
|
||||
"scripts/lib/secret-scan.ts",
|
||||
"scripts/lib/supply-chain.ts",
|
||||
"scripts/lib/validated-json-artifact.ts",
|
||||
"scripts/lib/vite-route-chunks.ts",
|
||||
"src/contracts/release-artifacts.ts",
|
||||
"src/features/installed-contract-contributions.ts",
|
||||
"src/features/installed-feature-contracts.ts",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export type ViteManifestRouteEntry = Readonly<{
|
||||
file: string;
|
||||
name?: string;
|
||||
isDynamicEntry?: boolean;
|
||||
dynamicImports?: readonly string[];
|
||||
}>;
|
||||
|
||||
export function findViteDynamicRouteChunk(
|
||||
manifest: Readonly<Record<string, ViteManifestRouteEntry>>,
|
||||
moduleId: string,
|
||||
): ViteManifestRouteEntry | undefined {
|
||||
const dynamicallyImportedKeys = new Set(
|
||||
Object.values(manifest).flatMap((entry) => entry.dynamicImports ?? []),
|
||||
);
|
||||
return Object.entries(manifest).find(
|
||||
([key, entry]) =>
|
||||
entry.name === moduleId &&
|
||||
(entry.isDynamicEntry === true || dynamicallyImportedKeys.has(key)),
|
||||
)?.[1];
|
||||
}
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
} from "./lib/build-manifest-outputs.ts";
|
||||
import { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
import {
|
||||
findViteDynamicRouteChunk,
|
||||
type ViteManifestRouteEntry,
|
||||
} from "./lib/vite-route-chunks.ts";
|
||||
import { releaseVerificationArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
|
||||
type CoherenceFixture = Readonly<{
|
||||
@@ -38,11 +42,6 @@ type CoherenceFixture = Readonly<{
|
||||
}>;
|
||||
type RuntimeConfigDocument = RuntimeConfigArtifact &
|
||||
Readonly<{ BUILD_ID: string; RELEASE_ID: string }>;
|
||||
type ViteManifestEntry = Readonly<{
|
||||
file: string;
|
||||
name?: string;
|
||||
isDynamicEntry?: boolean;
|
||||
}>;
|
||||
|
||||
export type ReleaseArtifactReader = (path: string) => Promise<unknown>;
|
||||
|
||||
@@ -170,9 +169,9 @@ const runtimeContracts: Readonly<Record<string, { moduleId: string }>> =
|
||||
ROUTE_RUNTIME_CONTRACT;
|
||||
for (const definition of Object.values(ROUTE_REGISTRY)) {
|
||||
const runtime = runtimeContracts[definition.routeId];
|
||||
const viteEntry = Object.values(viteManifestObject).find(
|
||||
(entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry,
|
||||
);
|
||||
const viteEntry = runtime
|
||||
? findViteDynamicRouteChunk(viteManifestObject, runtime.moduleId)
|
||||
: undefined;
|
||||
const routeAsset = release.routeChunks[definition.chunkId];
|
||||
if (!runtime || !viteEntry || routeAsset !== viteEntry.file) {
|
||||
artifactMismatches.push(`routeChunk:mismatch:${definition.chunkId}`);
|
||||
@@ -315,9 +314,9 @@ function parseCompatibilityTuple(value: unknown, label: string): CompatibilityTu
|
||||
|
||||
function parseViteManifest(
|
||||
value: unknown,
|
||||
): Readonly<Record<string, ViteManifestEntry>> {
|
||||
): Readonly<Record<string, ViteManifestRouteEntry>> {
|
||||
const document = requireRecord(value, "Vite manifest");
|
||||
const entries: Record<string, ViteManifestEntry> = {};
|
||||
const entries: Record<string, ViteManifestRouteEntry> = {};
|
||||
for (const [key, candidate] of Object.entries(document)) {
|
||||
const entry = requireRecord(candidate, `Vite manifest entry ${key}`);
|
||||
entries[key] = {
|
||||
@@ -326,6 +325,10 @@ function parseViteManifest(
|
||||
...(typeof entry.isDynamicEntry === "boolean"
|
||||
? { isDynamicEntry: entry.isDynamicEntry }
|
||||
: {}),
|
||||
...(Array.isArray(entry.dynamicImports) &&
|
||||
entry.dynamicImports.every((item) => typeof item === "string")
|
||||
? { dynamicImports: entry.dynamicImports as string[] }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return entries;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createRoot } from "react-dom/client";
|
||||
|
||||
import { recordBootFailure } from "../adapters/diagnostics/bounded-diagnostics.ts";
|
||||
import { BootErrorShell } from "../presentation/boundaries/boot-error-shell.tsx";
|
||||
import "../presentation/styles/theme.css";
|
||||
import "pretendard/dist/web/variable/pretendardvariable.css";
|
||||
import "@fontsource/ibm-plex-mono/400.css";
|
||||
import "@fontsource/ibm-plex-mono/500.css";
|
||||
|
||||
@@ -13,7 +13,9 @@ const navigation = [
|
||||
|
||||
export function SiteHeader({ currentPath }: { currentPath?: string }) {
|
||||
const location = useLocation();
|
||||
const activePath = currentPath ?? location.pathname;
|
||||
const activePath =
|
||||
currentPath ??
|
||||
(location.pathname.startsWith("/topics/") ? "/explore" : location.pathname);
|
||||
const menuRef = useRef<HTMLDetailsElement>(null);
|
||||
const summaryRef = useRef<HTMLElement>(null);
|
||||
|
||||
|
||||
@@ -1,53 +1,34 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
const styles = {
|
||||
error: {
|
||||
fontFamily:
|
||||
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
|
||||
height: "100vh",
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
desc: {
|
||||
display: "inline-block",
|
||||
},
|
||||
h1: {
|
||||
display: "inline-block",
|
||||
margin: "0 20px 0 0",
|
||||
padding: "0 23px 0 0",
|
||||
fontSize: 24,
|
||||
fontWeight: 500,
|
||||
verticalAlign: "top",
|
||||
lineHeight: "49px",
|
||||
},
|
||||
h2: {
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
lineHeight: "49px",
|
||||
margin: 0,
|
||||
},
|
||||
} as const satisfies Record<string, CSSProperties>;
|
||||
import { useSourceCompatiblePublicNotFound } from "../public-not-found-layout.tsx";
|
||||
|
||||
export function PublicNotFoundPage() {
|
||||
useSourceCompatiblePublicNotFound();
|
||||
return (
|
||||
<>
|
||||
<title>404: This page could not be found.</title>
|
||||
<div style={styles.error}>
|
||||
<div>
|
||||
<style>{
|
||||
"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"
|
||||
}</style>
|
||||
<h1 className="next-error-h1" style={styles.h1}>
|
||||
404
|
||||
</h1>
|
||||
<div style={styles.desc}>
|
||||
<h2 style={styles.h2}>This page could not be found.</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
html { background: white; color: black; }
|
||||
body {
|
||||
min-width: 0;
|
||||
margin: 8px;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
color: black;
|
||||
font-family: "Times New Roman";
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
text-rendering: auto;
|
||||
word-break: normal;
|
||||
}
|
||||
pre.tech-log-plain-not-found {
|
||||
margin: 13px 0;
|
||||
color: black;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
line-height: normal;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`}</style>
|
||||
<pre className="tech-log-plain-not-found">Not Found</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
} from "react";
|
||||
|
||||
type SetNestedNotFound = (active: boolean) => void;
|
||||
|
||||
export const PublicNotFoundLayoutContext =
|
||||
createContext<SetNestedNotFound | null>(null);
|
||||
|
||||
export function useSourceCompatiblePublicNotFound(): void {
|
||||
const setNestedNotFound = useContext(PublicNotFoundLayoutContext);
|
||||
useLayoutEffect(() => {
|
||||
setNestedNotFound?.(true);
|
||||
return () => setNestedNotFound?.(false);
|
||||
}, [setNestedNotFound]);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { type ReactNode, useCallback, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { publicSiteConfig } from "../../contracts/public-site-config.ts";
|
||||
import { SiteHeader } from "./components/site-header.tsx";
|
||||
import { PublicNotFoundLayoutContext } from "./public-not-found-layout.tsx";
|
||||
|
||||
type PublicShellProps = {
|
||||
children: ReactNode;
|
||||
@@ -10,7 +11,13 @@ type PublicShellProps = {
|
||||
};
|
||||
|
||||
export function PublicShell({ children, currentPath }: PublicShellProps) {
|
||||
const [nestedNotFound, setNestedNotFound] = useState(false);
|
||||
const updateNestedNotFound = useCallback((active: boolean) => {
|
||||
setNestedNotFound(active);
|
||||
}, []);
|
||||
return (
|
||||
<PublicNotFoundLayoutContext.Provider value={updateNestedNotFound}>
|
||||
{nestedNotFound ? children : (
|
||||
<div className="site-frame">
|
||||
<a className="skip-link" href="#main-content">
|
||||
본문으로 건너뛰기
|
||||
@@ -34,5 +41,7 @@ export function PublicShell({ children, currentPath }: PublicShellProps) {
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)}
|
||||
</PublicNotFoundLayoutContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { WorkingCopyInput } from "../../../contracts/studio/contract.ts";
|
||||
import { ContentFormatError } from "../../../domain/content-format/parse-case-content.ts";
|
||||
import { projectWorkingCopy } from "../../../domain/content-format/project-public-render-model.ts";
|
||||
import { PublicRecordRenderer } from "../../shared/public-render/public-record-renderer.tsx";
|
||||
import { useStudio } from "../use-studio.ts";
|
||||
|
||||
type CatalogEntry = components["schemas"]["CatalogEntry"];
|
||||
|
||||
@@ -24,6 +25,7 @@ function resolvePreviewEvidenceAsset(key: string) {
|
||||
}
|
||||
|
||||
export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; catalog: CatalogEntry[] }) {
|
||||
const studio = useStudio();
|
||||
let model: ReturnType<typeof projectWorkingCopy> | null = null;
|
||||
let issues: string[] | null = null;
|
||||
try {
|
||||
@@ -45,7 +47,7 @@ export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; ca
|
||||
model={model!}
|
||||
embedded
|
||||
resolveEvidenceAsset={resolvePreviewEvidenceAsset}
|
||||
resolvePublishedLabel={() => undefined}
|
||||
resolvePublishedLabel={studio.resolvePublishedLabel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -288,7 +288,7 @@ export function PublicPreviewScreen({ documentId }: { documentId: string }) {
|
||||
model={previewDetail.preview.renderModel}
|
||||
embedded
|
||||
resolveEvidenceAsset={resolvePreviewEvidenceAsset}
|
||||
resolvePublishedLabel={() => undefined}
|
||||
resolvePublishedLabel={studio.resolvePublishedLabel}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable react-hooks/set-state-in-effect -- a changed event or retry intentionally enters a fresh loading state. */
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
@@ -50,7 +48,8 @@ export function PublicationEventPreviewScreen({
|
||||
publicationEventId: string;
|
||||
classes?: PublicationFlowClasses;
|
||||
}) {
|
||||
const { gateway } = useStudio();
|
||||
const studio = useStudio();
|
||||
const { gateway } = studio;
|
||||
const [retry, setRetry] = useState(0);
|
||||
const [state, setState] = useState<
|
||||
| { status: "LOADING" }
|
||||
@@ -168,7 +167,7 @@ export function PublicationEventPreviewScreen({
|
||||
model={renderModel}
|
||||
embedded
|
||||
resolveEvidenceAsset={resolveEvidenceAsset}
|
||||
resolvePublishedLabel={() => undefined}
|
||||
resolvePublishedLabel={studio.resolvePublishedLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const defaultPublicationFlowClasses = {
|
||||
import styles from "../../styles/publication-flow.module.css";
|
||||
|
||||
const publicationFlowClassContract = {
|
||||
page: "studio-publication-page",
|
||||
snapshotPage: "studio-snapshot-page",
|
||||
heading: "studio-publication-heading",
|
||||
@@ -35,4 +37,11 @@ export const defaultPublicationFlowClasses = {
|
||||
snapshotDocument: "studio-publication-snapshot-document",
|
||||
} as const;
|
||||
|
||||
export type PublicationFlowClasses = Record<keyof typeof defaultPublicationFlowClasses, string>;
|
||||
export type PublicationFlowClasses = Record<
|
||||
keyof typeof publicationFlowClassContract,
|
||||
string
|
||||
>;
|
||||
|
||||
export const defaultPublicationFlowClasses = Object.fromEntries(
|
||||
Object.keys(publicationFlowClassContract).map((key) => [key, styles[key]]),
|
||||
) as PublicationFlowClasses;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable react-hooks/set-state-in-effect -- a changed history query intentionally enters a fresh loading state. */
|
||||
|
||||
import { useEffect, useRef, useState, type FormEvent } from "react";
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable react-hooks/set-state-in-effect -- a changed document or retry intentionally enters a fresh loading state. */
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
import { isStudioGatewayError } from "../../application/ports/studio-gateway-error.ts";
|
||||
import type { StudioGateway } from "../../application/ports/studio-gateway.ts";
|
||||
import type { ResolvePublishedLabel } from "../../domain/public-render-content.ts";
|
||||
import type {
|
||||
WorkingCopy,
|
||||
WorkingCopyInput,
|
||||
@@ -24,6 +25,7 @@ import { useBeforeUnload } from "./components/use-before-unload.ts";
|
||||
type StudioProviderProps = Readonly<{
|
||||
children: ReactNode;
|
||||
createGateway: () => StudioGateway;
|
||||
resolvePublishedLabel?: ResolvePublishedLabel;
|
||||
now?: () => Date;
|
||||
navigate?: (href: string) => void;
|
||||
}>;
|
||||
@@ -41,9 +43,12 @@ function defaultNavigate(href: string): void {
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
}
|
||||
|
||||
const missingPublishedLabel: ResolvePublishedLabel = () => undefined;
|
||||
|
||||
export function StudioProvider({
|
||||
children,
|
||||
createGateway,
|
||||
resolvePublishedLabel = missingPublishedLabel,
|
||||
now = () => new Date("2026-08-14T01:00:00.000Z"),
|
||||
navigate = defaultNavigate,
|
||||
}: StudioProviderProps) {
|
||||
@@ -143,6 +148,7 @@ export function StudioProvider({
|
||||
const value = useMemo<StudioContextValue>(
|
||||
() => ({
|
||||
gateway,
|
||||
resolvePublishedLabel,
|
||||
now,
|
||||
editor,
|
||||
requestAnnouncement,
|
||||
@@ -161,6 +167,7 @@ export function StudioProvider({
|
||||
navigateInternal,
|
||||
now,
|
||||
requestAnnouncement,
|
||||
resolvePublishedLabel,
|
||||
setEditorStatus,
|
||||
updateEditorDraft,
|
||||
],
|
||||
|
||||
@@ -47,6 +47,14 @@ export function StudioShell({ children }: StudioShellProps) {
|
||||
() => application.features.get(TECH_LOG_FEATURE_ID).createStudioGateway(),
|
||||
[application],
|
||||
);
|
||||
const resolvePublishedLabel = useCallback(
|
||||
(path: string) =>
|
||||
application.features
|
||||
.get(TECH_LOG_FEATURE_ID)
|
||||
.publicContent.listRecords()
|
||||
.find((record) => record.path === path)?.publishedLabel,
|
||||
[application],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const resetPersistedSession = (event: PageTransitionEvent) => {
|
||||
@@ -61,6 +69,7 @@ export function StudioShell({ children }: StudioShellProps) {
|
||||
<StudioProvider
|
||||
key={generation}
|
||||
createGateway={createGateway}
|
||||
resolvePublishedLabel={resolvePublishedLabel}
|
||||
navigate={navigateInternal}
|
||||
>
|
||||
<StudioFrame>{children}</StudioFrame>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
|
||||
import type { StudioGateway } from "../../application/ports/studio-gateway.ts";
|
||||
import type { ResolvePublishedLabel } from "../../domain/public-render-content.ts";
|
||||
import type {
|
||||
WorkingCopy,
|
||||
WorkingCopyInput,
|
||||
@@ -17,6 +18,7 @@ export type StudioEditorState = Readonly<{
|
||||
|
||||
export type StudioContextValue = Readonly<{
|
||||
gateway: StudioGateway;
|
||||
resolvePublishedLabel: ResolvePublishedLabel;
|
||||
now(): Date;
|
||||
editor: StudioEditorState | null;
|
||||
requestAnnouncement: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--canvas: #f7f7f5;
|
||||
|
||||
@@ -119,8 +119,6 @@ function RouteLifecycle({
|
||||
title: resolve(`route.${definition.routeId}.title`),
|
||||
appName: message("common.appName"),
|
||||
});
|
||||
const main = document.getElementById("main-content");
|
||||
main?.focus({ preventScroll: true });
|
||||
try {
|
||||
if (!navigator.userAgent.toLowerCase().includes("jsdom")) {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||
|
||||
@@ -343,20 +343,12 @@ describe("generic application router", () => {
|
||||
expect(window.location.pathname).toBe("/projects");
|
||||
});
|
||||
|
||||
it("renders the Public shell and TechLog not-found route", async () => {
|
||||
it("renders the source-compatible plain TechLog not-found response", async () => {
|
||||
window.history.pushState({}, "", "/missing");
|
||||
renderRouter();
|
||||
|
||||
expect(
|
||||
await screen.findByRole("heading", {
|
||||
name: "404",
|
||||
level: 1,
|
||||
}),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "This page could not be found." }),
|
||||
).toBeVisible();
|
||||
expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
|
||||
expect(screen.queryByRole("navigation", { name: "주요 탐색" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps Studio routes inside the persistent Studio layout", async () => {
|
||||
|
||||
@@ -1,58 +1,40 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
|
||||
|
||||
for (const route of Object.values(ROUTE_REGISTRY).map((definition) => {
|
||||
if (definition.path === "*") return "/not-found";
|
||||
return definition.path.replace(":resourceId", "reference-1");
|
||||
})) {
|
||||
test(`@a11y ${route} has no critical or serious axe violations`, async ({
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import {
|
||||
gotoTechLog,
|
||||
TECH_LOG_CANONICAL_ROUTES,
|
||||
} from "../support/browser/tech-log-fixtures.ts";
|
||||
|
||||
for (const route of TECH_LOG_CANONICAL_ROUTES) {
|
||||
test(`@a11y ${route.routeId} has no critical or serious Axe violations`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(route);
|
||||
await expect(page.getByRole("main")).toBeVisible();
|
||||
await gotoTechLog(page, route.path);
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
||||
.analyze();
|
||||
const blocking = results.violations.filter((violation) =>
|
||||
["critical", "serious"].includes(violation.impact ?? ""),
|
||||
);
|
||||
expect(blocking).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test("@a11y keyboard reaches the primary route action with visible focus", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
const action = page.getByRole("link", { name: "플랫폼 구성 보기" });
|
||||
await expect(action).toBeVisible();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(action).toBeFocused();
|
||||
await expect(action).toHaveCSS("outline-style", "solid");
|
||||
});
|
||||
|
||||
test("@a11y reduced-motion policy disables long animation", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto("/");
|
||||
const duration = await page
|
||||
.locator("body")
|
||||
.evaluate((body) => getComputedStyle(body).animationDuration);
|
||||
expect(["0s", "0.00001s", "1e-05s"]).toContain(duration);
|
||||
});
|
||||
|
||||
test("@a11y opened design-system dialog has no blocking violations", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/ui");
|
||||
await page.getByRole("button", { name: "모달 열기" }).click();
|
||||
const results = await new AxeBuilder({ page })
|
||||
.include(".ui-dialog")
|
||||
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
||||
.analyze();
|
||||
expect(
|
||||
results.violations.filter((violation) =>
|
||||
["critical", "serious"].includes(violation.impact ?? ""),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test("@a11y keyboard reaches a visible Public navigation focus indicator", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoTechLog(page, "/");
|
||||
const action = page.getByRole("link", { name: "TechLog 홈" });
|
||||
await action.focus();
|
||||
await expect(action).toBeFocused();
|
||||
await expect(action).toHaveCSS("outline-style", "solid");
|
||||
});
|
||||
|
||||
test("@a11y reduced-motion preference leaves no running document animations", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoTechLog(page, "/");
|
||||
expect(await page.evaluate(() => document.getAnimations().length)).toBe(0);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import { gotoTechLog } from "../support/browser/tech-log-fixtures.ts";
|
||||
|
||||
test("boots the public app shell", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await gotoTechLog(page, "/");
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
|
||||
"Tech Log",
|
||||
);
|
||||
@@ -9,36 +10,32 @@ test("boots the public app shell", async ({ page }) => {
|
||||
await expect(page.getByRole("main")).toBeVisible();
|
||||
});
|
||||
|
||||
test("navigates to a registry-backed example without a page reload", async ({
|
||||
test("navigates to a registry-backed Public route without a page reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await gotoTechLog(page, "/");
|
||||
await page
|
||||
.getByRole("navigation", { name: "주요 탐색" })
|
||||
.getByRole("link", { name: "화면 상태" })
|
||||
.getByRole("link", { name: "프로젝트" })
|
||||
.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/examples\/states$/);
|
||||
await expect(page).toHaveURL(/\/projects$/);
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 1, name: "화면 상태" }),
|
||||
).toBeFocused();
|
||||
page.getByRole("heading", { level: 1, name: "프로젝트" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("provides an escape-dismissible mobile navigation", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/");
|
||||
const menu = page.getByRole("button", { name: "메뉴", exact: true });
|
||||
await gotoTechLog(page, "/");
|
||||
const menu = page.locator("details.mobile-nav > summary");
|
||||
|
||||
await menu.click();
|
||||
await expect(menu).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||
await expect(page.locator(".ui-drawer")).toHaveJSProperty("open", true);
|
||||
expect(
|
||||
await page.locator(".ui-drawer").evaluate((drawer) => drawer.matches(":modal")),
|
||||
).toBe(true);
|
||||
await expect(page.locator("details.mobile-nav")).toHaveAttribute("open", "");
|
||||
await expect(page.getByRole("navigation", { name: "모바일 주요 탐색" })).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(menu).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeHidden();
|
||||
await expect(page.locator("details.mobile-nav")).not.toHaveAttribute("open", "");
|
||||
await expect(page.getByRole("navigation", { name: "모바일 주요 탐색" })).toBeHidden();
|
||||
await expect(menu).toBeFocused();
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("boots and navigates the compact production shell", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("main")).toBeVisible();
|
||||
const menu = page.getByRole("button", { name: "메뉴", exact: true });
|
||||
await expect(menu).toHaveCSS("min-width", "44px");
|
||||
await menu.click();
|
||||
const navigation = page.getByRole("navigation", { name: "주요 탐색" });
|
||||
await expect(navigation).toBeVisible();
|
||||
// The drawer is a non-modal dialog, so page content stays in the
|
||||
// accessibility tree while it is open. Scope to the navigation and match the
|
||||
// whole name, or a route call to action on the page behind it also matches.
|
||||
await navigation
|
||||
.getByRole("link", { name: "UI 구성요소", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL(/\/examples\/ui$/);
|
||||
await expect(page.locator("html")).toHaveAttribute("data-build-id", "local-build");
|
||||
const overflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth - window.innerWidth,
|
||||
);
|
||||
expect(overflow).toBeLessThanOrEqual(1);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("runs menu typeahead, tabs and duplicate toast interactions", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/ui");
|
||||
|
||||
const menuTrigger = page.getByRole("button", { name: "작업 메뉴" });
|
||||
await menuTrigger.focus();
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await page.keyboard.type("toast");
|
||||
const toastItem = page.getByRole("menuitem", { name: "Toast 표시" });
|
||||
await expect(toastItem).toBeFocused();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page.getByText("예제가 저장되었습니다.")).toBeVisible();
|
||||
await expect(menuTrigger).toBeFocused();
|
||||
|
||||
const tokenTab = page.getByRole("tab", { name: "토큰" });
|
||||
await tokenTab.focus();
|
||||
await page.keyboard.press("ArrowRight");
|
||||
const primitiveTab = page.getByRole("tab", { name: "프리미티브" });
|
||||
await expect(primitiveTab).toBeFocused();
|
||||
await expect(primitiveTab).toHaveAttribute("aria-selected", "false");
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(primitiveTab).toHaveAttribute("aria-selected", "true");
|
||||
await expect(
|
||||
page.getByRole("tabpanel", { name: "프리미티브" }),
|
||||
).toContainText("native semantics");
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("switches the shell locale and keeps pseudo-locale copy within compact layout", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 320, height: 720 });
|
||||
await page.goto("/");
|
||||
|
||||
const locale = page.getByRole("combobox", { name: "언어" });
|
||||
await locale.selectOption("en-US");
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "en-US");
|
||||
await expect(page.locator("html")).toHaveAttribute("dir", "ltr");
|
||||
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
||||
|
||||
await page.getByRole("combobox", { name: "Language" }).selectOption("en-XA");
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "en-XA");
|
||||
const dimensions = await page.evaluate(() => ({
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
}));
|
||||
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
|
||||
await expect(page.getByRole("button", { name: /Ménú/ })).toBeVisible();
|
||||
});
|
||||
|
||||
test("applies RTL to the shell and modal navigation without changing action semantics", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/");
|
||||
await page.getByRole("combobox", { name: "언어" }).selectOption("ar-EG");
|
||||
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "ar-EG");
|
||||
await expect(page.locator("html")).toHaveAttribute("dir", "rtl");
|
||||
const trigger = page.getByRole("button", { name: "Menu" });
|
||||
await trigger.click();
|
||||
await expect(page.getByRole("dialog", { name: "Menu" })).toBeVisible();
|
||||
await expect(page.getByRole("navigation", { name: "Primary navigation" }))
|
||||
.toHaveCount(1);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
@@ -1,36 +1,36 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import {
|
||||
gotoTechLog,
|
||||
horizontalOverflow,
|
||||
TECH_LOG_CANONICAL_ROUTES,
|
||||
} from "../support/browser/tech-log-fixtures.ts";
|
||||
|
||||
test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 320, height: 720 });
|
||||
await page.goto("/examples/ui");
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 1, name: "UI 구성요소" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "메뉴", exact: true }),
|
||||
).toBeVisible();
|
||||
for (const route of TECH_LOG_CANONICAL_ROUTES) {
|
||||
for (const width of [360, 1440] as const) {
|
||||
test(`${route.routeId} has no horizontal overflow at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await gotoTechLog(page, route.path);
|
||||
const dimensions = await horizontalOverflow(page);
|
||||
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
|
||||
if (route.routeId !== "NOT_FOUND") {
|
||||
await expect(page.getByRole("main")).toBeVisible();
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByText("Not Found", { exact: true })).toBeVisible();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const viewport = await page.evaluate(() => ({
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
}));
|
||||
expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth);
|
||||
});
|
||||
|
||||
test("keeps desktop navigation and two-column examples at wide viewports", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto("/examples/ui");
|
||||
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "메뉴", exact: true }),
|
||||
).toBeHidden();
|
||||
await expect(page.locator(".component-grid--two").first()).toHaveCSS(
|
||||
"grid-template-columns",
|
||||
/.+px .+px/,
|
||||
test("compact Public controls retain 44px targets", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await gotoTechLog(page, "/");
|
||||
await expect(page.locator("details.mobile-nav > summary")).toHaveCSS(
|
||||
"min-height",
|
||||
"44px",
|
||||
);
|
||||
await expect(page.getByRole("button", { name: "검색 열기" })).toHaveCSS(
|
||||
"min-height",
|
||||
"44px",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import { gotoTechLog } from "../support/browser/tech-log-fixtures.ts";
|
||||
|
||||
function blockingViolations(results: Awaited<ReturnType<AxeBuilder["analyze"]>>) {
|
||||
return results.violations.filter((violation) =>
|
||||
["critical", "serious"].includes(violation.impact ?? ""),
|
||||
);
|
||||
}
|
||||
|
||||
test("Public shell exposes one main landmark, one h1, labelled navigation, and current link", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoTechLog(page, "/projects");
|
||||
await expect(page.getByRole("main")).toHaveCount(1);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveCount(1);
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "프로젝트", exact: true })).toHaveAttribute(
|
||||
"aria-current",
|
||||
"page",
|
||||
);
|
||||
});
|
||||
|
||||
test("Studio mobile navigation is keyboard reachable and exposes its expanded state", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await gotoTechLog(page, "/studio");
|
||||
const trigger = page.locator(".studio-menu-trigger");
|
||||
await trigger.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(page.getByRole("navigation", { name: "Studio 모바일 탐색" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("dirty-leave dialog is labelled, traps focus, and restores the invoking link", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoTechLog(
|
||||
page,
|
||||
"/studio/documents/11111111-1111-4111-8111-111111111111/edit",
|
||||
);
|
||||
await page.getByLabel("요약").fill("저장하지 않은 접근성 검증 변경");
|
||||
const trigger = page.getByRole("link", { name: "게시 기록", exact: true }).first();
|
||||
await trigger.click();
|
||||
const dialog = page.getByRole("dialog", { name: "저장하지 않은 변경" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByRole("button", { name: "이 페이지에 머무르기" })).toBeFocused();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
|
||||
test("open Public search and Studio leave dialogs have no blocking Axe findings", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoTechLog(page, "/");
|
||||
await page.getByRole("button", { name: "검색 열기" }).click();
|
||||
let results = await new AxeBuilder({ page })
|
||||
.include("dialog")
|
||||
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
||||
.analyze();
|
||||
expect(blockingViolations(results)).toEqual([]);
|
||||
|
||||
await gotoTechLog(
|
||||
page,
|
||||
"/studio/documents/11111111-1111-4111-8111-111111111111/edit",
|
||||
);
|
||||
await page.getByLabel("요약").fill("dialog axe state");
|
||||
await page.getByRole("link", { name: "게시 기록", exact: true }).first().click();
|
||||
results = await new AxeBuilder({ page })
|
||||
.include("dialog")
|
||||
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
||||
.analyze();
|
||||
expect(blockingViolations(results)).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import {
|
||||
gotoTechLog,
|
||||
TECH_LOG_PUBLIC_FIXTURE_PATHS,
|
||||
} from "../support/browser/tech-log-fixtures.ts";
|
||||
|
||||
for (const path of TECH_LOG_PUBLIC_FIXTURE_PATHS) {
|
||||
test(`renders known Public fixture ${path}`, async ({ page }) => {
|
||||
await gotoTechLog(page, path);
|
||||
await expect(page.locator(".site-frame")).toBeVisible();
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
await expect(page.getByText("404", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
test("search dialog traps focus, closes with Escape, and restores its trigger", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoTechLog(page, "/");
|
||||
const trigger = page.getByRole("button", { name: "TechLog 검색 열기" });
|
||||
await trigger.click();
|
||||
const dialog = page.getByRole("dialog", { name: "TechLog 검색" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByRole("searchbox", { name: "검색어" })).toBeFocused();
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Shift+Tab");
|
||||
await expect(dialog.getByRole("searchbox", { name: "검색어" })).toBeFocused();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
|
||||
test("search and explore navigation preserve canonical URL state", async ({ page }) => {
|
||||
await gotoTechLog(page, "/explore");
|
||||
await page.getByLabel("유형").selectOption("CASE");
|
||||
await page.getByRole("button", { name: "적용" }).click();
|
||||
await expect(page).toHaveURL(/\/explore\?type=CASE$/);
|
||||
|
||||
await page.getByRole("button", { name: "TechLog 검색 열기" }).click();
|
||||
await page.getByRole("searchbox", { name: "검색어" }).fill("JPA");
|
||||
await page.getByRole("link", { name: /전체 검색 결과 보기/ }).click();
|
||||
await expect(page).toHaveURL(/\/search\?q=JPA$/);
|
||||
await expect(page.getByRole("heading", { level: 1, name: "검색" })).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import {
|
||||
gotoTechLog,
|
||||
horizontalOverflow,
|
||||
TECH_LOG_BREAKPOINT_WIDTHS,
|
||||
} from "../support/browser/tech-log-fixtures.ts";
|
||||
|
||||
for (const width of TECH_LOG_BREAKPOINT_WIDTHS) {
|
||||
test(`Public shell has no overflow and preserves the ${width}px breakpoint contract`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await gotoTechLog(page, "/");
|
||||
const dimensions = await horizontalOverflow(page);
|
||||
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
|
||||
|
||||
if (width <= 1050) {
|
||||
await expect(page.locator("details.mobile-nav > summary")).toBeVisible();
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeHidden();
|
||||
} else {
|
||||
await expect(page.locator("details.mobile-nav > summary")).toBeHidden();
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const width of [1050, 1024, 980, 900, 820, 768, 767, 420, 390, 375] as const) {
|
||||
test(`Studio editor has no overflow and preserves the ${width}px editor transition`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
await gotoTechLog(
|
||||
page,
|
||||
"/studio/documents/11111111-1111-4111-8111-111111111111/edit",
|
||||
);
|
||||
const dimensions = await horizontalOverflow(page);
|
||||
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "저장" })).toBeVisible();
|
||||
});
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("persists an explicit color scheme through the storage contract", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
const selector = page.getByRole("combobox", { name: "색상 테마" });
|
||||
|
||||
await selector.selectOption("dark");
|
||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||
await expect(page.locator("html")).toHaveCSS("color-scheme", "dark");
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
||||
.analyze();
|
||||
expect(
|
||||
results.violations.filter((violation) =>
|
||||
["critical", "serious"].includes(violation.impact ?? ""),
|
||||
),
|
||||
).toEqual([]);
|
||||
|
||||
await page.reload();
|
||||
await expect(selector).toHaveValue("dark");
|
||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||
});
|
||||
|
||||
test("tracks operating-system changes while system preference is selected", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.emulateMedia({ colorScheme: "dark" });
|
||||
await page.goto("/");
|
||||
const selector = page.getByRole("combobox", { name: "색상 테마" });
|
||||
await selector.selectOption("system");
|
||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
|
||||
});
|
||||
@@ -372,20 +372,18 @@ describe("TechLog explore discovery", () => {
|
||||
).toHaveAttribute("href", "/explore");
|
||||
});
|
||||
|
||||
it("renders an unknown kind through the registered Public not-found runtime", () => {
|
||||
it("renders an unknown kind as the source production plain 404", async () => {
|
||||
const { router, container } = renderDiscoveryRoute(
|
||||
"TECH_LOG_EXPLORE_KIND",
|
||||
"/explore/unknown",
|
||||
);
|
||||
|
||||
expect(router.state.location.pathname).toBe("/explore/unknown");
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "페이지를 찾을 수 없습니다." }),
|
||||
).toBeVisible();
|
||||
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(container.querySelector(".site-frame")).not.toBeNull();
|
||||
expect(container.querySelector(".site-frame")).toBeNull();
|
||||
});
|
||||
|
||||
it("recovers a rejecting registered not-found runtime under its own chunk contract", async () => {
|
||||
|
||||
@@ -337,12 +337,12 @@ describe("TechLog topics and Public not-found routing", () => {
|
||||
["TECH_LOG_REFERENCE" as const, "/references/not-registered"],
|
||||
["TECH_LOG_QUESTION" as const, "/questions/not-registered"],
|
||||
["TECH_LOG_TOPIC" as const, "/topics/not-registered"],
|
||||
])("uses the registered in-shell Public not-found for %s", (routeId, path) => {
|
||||
])("uses the source production plain 404 for %s", async (routeId, path) => {
|
||||
const { container, router } = renderDocumentRoute(routeId, path);
|
||||
|
||||
expect(router.state.location.pathname).toBe(path);
|
||||
expect(screen.getByRole("heading", { name: "페이지를 찾을 수 없습니다." })).toBeVisible();
|
||||
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
|
||||
expect(screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." })).not.toBeInTheDocument();
|
||||
expect(container.querySelector(".site-frame")).not.toBeNull();
|
||||
expect(container.querySelector(".site-frame")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,20 +360,25 @@ describe("TechLog Public not-found runtime", () => {
|
||||
["TECH_LOG_PROJECT_RECORDS" as const, "/projects/missing-project/records"],
|
||||
["TECH_LOG_RELEASE" as const, "/releases/9.9.9"],
|
||||
["TECH_LOG_CASE" as const, "/cases/not-registered"],
|
||||
["NOT_FOUND" as const, "/definitely-not-a-product-route"],
|
||||
])("renders exact in-shell Public fallback copy for %s", (routeId, path) => {
|
||||
])("renders the source-compatible plain fallback for %s", async (routeId, path) => {
|
||||
const { container, router } = renderPublicRoute(routeId, path);
|
||||
|
||||
expect(router.state.location.pathname).toBe(path);
|
||||
expect(screen.getByRole("heading", { level: 1, name: "404" })).toHaveClass(
|
||||
"next-error-h1",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 2, name: "This page could not be found." }),
|
||||
).toBeVisible();
|
||||
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(container.querySelector(".site-frame")).not.toBeNull();
|
||||
expect(container.querySelector(".site-frame")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the source-compatible plain fallback for the Public catch-all", () => {
|
||||
const { container, router } = renderPublicRoute(
|
||||
"NOT_FOUND",
|
||||
"/definitely-not-a-product-route",
|
||||
);
|
||||
|
||||
expect(router.state.location.pathname).toBe("/definitely-not-a-product-route");
|
||||
expect(screen.getByText("Not Found", { exact: true })).toBeVisible();
|
||||
expect(container.querySelector(".site-frame")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
export const TECH_LOG_FIXED_TIME = "2026-08-14T01:00:00.000Z";
|
||||
|
||||
export const TECH_LOG_CANONICAL_ROUTES = [
|
||||
{ routeId: "TECH_LOG_HOME", path: "/", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_EXPLORE", path: "/explore", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_EXPLORE_KIND", path: "/explore/cases", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_CASE", path: "/cases/collection-fetch-join-pagination", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_REFERENCE", path: "/references/state-and-nonce-boundary", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_QUESTION", path: "/questions/validate-edge-token-again", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_TOPIC", path: "/topics/jpa", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_PROJECTS", path: "/projects", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_PROJECT", path: "/projects/backend-skeleton", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_PROJECT_RECORDS", path: "/projects/backend-skeleton/records", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_PROJECT_DECISIONS", path: "/projects/backend-skeleton/decisions", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_PROJECT_ACTIVITY", path: "/projects/backend-skeleton/activity", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_RELEASES", path: "/releases", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_RELEASE", path: "/releases/0.1.0", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_PROFILE", path: "/profile", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_SEARCH", path: "/search?q=JPA", layout: "PUBLIC" },
|
||||
{ routeId: "TECH_LOG_STUDIO_HOME", path: "/studio", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_DOCUMENTS", path: "/studio/documents", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_DOCUMENT_NEW", path: "/studio/documents/new", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_DOCUMENT_EDIT", path: "/studio/documents/11111111-1111-4111-8111-111111111111/edit", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_DOCUMENT_VALIDATION", path: "/studio/documents/11111111-1111-4111-8111-111111111113/validation", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_DOCUMENT_PREVIEW", path: "/studio/documents/11111111-1111-4111-8111-111111111111/preview", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_DOCUMENT_PUBLISH", path: "/studio/documents/11111111-1111-4111-8111-111111111113/publish", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_PUBLICATIONS", path: "/studio/publications", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_PUBLICATION_PREVIEW", path: "/studio/publications/66666666-6666-4666-8666-666666666661/preview", layout: "STUDIO" },
|
||||
{ routeId: "TECH_LOG_STUDIO_NOT_FOUND", path: "/studio/unknown-screen", layout: "STUDIO" },
|
||||
{ routeId: "NOT_FOUND", path: "/unknown-public-screen", layout: "PUBLIC" },
|
||||
] as const;
|
||||
|
||||
export const TECH_LOG_PUBLIC_FIXTURE_PATHS = [
|
||||
"/cases/collection-fetch-join-pagination",
|
||||
"/cases/redis-adapter-ttl-boundary",
|
||||
"/references/state-and-nonce-boundary",
|
||||
"/references/jpa-list-fetch-strategy",
|
||||
"/questions/validate-edge-token-again",
|
||||
"/questions/collection-fetch-join-with-pagination",
|
||||
"/topics/jpa",
|
||||
"/topics/redis",
|
||||
"/topics/authentication",
|
||||
"/projects/backend-skeleton",
|
||||
"/projects/backend-skeleton/records",
|
||||
"/projects/backend-skeleton/decisions",
|
||||
"/projects/backend-skeleton/activity",
|
||||
"/projects/auth-lab",
|
||||
"/projects/auth-lab/records",
|
||||
"/projects/auth-lab/decisions",
|
||||
"/projects/auth-lab/activity",
|
||||
"/releases/0.1.0",
|
||||
] as const;
|
||||
|
||||
export const TECH_LOG_UNKNOWN_PUBLIC_PATHS = [
|
||||
["explore-kind", "/explore/unknown-kind"],
|
||||
["case", "/cases/not-registered"],
|
||||
["reference", "/references/not-registered"],
|
||||
["question", "/questions/not-registered"],
|
||||
["topic", "/topics/not-registered"],
|
||||
["project", "/projects/missing-project"],
|
||||
["project-records", "/projects/missing-project/records"],
|
||||
["project-decisions", "/projects/missing-project/decisions"],
|
||||
["project-activity", "/projects/missing-project/activity"],
|
||||
["release", "/releases/9.9.9"],
|
||||
] as const;
|
||||
|
||||
export const TECH_LOG_STUDIO_STATE_PATHS = [
|
||||
["dashboard", "/studio"],
|
||||
["document-list", "/studio/documents"],
|
||||
["new-document", "/studio/documents/new"],
|
||||
["case-editor", "/studio/documents/11111111-1111-4111-8111-111111111111/edit"],
|
||||
["reference-editor", "/studio/documents/11111111-1111-4111-8111-111111111113/edit"],
|
||||
["question-editor", "/studio/documents/11111111-1111-4111-8111-111111111114/edit"],
|
||||
["conflict-editor", "/studio/documents/11111111-1111-4111-8111-111111111115/edit"],
|
||||
["valid-validation", "/studio/documents/11111111-1111-4111-8111-111111111113/validation"],
|
||||
["invalid-validation", "/studio/documents/11111111-1111-4111-8111-111111111114/validation"],
|
||||
["current-preview", "/studio/documents/11111111-1111-4111-8111-111111111111/preview"],
|
||||
["missing-preview", "/studio/documents/11111111-1111-4111-8111-111111111113/preview"],
|
||||
["expired-preview", "/studio/documents/11111111-1111-4111-8111-111111111117/preview"],
|
||||
["publish-ready", "/studio/documents/11111111-1111-4111-8111-111111111113/publish"],
|
||||
["publish-blocked", "/studio/documents/11111111-1111-4111-8111-111111111114/publish"],
|
||||
["publications", "/studio/publications"],
|
||||
["publication-snapshot", "/studio/publications/66666666-6666-4666-8666-666666666661/preview"],
|
||||
["missing-document", "/studio/documents/99999999-9999-4999-8999-999999999999/edit"],
|
||||
["missing-publication", "/studio/publications/99999999-9999-4999-8999-999999999999/preview"],
|
||||
["unknown-studio-route", "/studio/unknown-screen"],
|
||||
] as const;
|
||||
|
||||
export const TECH_LOG_BREAKPOINT_WIDTHS = [
|
||||
1180,
|
||||
1179,
|
||||
1050,
|
||||
1024,
|
||||
980,
|
||||
900,
|
||||
820,
|
||||
768,
|
||||
767,
|
||||
420,
|
||||
390,
|
||||
375,
|
||||
] as const;
|
||||
|
||||
export async function prepareTechLogPage(page: Page) {
|
||||
await page.clock.setFixedTime(new Date(TECH_LOG_FIXED_TIME));
|
||||
await page.emulateMedia({ colorScheme: "light", reducedMotion: "reduce" });
|
||||
}
|
||||
|
||||
export async function settleTechLogPage(page: Page) {
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
});
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
*, *::before, *::after {
|
||||
animation-delay: 0s !important;
|
||||
animation-duration: 0s !important;
|
||||
caret-color: transparent !important;
|
||||
transition-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function gotoTechLog(page: Page, path: string) {
|
||||
await prepareTechLogPage(page);
|
||||
await page.goto(path);
|
||||
await page.locator("body").waitFor({ state: "visible" });
|
||||
await settleTechLogPage(page);
|
||||
}
|
||||
|
||||
export async function horizontalOverflow(page: Page) {
|
||||
return page.evaluate(() => ({
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// @ts-nocheck -- standalone evidence runner executed directly with tsx.
|
||||
import { createRequire } from "node:module";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
import {
|
||||
TECH_LOG_BREAKPOINT_WIDTHS,
|
||||
TECH_LOG_CANONICAL_ROUTES,
|
||||
TECH_LOG_FIXED_TIME,
|
||||
TECH_LOG_STUDIO_STATE_PATHS,
|
||||
TECH_LOG_UNKNOWN_PUBLIC_PATHS,
|
||||
} from "./tech-log-fixtures.ts";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { PNG } = require(
|
||||
resolve("node_modules/.pnpm/playwright-core@1.62.0/node_modules/playwright-core/lib/utilsBundle.js"),
|
||||
);
|
||||
|
||||
const sourceBaseUrl = process.env.TECH_LOG_SOURCE_URL ?? "http://127.0.0.1:4175";
|
||||
const targetBaseUrl = process.env.TECH_LOG_TARGET_URL ?? "http://127.0.0.1:4174";
|
||||
const outputPath = process.env.TECH_LOG_PARITY_OUTPUT ?? "artifacts/quality/tech-log-source-parity.json";
|
||||
const fixedDate = new Date(TECH_LOG_FIXED_TIME);
|
||||
const noMotionCss = `
|
||||
*, *::before, *::after {
|
||||
animation-delay: 0s !important;
|
||||
animation-duration: 0s !important;
|
||||
caret-color: transparent !important;
|
||||
transition-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const allCases = [
|
||||
...TECH_LOG_CANONICAL_ROUTES.flatMap(({ routeId, path }) =>
|
||||
[360, 1440].map((width) => ({ name: `${routeId}-${width}`, path, width }))),
|
||||
...TECH_LOG_BREAKPOINT_WIDTHS.map((width) => ({
|
||||
name: `TECH_LOG_HOME-BREAKPOINT-${width}`,
|
||||
path: "/",
|
||||
width,
|
||||
})),
|
||||
...TECH_LOG_STUDIO_STATE_PATHS.map(([state, path]) => ({
|
||||
name: `TECH_LOG_STUDIO_STATE-${state}`,
|
||||
path,
|
||||
width: 1440,
|
||||
})),
|
||||
...TECH_LOG_UNKNOWN_PUBLIC_PATHS.flatMap(([route, path]) =>
|
||||
[360, 1440].map((width) => ({
|
||||
name: `TECH_LOG_UNKNOWN_${route.toUpperCase().replaceAll("-", "_")}-${width}`,
|
||||
path,
|
||||
width,
|
||||
}))),
|
||||
{ name: "TECH_LOG_SEARCH_DIALOG", path: "/", width: 390, action: "search-dialog" },
|
||||
{ name: "TECH_LOG_STUDIO_MOBILE_MENU", path: "/studio", width: 390, action: "studio-menu" },
|
||||
{
|
||||
name: "TECH_LOG_STUDIO_IMMEDIATE_PREVIEW",
|
||||
path: "/studio/documents/11111111-1111-4111-8111-111111111111/edit",
|
||||
width: 1440,
|
||||
action: "immediate-preview",
|
||||
},
|
||||
{
|
||||
name: "TECH_LOG_STUDIO_DIRTY_DIALOG",
|
||||
path: "/studio/documents/11111111-1111-4111-8111-111111111111/edit",
|
||||
width: 1440,
|
||||
action: "dirty-dialog",
|
||||
},
|
||||
{
|
||||
name: "TECH_LOG_STUDIO_CURRENT_PREVIEW_CREATED",
|
||||
path: "/studio/documents/11111111-1111-4111-8111-111111111113/preview",
|
||||
width: 1440,
|
||||
action: "create-preview",
|
||||
},
|
||||
{
|
||||
name: "TECH_LOG_STUDIO_UNPUBLISH_DIALOG",
|
||||
path: "/studio/publications",
|
||||
width: 1440,
|
||||
action: "unpublish-dialog",
|
||||
},
|
||||
{
|
||||
name: "TECH_LOG_STUDIO_WARNING_ACKNOWLEDGED",
|
||||
path: "/studio/documents/11111111-1111-4111-8111-111111111116/validation",
|
||||
width: 1440,
|
||||
action: "warning-acknowledged",
|
||||
},
|
||||
];
|
||||
const caseFilter = process.env.TECH_LOG_PARITY_CASE;
|
||||
const cases = caseFilter
|
||||
? allCases.filter(({ name }) => name.includes(caseFilter))
|
||||
: allCases;
|
||||
const diagnosticDirectory = process.env.TECH_LOG_PARITY_DIAGNOSTICS ?? "/tmp/techlog-parity-diagnostics";
|
||||
|
||||
async function interact(page, action) {
|
||||
if (action === "search-dialog") {
|
||||
await page.getByRole("button", { name: "검색 열기" }).click();
|
||||
} else if (action === "studio-menu") {
|
||||
await page.getByRole("button", { name: "Studio 메뉴 열기" }).click();
|
||||
} else if (action === "immediate-preview") {
|
||||
await page.getByRole("tab", { name: "즉시 미리보기" }).click();
|
||||
} else if (action === "dirty-dialog") {
|
||||
await page.getByLabel("요약").fill("저장하지 않은 시각 검증 변경");
|
||||
await page.getByRole("link", { name: "게시 기록", exact: true }).first().click();
|
||||
} else if (action === "create-preview") {
|
||||
await page.getByRole("button", { name: "Public Preview 만들기" }).click();
|
||||
await page.getByText("현재 저장 버전의 Public Preview입니다.").waitFor();
|
||||
} else if (action === "unpublish-dialog") {
|
||||
await page.getByRole("button", { name: /컬렉션 Fetch Join과 페이징은 왜 충돌하는가 게시 취소/ }).click();
|
||||
} else if (action === "warning-acknowledged") {
|
||||
await page.getByRole("button", { name: "검증하기" }).click();
|
||||
await page.getByText("경고를 확인하고 Preview를 만들 수 있습니다").waitFor();
|
||||
await page.getByRole("link", { name: "Public Preview 만들기" }).click();
|
||||
await page.getByRole("button", { name: "Public Preview 만들기" }).click();
|
||||
await page.getByText("현재 저장 버전의 Public Preview입니다.").waitFor();
|
||||
await page.getByRole("link", { name: "게시 준비로 이동" }).click();
|
||||
await page.getByRole("checkbox").check();
|
||||
await page.getByRole("button", { name: /게시$/ }).waitFor();
|
||||
}
|
||||
}
|
||||
|
||||
async function settle(page, baseUrl, parityCase) {
|
||||
await page.setViewportSize({ width: parityCase.width, height: 1000 });
|
||||
await page.clock.setFixedTime(fixedDate);
|
||||
await page.goto(new URL(parityCase.path, baseUrl).href, { waitUntil: "networkidle" });
|
||||
await page.locator("body").waitFor({ state: "visible" });
|
||||
await page.evaluate(async () => document.fonts.ready);
|
||||
await page.addStyleTag({ content: noMotionCss });
|
||||
await interact(page, parityCase.action);
|
||||
await page.evaluate(async () => document.fonts.ready);
|
||||
}
|
||||
|
||||
async function projection(page) {
|
||||
return page.evaluate(() => {
|
||||
const normalizeReference = (value) => value
|
||||
.split(" ")
|
||||
.map((token) => token.replace(/^_[rR].*?_(?=-|$)/, "<generated-id>"))
|
||||
.join(" ");
|
||||
const attributes = (element) => Object.fromEntries(
|
||||
[...element.attributes]
|
||||
.filter(({ name }) => name === "role" || name.startsWith("aria-"))
|
||||
.map(({ name, value }) => [name, normalizeReference(value)])
|
||||
.sort(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
return [...document.querySelectorAll("header, main, footer, dialog")].map((element) => ({
|
||||
tag: element.tagName.toLowerCase(),
|
||||
className: [...element.classList].map((name) => name.replace(/^_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)$/, "_$1_<generated-module>_$2")).sort().join(" "),
|
||||
text: element.textContent?.replace(/\s+/g, " ").trim() ?? "",
|
||||
aria: attributes(element),
|
||||
descendants: [...element.querySelectorAll("[role], [aria-label], [aria-labelledby], [aria-describedby], [aria-current], [aria-expanded], [aria-controls]")]
|
||||
.map((child) => ({
|
||||
tag: child.tagName.toLowerCase(),
|
||||
className: [...child.classList].map((name) => name.replace(/^_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)$/, "_$1_<generated-module>_$2")).sort().join(" "),
|
||||
text: child.textContent?.replace(/\s+/g, " ").trim() ?? "",
|
||||
aria: attributes(child),
|
||||
})),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async function layoutProjection(page) {
|
||||
return page.evaluate(() => [...document.querySelectorAll("header, main, main *, footer, dialog, .studio-app *")].map((element) => {
|
||||
const box = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
tag: element.tagName.toLowerCase(),
|
||||
className: [...element.classList].sort().join(" "),
|
||||
top: box.top,
|
||||
left: box.left,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
lineHeight: style.lineHeight,
|
||||
margin: style.margin,
|
||||
padding: style.padding,
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
function pixelDifference(sourceBuffer, targetBuffer) {
|
||||
const source = PNG.sync.read(sourceBuffer);
|
||||
const target = PNG.sync.read(targetBuffer);
|
||||
if (source.width !== target.width || source.height !== target.height) {
|
||||
return { pixels: null, sourceSize: [source.width, source.height], targetSize: [target.width, target.height] };
|
||||
}
|
||||
let pixels = 0;
|
||||
for (let offset = 0; offset < source.data.length; offset += 4) {
|
||||
if (
|
||||
source.data[offset] !== target.data[offset] ||
|
||||
source.data[offset + 1] !== target.data[offset + 1] ||
|
||||
source.data[offset + 2] !== target.data[offset + 2] ||
|
||||
source.data[offset + 3] !== target.data[offset + 3]
|
||||
) pixels += 1;
|
||||
}
|
||||
return { pixels, sourceSize: [source.width, source.height], targetSize: [target.width, target.height] };
|
||||
}
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const contextOptions = {
|
||||
colorScheme: "light",
|
||||
deviceScaleFactor: 1,
|
||||
locale: "ko-KR",
|
||||
reducedMotion: "reduce",
|
||||
serviceWorkers: "block",
|
||||
timezoneId: "Asia/Seoul",
|
||||
};
|
||||
const sourceContext = await browser.newContext(contextOptions);
|
||||
const targetContext = await browser.newContext(contextOptions);
|
||||
const results = [];
|
||||
|
||||
try {
|
||||
for (const parityCase of cases) {
|
||||
const sourcePage = await sourceContext.newPage();
|
||||
const targetPage = await targetContext.newPage();
|
||||
const sourceErrors = [];
|
||||
const targetErrors = [];
|
||||
for (const [page, errors] of [[sourcePage, sourceErrors], [targetPage, targetErrors]]) {
|
||||
page.on("console", (message) => { if (message.type() === "error") errors.push(`console: ${message.text()}`); });
|
||||
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
|
||||
page.on("requestfailed", (request) => errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText ?? ""}`));
|
||||
}
|
||||
await Promise.all([
|
||||
settle(sourcePage, sourceBaseUrl, parityCase),
|
||||
settle(targetPage, targetBaseUrl, parityCase),
|
||||
]);
|
||||
const [sourceShot, targetShot, sourceDom, targetDom, sourceLayout, targetLayout, sourceFonts, targetFonts] = await Promise.all([
|
||||
sourcePage.screenshot({ fullPage: true }),
|
||||
targetPage.screenshot({ fullPage: true }),
|
||||
projection(sourcePage),
|
||||
projection(targetPage),
|
||||
layoutProjection(sourcePage),
|
||||
layoutProjection(targetPage),
|
||||
sourcePage.evaluate(() => [...document.fonts].map(({ family, status }) => ({ family, status })).filter(({ family }) => /Pretendard|IBM Plex Mono/.test(family))),
|
||||
targetPage.evaluate(() => [...document.fonts].map(({ family, status }) => ({ family, status })).filter(({ family }) => /Pretendard|IBM Plex Mono/.test(family))),
|
||||
]);
|
||||
const pixel = pixelDifference(sourceShot, targetShot);
|
||||
const domEqual = JSON.stringify(sourceDom) === JSON.stringify(targetDom);
|
||||
const fontsEqual = JSON.stringify(sourceFonts) === JSON.stringify(targetFonts);
|
||||
const isPlainNotFound =
|
||||
parityCase.name.includes("NOT_FOUND") || parityCase.name.includes("UNKNOWN");
|
||||
const expectsSourceNotFoundResponse =
|
||||
isPlainNotFound || parityCase.path.includes("unknown");
|
||||
const expectedNotFoundConsole = expectsSourceNotFoundResponse
|
||||
? (message) => message === "console: Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
: () => false;
|
||||
const unexplainedSourceErrors = sourceErrors.filter((message) => !expectedNotFoundConsole(message));
|
||||
const unexplainedTargetErrors = targetErrors.filter((message) => !expectedNotFoundConsole(message));
|
||||
const fontContractApplies = !isPlainNotFound;
|
||||
const passed = pixel.pixels === 0 && domEqual && (!fontContractApplies || fontsEqual) && unexplainedSourceErrors.length === 0 && unexplainedTargetErrors.length === 0;
|
||||
if (!passed) {
|
||||
await mkdir(diagnosticDirectory, { recursive: true });
|
||||
const stem = parityCase.name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
||||
await Promise.all([
|
||||
writeFile(resolve(diagnosticDirectory, `${stem}-source.png`), sourceShot),
|
||||
writeFile(resolve(diagnosticDirectory, `${stem}-target.png`), targetShot),
|
||||
writeFile(resolve(diagnosticDirectory, `${stem}-dom.json`), `${JSON.stringify({ sourceDom, targetDom, sourceLayout, targetLayout }, null, 2)}\n`),
|
||||
]);
|
||||
}
|
||||
results.push({ name: parityCase.name, path: parityCase.path, width: parityCase.width, action: parityCase.action ?? null, pixel, domEqual, fontsEqual, fontContractApplies, sourceFonts, targetFonts, sourceErrors, targetErrors, unexplainedSourceErrors, unexplainedTargetErrors, passed });
|
||||
console.log(`${passed ? "PASS" : "FAIL"} ${parityCase.name} pixels=${pixel.pixels ?? "SIZE"} dom=${domEqual} fonts=${fontContractApplies ? fontsEqual : "source-plain-text"} errors=${unexplainedSourceErrors.length}/${unexplainedTargetErrors.length}`);
|
||||
await Promise.all([sourcePage.close(), targetPage.close()]);
|
||||
}
|
||||
} finally {
|
||||
await Promise.all([sourceContext.close(), targetContext.close()]);
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
const evidence = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
sourceBaseUrl,
|
||||
targetBaseUrl,
|
||||
conditions: { ...contextOptions, fixedTime: TECH_LOG_FIXED_TIME, viewportHeight: 1000, screenshots: "fullPage", masks: 0 },
|
||||
total: results.length,
|
||||
passed: results.filter((result) => result.passed).length,
|
||||
failed: results.filter((result) => !result.passed).length,
|
||||
totalDifferentPixels: results.reduce((sum, result) => sum + (result.pixel.pixels ?? 0), 0),
|
||||
results,
|
||||
};
|
||||
await mkdir(resolve(outputPath, ".."), { recursive: true });
|
||||
await writeFile(outputPath, `${JSON.stringify(evidence, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ outputPath, total: evidence.total, passed: evidence.passed, failed: evidence.failed, totalDifferentPixels: evidence.totalDifferentPixels }));
|
||||
if (evidence.failed > 0) process.exitCode = 1;
|
||||
@@ -190,6 +190,7 @@ jobs:
|
||||
scripts/lib/secret-scan.ts \\
|
||||
scripts/lib/supply-chain.ts \\
|
||||
scripts/lib/validated-json-artifact.ts \\
|
||||
scripts/lib/vite-route-chunks.ts \\
|
||||
src/contracts/release-artifacts.ts \\
|
||||
src/features/installed-contract-contributions.ts \\
|
||||
src/features/installed-feature-contracts.ts \\
|
||||
|
||||
@@ -121,6 +121,29 @@ describe("bounded body reader", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("abandons and cancels a bounded read when its lifetime aborts", async () => {
|
||||
const controller = new AbortController();
|
||||
const reader = {
|
||||
read: vi.fn(() => new Promise<never>(() => {})),
|
||||
cancel: vi.fn().mockRejectedValue(new Error("abandoned cancel failed")),
|
||||
releaseLock: vi.fn(),
|
||||
};
|
||||
const response = {
|
||||
headers: new Headers(),
|
||||
body: { getReader: () => reader },
|
||||
} as unknown as Response;
|
||||
|
||||
const outcome = readBoundedBytes(response, 3, controller.signal);
|
||||
controller.abort();
|
||||
|
||||
await expect(outcome).resolves.toEqual({
|
||||
ok: false,
|
||||
code: "RESPONSE_STREAM_FAILURE",
|
||||
});
|
||||
expect(reader.cancel).toHaveBeenCalledOnce();
|
||||
expect(reader.releaseLock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("detects a forbidden body from declared length without reading it", async () => {
|
||||
const cancel = vi.fn();
|
||||
const response = {
|
||||
@@ -200,6 +223,29 @@ describe("bounded body reader", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("abandons and cancels a forbidden-body probe when its lifetime aborts", async () => {
|
||||
const controller = new AbortController();
|
||||
const reader = {
|
||||
read: vi.fn(() => new Promise<never>(() => {})),
|
||||
cancel: vi.fn().mockRejectedValue(new Error("abandoned cancel failed")),
|
||||
releaseLock: vi.fn(),
|
||||
};
|
||||
const response = {
|
||||
headers: new Headers(),
|
||||
body: { getReader: () => reader },
|
||||
} as unknown as Response;
|
||||
|
||||
const outcome = probeForbiddenBody(response, controller.signal);
|
||||
controller.abort();
|
||||
|
||||
await expect(outcome).resolves.toEqual({
|
||||
ok: false,
|
||||
code: "RESPONSE_STREAM_FAILURE",
|
||||
});
|
||||
expect(reader.cancel).toHaveBeenCalledOnce();
|
||||
expect(reader.releaseLock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("decodes valid JSON and distinguishes UTF-8 from JSON failures", () => {
|
||||
expect(decodeJsonBytes(new TextEncoder().encode('{"ok":true}'))).toEqual({
|
||||
ok: true,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { containsRawPaletteValue } from "../../scripts/lib/design-system-source.ts";
|
||||
|
||||
describe("design-system source palette detection", () => {
|
||||
it("rejects palette literals without treating URL fragments as colors", () => {
|
||||
expect(containsRawPaletteValue('style={{ color: "#ff0000" }}')).toBe(true);
|
||||
expect(containsRawPaletteValue('const css = "color: rgb(255 0 0)"')).toBe(true);
|
||||
expect(
|
||||
containsRawPaletteValue(
|
||||
'path: "/projects/backend-skeleton/decisions#feed-pagination-boundary"',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -335,9 +335,9 @@ describe("registry governance manifest", () => {
|
||||
});
|
||||
expect(versions?.rows).toBeDefined();
|
||||
expect(approval).toMatchObject({
|
||||
owner: "frontend-platform",
|
||||
owner: "tech-log-frontend",
|
||||
reason:
|
||||
"Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
|
||||
"Install approved TechLog Public and Studio route contract",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1770,6 +1770,7 @@ async function createArchivedAssessmentFixture(): Promise<{
|
||||
"scripts/lib/secret-scan.ts",
|
||||
"scripts/lib/supply-chain.ts",
|
||||
"scripts/lib/validated-json-artifact.ts",
|
||||
"scripts/lib/vite-route-chunks.ts",
|
||||
"src/contracts/release-artifacts.ts",
|
||||
"src/features/installed-contract-contributions.ts",
|
||||
"src/features/installed-feature-contracts.ts",
|
||||
@@ -1799,6 +1800,7 @@ async function createArchivedAssessmentFixture(): Promise<{
|
||||
"scripts/lib/secret-scan.ts",
|
||||
"scripts/lib/supply-chain.ts",
|
||||
"scripts/lib/validated-json-artifact.ts",
|
||||
"scripts/lib/vite-route-chunks.ts",
|
||||
"src/contracts/release-artifacts.ts",
|
||||
"src/features/installed-contract-contributions.ts",
|
||||
"src/features/installed-feature-contracts.ts",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { findViteDynamicRouteChunk } from "../../scripts/lib/vite-route-chunks.ts";
|
||||
|
||||
describe("Vite route chunk identity", () => {
|
||||
it("recognizes a governed manual chunk referenced by an entry dynamic import", () => {
|
||||
const manifest = {
|
||||
"_route-tech-log-home-abc.js": {
|
||||
file: "assets/route-tech-log-home-abc.js",
|
||||
name: "route-tech-log-home",
|
||||
},
|
||||
"index.html": {
|
||||
file: "assets/index.js",
|
||||
name: "index",
|
||||
isEntry: true,
|
||||
dynamicImports: ["_route-tech-log-home-abc.js"],
|
||||
},
|
||||
};
|
||||
|
||||
expect(findViteDynamicRouteChunk(manifest, "route-tech-log-home")).toEqual({
|
||||
file: "assets/route-tech-log-home-abc.js",
|
||||
name: "route-tech-log-home",
|
||||
});
|
||||
});
|
||||
|
||||
it("retains Vite native dynamic-entry support", () => {
|
||||
const manifest = {
|
||||
"src/page.tsx": {
|
||||
file: "assets/page.js",
|
||||
name: "route-page",
|
||||
isDynamicEntry: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(findViteDynamicRouteChunk(manifest, "route-page")?.file).toBe(
|
||||
"assets/page.js",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a named chunk that is not dynamically reachable", () => {
|
||||
const manifest = {
|
||||
"_route-tech-log-home-abc.js": {
|
||||
file: "assets/route-tech-log-home-abc.js",
|
||||
name: "route-tech-log-home",
|
||||
},
|
||||
"index.html": {
|
||||
file: "assets/index.js",
|
||||
name: "index",
|
||||
isEntry: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(findViteDynamicRouteChunk(manifest, "route-tech-log-home")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 556 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 212 KiB |