test: prove TechLog UI migration parity

This commit is contained in:
DongHyeonka
2026-08-16 02:48:26 +09:00
parent c5c8b9423c
commit 6c2780b7a7
176 changed files with 2393 additions and 616 deletions
+1
View File
@@ -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.
+26 -27
View File
@@ -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.
+1
View File
@@ -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"
}
+633 -153
View File
@@ -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."
}
]
}
+2 -1
View File
@@ -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.
+1 -1
View File
@@ -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",
+7 -7
View File
@@ -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 },
+11 -4
View File
@@ -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 -1
View File
@@ -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}`);
}
+13 -10
View 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;
+6
View File
@@ -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);
}
+22 -6
View File
@@ -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" ||
+1
View File
@@ -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",
+20
View File
@@ -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];
}
+13 -10
View File
@@ -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;
-1
View File
@@ -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;
-2
View File
@@ -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" });
+3 -11
View File
@@ -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 () => {
+26 -44
View File
@@ -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";
import {
gotoTechLog,
TECH_LOG_CANONICAL_ROUTES,
} from "../support/browser/tech-log-fixtures.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 ({
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);
});
+14 -17
View File
@@ -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();
});
-23
View File
@@ -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");
});
-41
View File
@@ -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();
});
+30 -30
View File
@@ -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();
const viewport = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth);
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();
}
});
}
}
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",
);
});
+77
View File
@@ -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();
});
+41
View File
@@ -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();
});
}
-38
View File
@@ -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();
});
});
+140
View File
@@ -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 \\
+46
View File
@@ -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,
+15
View File
@@ -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);
});
});
+2 -2
View File
@@ -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",
});
});
+2
View File
@@ -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",
+55
View File
@@ -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();
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Some files were not shown because too many files have changed in this diff Show More