fix: complete TechLog migration evidence

This commit is contained in:
DongHyeonka
2026-08-16 04:55:52 +09:00
parent 6c2780b7a7
commit 3a7c5deca0
81 changed files with 1492 additions and 345 deletions
+12
View File
@@ -43,6 +43,18 @@ load. The exact route, dependency, stylesheet, asset, and parity inventory is
recorded in
[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md).
`corepack pnpm build` emits a self-contained `dist/server.mjs` production
boundary. `corepack pnpm preview --host 127.0.0.1 --port 4174` serves known
Public and Studio routes as SPA documents, preserves the in-shell Studio 404,
and returns source-exact raw `404 text/plain` responses for missing Public
content. With the read-only source temp-copy server on `4375`, run:
```bash
TECH_LOG_SOURCE_URL=http://127.0.0.1:4375 \
TECH_LOG_TARGET_URL=http://127.0.0.1:4174 \
corepack pnpm verify:tech-log-source-parity
```
## Architecture
Dependencies point inward:
-18
View File
@@ -1,18 +0,0 @@
# APP_HOME accessibility review
Status: pending-manual-review
Route ID: APP_HOME
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Automated axe, keyboard-focus, and reduced-motion evidence is available; human review pending.
@@ -1,18 +0,0 @@
# EXAMPLES_AUTH accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_AUTH
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review session state announcements and unavailable external-integration behavior.
@@ -1,18 +0,0 @@
# EXAMPLES_PLATFORM accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_PLATFORM
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review the horizontally scrollable registry tables for keyboard reachability of the scroll container, table caption and header association announced per row, capability status badges carrying their meaning in text rather than colour alone, and the release identity region announcing its update through aria-live without interrupting a reader mid row.
@@ -1,18 +0,0 @@
# EXAMPLES_STATES accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_STATES
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review loading, refresh, empty, error, authentication, forbidden, and not-found announcements.
+3 -3
View File
@@ -10,9 +10,9 @@ Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,18 +0,0 @@
# REFERENCE_RESOURCE_STATUS accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_STATUS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
@@ -1,7 +1,7 @@
# REFERENCE_RESOURCE_FORM accessibility review
# TECH_LOG_CASE accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_FORM
Route ID: TECH_LOG_CASE
Release ID:
Reviewer:
Reviewed at:
@@ -15,4 +15,4 @@ M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,7 +1,7 @@
# EXAMPLES_UI accessibility review
# TECH_LOG_EXPLORE accessibility review
Status: pending-manual-review
Route ID: EXAMPLES_UI
Route ID: TECH_LOG_EXPLORE
Release ID:
Reviewer:
Reviewed at:
@@ -15,4 +15,4 @@ M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Review form primitives, Menu/Tabs keyboard behavior, Toast announcements, Tooltip supplemental copy, text-field error association and modal focus containment/restoration.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_EXPLORE_KIND accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_EXPLORE_KIND
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,7 +1,7 @@
# REFERENCE_RESOURCE_LIST accessibility review
# TECH_LOG_HOME accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_LIST
Route ID: TECH_LOG_HOME
Release ID:
Reviewer:
Reviewed at:
@@ -10,9 +10,9 @@ Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -1,7 +1,7 @@
# REFERENCE_RESOURCE_DETAIL accessibility review
# TECH_LOG_PROFILE accessibility review
Status: pending-manual-review
Route ID: REFERENCE_RESOURCE_DETAIL
Route ID: TECH_LOG_PROFILE
Release ID:
Reviewer:
Reviewed at:
@@ -10,9 +10,9 @@ Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECTS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECTS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT_ACTIVITY accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT_ACTIVITY
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT_DECISIONS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT_DECISIONS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_PROJECT_RECORDS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_PROJECT_RECORDS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_QUESTION accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_QUESTION
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_REFERENCE accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_REFERENCE
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_RELEASE accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_RELEASE
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_RELEASES accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_RELEASES
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_SEARCH accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_SEARCH
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENTS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENTS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_EDIT accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_EDIT
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_NEW accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_NEW
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_PREVIEW accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_PREVIEW
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_PUBLISH accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_PUBLISH
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_DOCUMENT_VALIDATION accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_DOCUMENT_VALIDATION
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_HOME accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_HOME
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_NOT_FOUND accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_NOT_FOUND
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_PUBLICATIONS accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_PUBLICATIONS
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_STUDIO_PUBLICATION_PREVIEW accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_STUDIO_PUBLICATION_PREVIEW
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
@@ -0,0 +1,18 @@
# TECH_LOG_TOPIC accessibility review
Status: pending-manual-review
Route ID: TECH_LOG_TOPIC
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: pending
M5 Error association: pending
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
+168 -39
View File
@@ -1006,58 +1006,166 @@
]
},
{
"id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md",
"path": "artifacts/tests/a11y-manual/APP_HOME.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_HOME.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
"path": "artifacts/tests/a11y-manual/EXAMPLES_UI.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
"path": "artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
"path": "artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_CASE.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
"path": "artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md",
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md",
"schemaId": "markdown",
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
"schemaId": "markdown",
"production": "command-generated",
"producerCommandIds": [
"review-a11y-manual"
]
"production": "source-controlled"
},
{
"id": "artifact-artifacts-tests-a11y-manual-report-json",
@@ -1757,11 +1865,32 @@
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
"evidenceArtifactIds": [
"artifact-artifacts-tests-a11y-json",
"artifact-artifacts-tests-a11y-manual-APP-HOME-md",
"artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
"artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
"artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
"artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
"artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
"artifact-artifacts-tests-a11y-manual-report-json"
],
+10 -10
View File
@@ -1,12 +1,11 @@
# Manual accessibility review checklist
Automated axe checks do not establish WCAG conformance. A human reviewer must
review all six route records in `artifacts/tests/a11y-manual/` against one
release candidate and sign them. The required scope is derived from the route
registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`,
`REFERENCE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the
gate rejects blank identity/timestamp/signature fields, pending verdicts,
mismatched release IDs, or missing routes.
review all 27 route records in `artifacts/tests/a11y-manual/` against one
release candidate and sign them. The required TechLog Public and Studio scope is
derived from the installed route registry, so the gate rejects stale, missing,
or additional route records as well as blank identity/timestamp/signature
fields, pending verdicts, and mismatched release IDs.
Allowed item verdicts:
@@ -17,7 +16,7 @@ Required record:
```text
Status: reviewed
Route ID: APP_HOME
Route ID: <exact installed route ID>
Release ID: <immutable release ID>
Reviewer: <human reviewer identity>
Reviewed at: <RFC 3339 timestamp>
@@ -45,7 +44,8 @@ The reviewer must verify:
- M7: non-essential motion is suppressed with reduced-motion preference
- Screen reader: headings, live regions, errors, and actions are announced once
`EXAMPLES_UI` requires real M4 modal and M5 field-error review; those items must
not be marked not-applicable on that route. Passing automated evidence means
Routes with dialogs or form errors require real M4 modal-focus or M5
error-association review; those items must not be marked not-applicable when the
reviewed route exposes the relevant behavior. Passing automated evidence means
only that tested pages had no critical or serious axe findings under the
recorded Chromium, Firefox, and WebKit runs.
recorded browser runs.
+2 -1
View File
@@ -12,7 +12,8 @@
"dev": "vite",
"build": "node scripts/build-frontend.ts",
"build:release-candidate": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security && corepack pnpm verify:release && node scripts/verify-supply-chain-artifacts.ts && node scripts/create-release-candidate.ts",
"preview": "vite preview",
"preview": "node dist/server.mjs",
"verify:tech-log-source-parity": "node tests/support/browser/verify-tech-log-source-parity.ts",
"lint": "eslint src scripts tests recipes .storybook vite.config.ts vitest.config.ts playwright*.config.ts --max-warnings=0",
"check:architecture": "node scripts/check-architecture.ts",
"check:design-system": "node scripts/check-design-system.ts",
+6 -2
View File
@@ -15,7 +15,8 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim
* 3. Vite app build (emptyOutDir = true)
* 4. scan app dist and generate the static asset source
* 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
* 6. generate Release Manifest V2 and the build manifest
* 6. generate the self-contained TechLog production serving boundary
* 7. generate Release Manifest V2 and the build manifest
*
* Steps 4 and 5 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
* and `null`: those modes never run an active worker build.
@@ -56,5 +57,8 @@ if (buildsActiveWorker) {
);
}
// 6. release + build manifest
// 6. production serving boundary
run("node", ["scripts/generate-tech-log-serving-artifact.ts"]);
// 7. release + build manifest
run("node", ["scripts/generate-build-manifest.ts"]);
+42 -3
View File
@@ -5,6 +5,7 @@ import path from "node:path";
import { z } from "zod";
import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts";
import { MANUAL_A11Y_ROUTE_IDS } from "../lib/manual-a11y-evidence.ts";
import {
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
@@ -442,7 +443,7 @@ export type LoadCiGateContractOptions = Readonly<{
}>;
const CANONICAL_GATE_SHAPE_SHA256 =
"a4a963d0b9deffb7a0a3d755bbbcb979d72610eb74751c3a2e5eca55251e12d4";
"492331de6d07926b1cdb569824c8feaa761eef76b9ac782c0404d40797383ef0";
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
const normalized = gates.map(
@@ -481,8 +482,8 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[]
`command authority baseline must contain exactly 81 definitions and 93 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`,
);
}
if (contract.artifacts.length !== 105) {
failures.push(`artifact authority baseline must contain exactly 105 artifacts; received ${contract.artifacts.length}`);
if (contract.artifacts.length !== 126) {
failures.push(`artifact authority baseline must contain exactly 126 artifacts; received ${contract.artifacts.length}`);
}
if (contract.stages.length !== 5) {
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
@@ -723,6 +724,44 @@ function validateContractSemantics(
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
}
}
const accessibilityGate = contract.gates.find(
({ id: gateId }) => gateId === "FE-GATE-009",
);
const manualAccessibilityArtifacts = MANUAL_A11Y_ROUTE_IDS.map((routeId) => ({
id: `artifact-artifacts-tests-a11y-manual-${routeId.replaceAll("_", "-")}-md`,
path: `artifacts/tests/a11y-manual/${routeId}.md`,
}));
const expectedAccessibilityEvidenceArtifactIds = [
"artifact-artifacts-tests-a11y-json",
...manualAccessibilityArtifacts.map(({ id: artifactId }) => artifactId),
"artifact-artifacts-tests-a11y-manual-report-json",
];
if (
!accessibilityGate ||
JSON.stringify(accessibilityGate.evidenceArtifactIds) !==
JSON.stringify(expectedAccessibilityEvidenceArtifactIds)
) {
issue(
"FE-GATE-009 manual accessibility evidence must exactly match the installed route scope",
);
}
for (const expected of manualAccessibilityArtifacts) {
const artifact = contract.artifacts.find(
({ id: artifactId }) => artifactId === expected.id,
);
if (
!artifact ||
artifact.path !== expected.path ||
artifact.schemaId !== "markdown" ||
artifact.production !== "source-controlled"
) {
issue(
`FE-GATE-009 manual accessibility artifact registration is invalid: ${expected.id}`,
);
}
}
const referencedRetentionClasses = new Set(
contract.gates.map(({ retentionClassId }) => retentionClassId),
);
@@ -0,0 +1,15 @@
import {
projects,
publicRecords,
releases,
} from "../src/features/tech-log/adapters/static/public-content.ts";
import { writeTechLogServingArtifact } from "./lib/tech-log-serving-artifact.ts";
import { createTechLogServingContract } from "./lib/tech-log-serving-contract.ts";
const contract = createTechLogServingContract({
projects,
publicRecords,
releases,
});
await writeTechLogServingArtifact({ distRoot: "dist", contract });
+133
View File
@@ -0,0 +1,133 @@
// @ts-nocheck -- copied byte-for-byte into the self-contained Node deployment artifact.
import { readFile, stat } from "node:fs/promises";
import { createServer } from "node:http";
import path from "node:path";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
const contentTypes = Object.freeze({
".css": "text/css;charset=UTF-8",
".html": "text/html;charset=UTF-8",
".js": "text/javascript;charset=UTF-8",
".json": "application/json",
".png": "image/png",
".svg": "image/svg+xml",
".woff": "font/woff",
".woff2": "font/woff2",
});
function isStudioPath(pathname, prefix) {
return pathname === prefix || pathname.startsWith(`${prefix}/`);
}
async function sendFile(
file,
method,
response,
status = 200,
) {
const body = await readFile(file);
response.writeHead(status, {
"Cache-Control": "no-cache",
"Content-Length": body.byteLength,
"Content-Type": contentTypes[path.extname(file)] ?? "application/octet-stream",
Vary: "Origin",
});
if (method === "HEAD") {
response.end();
return;
}
response.end(body);
}
export function createTechLogProductionServer({
root,
contract,
}) {
const absoluteRoot = path.resolve(root);
const publicSpaPaths = new Set(contract.publicSpaPaths);
const studioSpaPathPatterns = contract.studioSpaPathPatterns.map(
(pattern) => new RegExp(pattern),
);
const server = createServer(async (request, response) => {
if (request.method !== "GET" && request.method !== "HEAD") {
response.writeHead(405, { Allow: "GET, HEAD" }).end();
return;
}
try {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
const pathname = decodeURIComponent(url.pathname);
const requested = path.resolve(absoluteRoot, `.${pathname}`);
if (
requested !== absoluteRoot &&
requested.startsWith(`${absoluteRoot}${path.sep}`)
) {
const details = await stat(requested).catch(() => null);
if (details?.isFile()) {
await sendFile(requested, request.method, response);
return;
}
}
if (
publicSpaPaths.has(pathname) ||
studioSpaPathPatterns.some((pattern) => pattern.test(pathname))
) {
await sendFile(path.join(absoluteRoot, "index.html"), request.method, response);
return;
}
if (isStudioPath(pathname, contract.studioPathPrefix)) {
await sendFile(
path.join(absoluteRoot, "index.html"),
request.method,
response,
contract.notFound.status,
);
return;
}
response.writeHead(contract.notFound.status, {
"Content-Type": contract.notFound.contentType,
});
response.end(request.method === "HEAD" ? undefined : contract.notFound.body);
} catch {
response.writeHead(contract.notFound.status, {
"Content-Type": contract.notFound.contentType,
});
response.end(request.method === "HEAD" ? undefined : contract.notFound.body);
}
});
return server;
}
function optionValue(name) {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : undefined;
}
async function startFromCommandLine() {
const root = path.resolve(
optionValue("--root") ?? path.dirname(fileURLToPath(import.meta.url)),
);
const host = optionValue("--host") ?? "127.0.0.1";
const port = Number(optionValue("--port") ?? 4173);
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new TypeError("--port must be an integer from 1 through 65535");
}
const contract = JSON.parse(
await readFile(path.join(root, "tech-log-serving-contract.json"), "utf8"),
);
const server = createTechLogProductionServer({ root, contract });
server.listen(port, host, () => {
process.stdout.write(`TechLog production server: ${root} on http://${host}:${port}\n`);
});
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => server.close(() => process.exit(0)));
}
}
const commandPath = process.argv[1];
if (
commandPath &&
import.meta.url === pathToFileURL(path.resolve(commandPath)).href
) {
await startFromCommandLine();
}
+27
View File
@@ -0,0 +1,27 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import type { TechLogServingContract } from "./tech-log-serving-contract.ts";
type ServingArtifactOptions = Readonly<{
distRoot: string;
contract: TechLogServingContract;
serverSourcePath?: string;
}>;
export async function writeTechLogServingArtifact({
distRoot,
contract,
serverSourcePath = "scripts/lib/tech-log-production-server.ts",
}: ServingArtifactOptions): Promise<void> {
const absoluteDistRoot = path.resolve(distRoot);
const serverSource = await readFile(path.resolve(serverSourcePath), "utf8");
await mkdir(absoluteDistRoot, { recursive: true });
await Promise.all([
writeFile(
path.join(absoluteDistRoot, "tech-log-serving-contract.json"),
`${JSON.stringify(contract, null, 2)}\n`,
),
writeFile(path.join(absoluteDistRoot, "server.mjs"), serverSource),
]);
}
+77
View File
@@ -0,0 +1,77 @@
export type TechLogServingContract = Readonly<{
schemaVersion: 1;
publicSpaPaths: readonly string[];
studioPathPrefix: "/studio";
studioSpaPathPatterns: readonly string[];
notFound: Readonly<{
status: 404;
contentType: "text/plain;charset=UTF-8";
body: "Not Found";
}>;
}>;
type ServingContractInput = Readonly<{
publicRecords: readonly Readonly<{
path: string;
topicSlug: string;
}>[];
projects: readonly Readonly<{ slug: string }>[];
releases: readonly Readonly<{ path: string }>[];
}>;
const staticPublicPaths = Object.freeze([
"/",
"/explore",
"/explore/cases",
"/explore/questions",
"/explore/references",
"/profile",
"/projects",
"/releases",
"/search",
]);
const studioSpaPathPatterns = Object.freeze([
"^/studio$",
"^/studio/documents$",
"^/studio/documents/new$",
"^/studio/documents/[^/]+/(edit|validation|preview|publish)$",
"^/studio/publications$",
"^/studio/publications/[^/]+/preview$",
]);
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
export function createTechLogServingContract({
publicRecords,
projects,
releases,
}: ServingContractInput): TechLogServingContract {
const publicSpaPaths = new Set(staticPublicPaths);
for (const record of publicRecords) {
publicSpaPaths.add(record.path);
publicSpaPaths.add(`/topics/${record.topicSlug}`);
}
for (const project of projects) {
const projectPath = `/projects/${project.slug}`;
publicSpaPaths.add(projectPath);
publicSpaPaths.add(`${projectPath}/activity`);
publicSpaPaths.add(`${projectPath}/decisions`);
publicSpaPaths.add(`${projectPath}/records`);
}
for (const release of releases) publicSpaPaths.add(release.path);
return Object.freeze({
schemaVersion: 1,
publicSpaPaths: Object.freeze([...publicSpaPaths].sort(asciiCompare)),
studioPathPrefix: "/studio",
studioSpaPathPatterns,
notFound: Object.freeze({
status: 404,
contentType: "text/plain;charset=UTF-8",
body: "Not Found",
}),
});
}
@@ -1,34 +1,11 @@
import { useSourceCompatiblePublicNotFound } from "../public-not-found-layout.tsx";
export function PublicNotFoundPage() {
useSourceCompatiblePublicNotFound();
return (
<>
<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>
</>
<main id="main-content" className="shell public-index-page">
<header className="public-page-header">
<p className="section-kicker">404</p>
<h1> .</h1>
<p> .</p>
</header>
</main>
);
}
@@ -1,18 +0,0 @@
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,9 +1,8 @@
import { type ReactNode, useCallback, useState } from "react";
import { type ReactNode } 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;
@@ -11,13 +10,7 @@ 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">
@@ -41,7 +34,5 @@ export function PublicShell({ children, currentPath }: PublicShellProps) {
</div>
</footer>
</div>
)}
</PublicNotFoundLayoutContext.Provider>
);
}
+6 -3
View File
@@ -343,12 +343,15 @@ describe("generic application router", () => {
expect(window.location.pathname).toBe("/projects");
});
it("renders the source-compatible plain TechLog not-found response", async () => {
it("keeps the unreachable client-side Public fallback inside its accessible shell", async () => {
window.history.pushState({}, "", "/missing");
renderRouter();
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
expect(screen.queryByRole("navigation", { name: "주요 탐색" })).not.toBeInTheDocument();
expect(
await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }),
).toBeVisible();
expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
expect(screen.queryByText("Not Found", { exact: true })).not.toBeInTheDocument();
});
it("keeps Studio routes inside the persistent Studio layout", async () => {
+12 -1
View File
@@ -6,7 +6,9 @@ import {
TECH_LOG_CANONICAL_ROUTES,
} from "../support/browser/tech-log-fixtures.ts";
for (const route of TECH_LOG_CANONICAL_ROUTES) {
for (const route of TECH_LOG_CANONICAL_ROUTES.filter(
({ routeId }) => routeId !== "NOT_FOUND",
)) {
test(`@a11y ${route.routeId} has no critical or serious Axe violations`, async ({
page,
}) => {
@@ -22,6 +24,15 @@ for (const route of TECH_LOG_CANONICAL_ROUTES) {
});
}
test("@a11y NOT_FOUND preserves the source raw non-HTML response", async ({
request,
}) => {
const response = await request.get("/definitely-not-a-product-route");
expect(response.status()).toBe(404);
expect(response.headers()["content-type"]).toBe("text/plain;charset=UTF-8");
expect(await response.text()).toBe("Not Found");
});
test("@a11y keyboard reaches a visible Public navigation focus indicator", async ({
page,
}) => {
+68
View File
@@ -0,0 +1,68 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
import {
TECH_LOG_CANONICAL_ROUTES,
TECH_LOG_PUBLIC_FIXTURE_PATHS,
} from "../support/browser/tech-log-fixtures.ts";
const knownSpaPaths = [
...new Set([
...TECH_LOG_CANONICAL_ROUTES
.filter(({ routeId }) => !["NOT_FOUND", "TECH_LOG_STUDIO_NOT_FOUND"].includes(routeId))
.map(({ path }) => path),
...TECH_LOG_PUBLIC_FIXTURE_PATHS,
]),
];
test("production HTTP preserves the source in-shell Studio 404", async ({
request,
}) => {
const response = await request.get("/studio/unknown-screen");
expect(response.status()).toBe(404);
expect(response.headers()["content-type"]).toBe("text/html;charset=UTF-8");
expect((await response.text()).startsWith("<!doctype html>")).toBe(true);
});
for (const path of knownSpaPaths) {
test(`production HTTP serves the SPA for ${path}`, async ({ request }) => {
const response = await request.get(path);
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toBe("text/html;charset=UTF-8");
expect((await response.text()).startsWith("<!doctype html>")).toBe(true);
});
}
for (const path of [
"/projects/missing-project",
"/definitely-not-a-product-route",
]) {
test(`production HTTP returns the source-exact raw 404 for ${path}`, async ({
request,
}) => {
const response = await request.get(path);
expect(response.status()).toBe(404);
expect(response.headers()["content-type"]).toBe(
"text/plain;charset=UTF-8",
);
expect(await response.body()).toEqual(Buffer.from("Not Found", "utf8"));
});
}
for (const path of ["/config.json", "/release-manifest.json"]) {
test(`production HTTP frames boot document ${path} like the reviewed preview`, async ({
request,
}) => {
const response = await request.get(path);
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toBe("application/json");
expect(response.headers()["cache-control"]).toBe("no-cache");
expect(response.headers().vary).toBe("Origin");
expect(Number(response.headers()["content-length"])).toBe(
(await response.body()).byteLength,
);
});
}
@@ -372,18 +372,20 @@ describe("TechLog explore discovery", () => {
).toHaveAttribute("href", "/explore");
});
it("renders an unknown kind as the source production plain 404", async () => {
it("keeps the unreachable unknown-kind client fallback accessible", async () => {
const { router, container } = renderDiscoveryRoute(
"TECH_LOG_EXPLORE_KIND",
"/explore/unknown",
);
expect(router.state.location.pathname).toBe("/explore/unknown");
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
expect(
await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }),
).toBeVisible();
expect(
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
).not.toBeInTheDocument();
expect(container.querySelector(".site-frame")).toBeNull();
expect(container.querySelector(".site-frame")).not.toBeNull();
});
it("recovers a rejecting registered not-found runtime under its own chunk contract", async () => {
@@ -337,12 +337,14 @@ 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 source production plain 404 for %s", async (routeId, path) => {
])("keeps the unreachable client fallback accessible for %s", async (routeId, path) => {
const { container, router } = renderDocumentRoute(routeId, path);
expect(router.state.location.pathname).toBe(path);
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
expect(
await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }),
).toBeVisible();
expect(screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." })).not.toBeInTheDocument();
expect(container.querySelector(".site-frame")).toBeNull();
expect(container.querySelector(".site-frame")).not.toBeNull();
});
});
@@ -360,25 +360,29 @@ 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"],
])("renders the source-compatible plain fallback for %s", async (routeId, path) => {
])("keeps the client-only fallback accessible for %s", async (routeId, path) => {
const { container, router } = renderPublicRoute(routeId, path);
expect(router.state.location.pathname).toBe(path);
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
expect(
await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }),
).toBeVisible();
expect(
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
).not.toBeInTheDocument();
expect(container.querySelector(".site-frame")).toBeNull();
expect(container.querySelector(".site-frame")).not.toBeNull();
});
it("renders the source-compatible plain fallback for the Public catch-all", () => {
it("keeps the client-only Public catch-all accessible", () => {
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();
expect(
screen.getByRole("heading", { name: "페이지를 찾을 수 없습니다." }),
).toBeVisible();
expect(container.querySelector(".site-frame")).not.toBeNull();
});
});
+21 -1
View File
@@ -15,6 +15,7 @@ function ignoredConsole(message: ConsoleMessage) {
export const test = base.extend({
page: async ({ page }, use) => {
const failures: string[] = [];
let mainDocumentStatus: number | null = null;
const onConsole = (message: ConsoleMessage) => {
if (
["error", "warning"].includes(message.type()) &&
@@ -31,14 +32,33 @@ export const test = base.extend({
`requestfailed:${request.method()}:${new URL(request.url()).pathname}:${request.failure()?.errorText ?? "unknown"}`,
);
};
const onResponse = (response: import("@playwright/test").Response) => {
if (
response.request().resourceType() === "document" &&
response.frame() === page.mainFrame()
) {
mainDocumentStatus = response.status();
}
};
page.on("console", onConsole);
page.on("pageerror", onPageError);
page.on("requestfailed", onRequestFailed);
page.on("response", onResponse);
await use(page);
page.off("console", onConsole);
page.off("pageerror", onPageError);
page.off("requestfailed", onRequestFailed);
expect(failures, "unexpected browser console/page errors").toEqual([]);
page.off("response", onResponse);
const expectedDocumentNotFoundConsole =
"console:error:Failed to load resource: the server responded with a status of 404 (Not Found)";
expect(
failures.filter(
(failure) =>
failure !== expectedDocumentNotFoundConsole ||
mainDocumentStatus !== 404,
),
"unexpected browser console/page errors",
).toEqual([]);
},
});
+5 -1
View File
@@ -127,8 +127,12 @@ export async function settleTechLogPage(page: Page) {
export async function gotoTechLog(page: Page, path: string) {
await prepareTechLogPage(page);
await page.goto(path);
const response = await page.goto(path, { waitUntil: "networkidle" });
await page.locator("body").waitFor({ state: "visible" });
if (response?.headers()["content-type"]?.startsWith("text/html")) {
await page.locator("html[data-build-id][data-release-id]").waitFor();
await page.locator(".site-frame, .studio-app").waitFor();
}
await settleTechLogPage(page);
}
@@ -1,7 +1,9 @@
// @ts-nocheck -- standalone evidence runner executed directly with tsx.
// @ts-nocheck -- standalone evidence runner executed directly by Node 24.
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { lstat, mkdir, readFile, readdir, readlink, writeFile } from "node:fs/promises";
import { relative, resolve } from "node:path";
import { chromium } from "@playwright/test";
@@ -9,6 +11,7 @@ import {
TECH_LOG_BREAKPOINT_WIDTHS,
TECH_LOG_CANONICAL_ROUTES,
TECH_LOG_FIXED_TIME,
TECH_LOG_PUBLIC_FIXTURE_PATHS,
TECH_LOG_STUDIO_STATE_PATHS,
TECH_LOG_UNKNOWN_PUBLIC_PATHS,
} from "./tech-log-fixtures.ts";
@@ -18,9 +21,10 @@ 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 sourceBaseUrl = process.env.TECH_LOG_SOURCE_URL ?? "http://127.0.0.1:4375";
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 sourceRoot = process.env.TECH_LOG_SOURCE_ROOT ?? "/home/donghyeon/workspace/techlog-studio-frontend";
const outputPath = process.env.TECH_LOG_PARITY_OUTPUT ?? "docs/operations/evidence/tech-log-source-parity.json";
const fixedDate = new Date(TECH_LOG_FIXED_TIME);
const noMotionCss = `
*, *::before, *::after {
@@ -35,6 +39,11 @@ const noMotionCss = `
const allCases = [
...TECH_LOG_CANONICAL_ROUTES.flatMap(({ routeId, path }) =>
[360, 1440].map((width) => ({ name: `${routeId}-${width}`, path, width }))),
...TECH_LOG_PUBLIC_FIXTURE_PATHS.map((path) => ({
name: `TECH_LOG_PUBLIC_FIXTURE-${path.slice(1).replaceAll("/", "-").toUpperCase()}`,
path,
width: 1440,
})),
...TECH_LOG_BREAKPOINT_WIDTHS.map((width) => ({
name: `TECH_LOG_HOME-BREAKPOINT-${width}`,
path: "/",
@@ -117,45 +126,123 @@ async function interact(page, action) {
}
}
async function settle(page, baseUrl, parityCase) {
async function settle(page, baseUrl, parityCase, waitForTargetBootstrap) {
page.setDefaultNavigationTimeout(15_000);
page.setDefaultTimeout(15_000);
await page.setViewportSize({ width: parityCase.width, height: 1000 });
await page.clock.setFixedTime(fixedDate);
await page.goto(new URL(parityCase.path, baseUrl).href, { waitUntil: "networkidle" });
const navigationResponse = await page.goto(
new URL(parityCase.path, baseUrl).href,
{ waitUntil: "networkidle" },
);
if (!navigationResponse) throw new Error(`Missing navigation response: ${parityCase.name}`);
const response = await responseMetadata(navigationResponse);
if (response.mimeType === "text/html" && waitForTargetBootstrap) {
// The target sets these only after config + release-manifest bodies have
// been fully read, validated, and composed. This is the product's boot
// completion signal, not a delay or retry.
await page.locator("html[data-build-id][data-release-id]").waitFor();
}
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);
return response;
}
async function projection(page) {
async function productSubtree(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) => ({
const normalize = (value) => value
.replace(/_[rR][^\s"']*?(?=-|\s|$)/g, "<generated-id>")
.replace(/_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)/g, "_$1_<generated-module>_$2");
const frameworkGeneratedAttributes = new Set([
"data-discover", // React Router link discovery metadata.
"data-nimg", // Next/Image metadata derived from source props.
"decoding", // Next/Image output derived from priority=false.
"srcset", // Next/Image widths derived from one source SVG.
"selected", // Next SSR output for a controlled <select> value.
]);
const serialize = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.nodeValue?.replace(/\s+/g, " ").trim() ?? "";
return text ? { type: "text", text } : null;
}
if (node.nodeType !== Node.ELEMENT_NODE) return null;
const element = node;
return {
type: "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),
})),
}));
classes: [...element.classList].map(normalize),
attributes: [...element.attributes]
.filter(({ name }) =>
name !== "class" && !frameworkGeneratedAttributes.has(name))
.map(({ name, value }) => [name, normalize(value)])
.sort(([left], [right]) => left.localeCompare(right)),
children: [...element.childNodes].map(serialize).filter(Boolean),
};
};
const root = document.querySelector(".site-frame, .studio-app") ?? document.body;
return serialize(root);
});
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
async function responseMetadata(response) {
const body = await response.body();
const contentType = response.headers()["content-type"] ?? "";
return {
status: response.status(),
contentType,
mimeType: contentType.split(";", 1)[0].toLowerCase(),
bodyBytes: body.byteLength,
bodySha256: sha256(body),
};
}
function responseContractEqual(source, target) {
if (source.status !== target.status || source.mimeType !== target.mimeType) return false;
if (source.status !== 404 || source.mimeType === "text/html") return true;
return source.contentType === target.contentType &&
source.bodyBytes === 9 &&
target.bodyBytes === 9 &&
source.bodySha256 === target.bodySha256 &&
source.bodySha256 === sha256("Not Found");
}
const excludedSourceDirectories = new Set([
".git", ".next", ".sites-runtime", ".vinext", ".turbo", "artifacts", "coverage", "dist", "node_modules",
]);
async function sourceChecksums(root) {
const checksums = [];
async function walk(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
if (entry.isDirectory() && excludedSourceDirectories.has(entry.name)) continue;
const absolutePath = resolve(directory, entry.name);
const sourcePath = relative(root, absolutePath).replaceAll("\\", "/");
if (entry.isDirectory()) {
await walk(absolutePath);
} else if (entry.isSymbolicLink()) {
const target = await readlink(absolutePath);
checksums.push({ path: sourcePath, bytes: Buffer.byteLength(target), sha256: sha256(target), kind: "symlink" });
} else if ((await lstat(absolutePath)).isFile()) {
const bytes = await readFile(absolutePath);
checksums.push({ path: sourcePath, bytes: bytes.byteLength, sha256: sha256(bytes), kind: "file" });
}
}
}
await walk(resolve(root));
checksums.sort((left, right) => left.path.localeCompare(right.path));
return {
files: checksums,
digest: sha256(checksums.map(({ path, bytes, sha256: digest, kind }) => `${kind}\0${path}\0${bytes}\0${digest}\n`).join("")),
};
}
async function layoutProjection(page) {
return page.evaluate(() => [...document.querySelectorAll("header, main, main *, footer, dialog, .studio-app *")].map((element) => {
const box = element.getBoundingClientRect();
@@ -194,6 +281,44 @@ function pixelDifference(sourceBuffer, targetBuffer) {
return { pixels, sourceSize: [source.width, source.height], targetSize: [target.width, target.height] };
}
function observePage(page, errors) {
const bootRequests = [];
const records = new Map();
const isBootRequest = (request) => {
const pathname = new URL(request.url()).pathname;
return pathname === "/config.json" || pathname === "/release-manifest.json";
};
page.on("request", (request) => {
if (!isBootRequest(request)) return;
const record = {
url: request.url(),
method: request.method(),
resourceType: request.resourceType(),
state: "started",
};
records.set(request, record);
bootRequests.push(record);
});
page.on("requestfinished", (request) => {
const record = records.get(request);
if (record) record.state = "finished";
});
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) => {
const failure = request.failure()?.errorText ?? "";
const record = records.get(request);
if (record) {
record.state = "failed";
record.failure = failure;
}
errors.push(`requestfailed: ${request.resourceType()} ${request.url()} ${failure}`);
});
return bootRequests;
}
const browser = await chromium.launch();
const contextOptions = {
colorScheme: "light",
@@ -203,40 +328,40 @@ const contextOptions = {
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 sourceContext = await browser.newContext(contextOptions);
const targetContext = await browser.newContext(contextOptions);
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 sourceBootRequests = observePage(sourcePage, sourceErrors);
const targetBootRequests = observePage(targetPage, targetErrors);
try {
const [sourceResponse, targetResponse] = await Promise.all([
settle(sourcePage, sourceBaseUrl, parityCase, false),
settle(targetPage, targetBaseUrl, parityCase, true),
]);
const [sourceShot, targetShot, sourceDom, targetDom, sourceLayout, targetLayout, sourceFonts, targetFonts] = await Promise.all([
const [sourceShot, targetShot, sourceTree, targetTree, sourceLayout, targetLayout, sourceFonts, targetFonts] = await Promise.all([
sourcePage.screenshot({ fullPage: true }),
targetPage.screenshot({ fullPage: true }),
projection(sourcePage),
projection(targetPage),
productSubtree(sourcePage),
productSubtree(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 sourceTreeJson = JSON.stringify(sourceTree);
const targetTreeJson = JSON.stringify(targetTree);
const structureEqual = sourceTreeJson === targetTreeJson;
const fontsEqual = JSON.stringify(sourceFonts) === JSON.stringify(targetFonts);
const isPlainNotFound =
parityCase.name.includes("NOT_FOUND") || parityCase.name.includes("UNKNOWN");
const responseEqual = responseContractEqual(sourceResponse, targetResponse);
const isPlainNotFound = sourceResponse.mimeType === "text/plain";
const expectsSourceNotFoundResponse =
isPlainNotFound || parityCase.path.includes("unknown");
const expectedNotFoundConsole = expectsSourceNotFoundResponse
@@ -245,36 +370,81 @@ try {
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;
const passed = pixel.pixels === 0 && structureEqual && responseEqual && (!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`),
writeFile(resolve(diagnosticDirectory, `${stem}-dom.json`), `${JSON.stringify({ sourceTree, targetTree, 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()]);
}
results.push({
name: parityCase.name,
path: parityCase.path,
width: parityCase.width,
action: parityCase.action ?? null,
pixel,
screenshotSha256: { source: sha256(sourceShot), target: sha256(targetShot) },
structureEqual,
productSubtreeSha256: { source: sha256(sourceTreeJson), target: sha256(targetTreeJson) },
responseEqual,
response: { source: sourceResponse, target: targetResponse },
fontsEqual,
fontContractApplies,
sourceFonts,
targetFonts,
sourceErrors,
targetErrors,
sourceBootRequests,
targetBootRequests,
unexplainedSourceErrors,
unexplainedTargetErrors,
passed,
});
console.log(`${passed ? "PASS" : "FAIL"} ${parityCase.name} pixels=${pixel.pixels ?? "SIZE"} tree=${structureEqual} response=${responseEqual} fonts=${fontContractApplies ? fontsEqual : "source-plain-text"} errors=${unexplainedSourceErrors.length}/${unexplainedTargetErrors.length}`);
} finally {
await Promise.all([sourceContext.close(), targetContext.close()]);
}
}
} finally {
await browser.close();
}
const evidence = {
const [sourceTree, buildManifest, viteManifest] = await Promise.all([
sourceChecksums(sourceRoot),
readFile("artifacts/release/build-manifest.json"),
readFile("dist/.vite/manifest.json"),
]);
const evidenceWithoutDigest = {
generatedAt: new Date().toISOString(),
sourceBaseUrl,
targetBaseUrl,
conditions: { ...contextOptions, fixedTime: TECH_LOG_FIXED_TIME, viewportHeight: 1000, screenshots: "fullPage", masks: 0 },
schemaVersion: 1,
source: {
baseUrl: sourceBaseUrl,
root: sourceRoot,
treeSha256: sourceTree.digest,
checksums: sourceTree.files,
},
target: {
baseUrl: targetBaseUrl,
commit: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(),
buildManifestSha256: sha256(buildManifest),
viteManifestSha256: sha256(viteManifest),
},
conditions: { ...contextOptions, fixedTime: TECH_LOG_FIXED_TIME, viewportHeight: 1000, screenshots: "fullPage", masks: 0, freshSourceAndTargetContextsPerCase: true },
caseInventory: allCases,
caseInventorySha256: sha256(JSON.stringify(allCases)),
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,
};
const evidence = {
...evidenceWithoutDigest,
evidenceSha256: sha256(JSON.stringify(evidenceWithoutDigest)),
};
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 }));
+23 -2
View File
@@ -192,14 +192,35 @@ describe("CI gate contract", () => {
expect(contract.commands).toHaveLength(81);
expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(93);
expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23);
expect(contract.gates.reduce((total, gate) => total + gate.evidenceArtifactIds.length, 0)).toBe(85);
expect(contract.artifacts).toHaveLength(105);
expect(contract.gates.reduce((total, gate) => total + gate.evidenceArtifactIds.length, 0)).toBe(106);
expect(contract.artifacts).toHaveLength(126);
expect(contract.stages).toHaveLength(5);
expect(contract.retention.classes).toHaveLength(5);
expect(index.gates.get("FE-GATE-015")?.commandIds).toHaveLength(2);
expect(index.gates.get("FE-GATE-020")?.name).toBe("removability");
});
it("rejects accessibility evidence registrations that drift from the installed route scope", async () => {
const candidate = JSON.parse(
JSON.stringify(await loadCiGateContract(process.cwd())),
) as Record<string, any>;
const gate = candidate.gates.find(
(entry: Record<string, any>) => entry.id === "FE-GATE-009",
);
const artifactId =
"artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md";
candidate.artifacts = candidate.artifacts.filter(
(entry: Record<string, any>) => entry.id !== artifactId,
);
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter(
(id: string) => id !== artifactId,
);
expect(() =>
parseCiGateContract(candidate, { mode: "removal-fixture" }),
).toThrow(/FE-GATE-009 manual accessibility evidence.*installed route scope/i);
});
it("keeps the action registry recursively immutable and resolves only known actions", () => {
expect(Object.isFrozen(CI_ACTION_REGISTRY)).toBe(true);
expect(Object.values(CI_ACTION_REGISTRY).every((action) => Object.isFrozen(action))).toBe(true);
+1 -1
View File
@@ -7,7 +7,7 @@ import {
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
const reviewed = `Status: reviewed
Route ID: APP_HOME
Route ID: TECH_LOG_HOME
Release ID: release-1
Reviewer: reviewer@example.test
Reviewed at: 2026-07-25T12:00:00Z
@@ -148,7 +148,7 @@ describe("selective Task 3 contract closure", () => {
expect(canonical.gates).toHaveLength(26);
expect(canonical.commands).toHaveLength(81);
expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(93);
expect(canonical.artifacts).toHaveLength(105);
expect(canonical.artifacts).toHaveLength(126);
expect(canonical.stages).toHaveLength(5);
expect(canonical.retention.classes).toHaveLength(5);
@@ -0,0 +1,26 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
describe("TechLog source parity evidence harness", () => {
it("is a checked Node runner with fixture, response, recursive tree, and provenance coverage", async () => {
const [packageJson, source] = await Promise.all([
readFile("package.json", "utf8").then(JSON.parse),
readFile("tests/support/browser/verify-tech-log-source-parity.ts", "utf8"),
]);
expect(packageJson.scripts["verify:tech-log-source-parity"]).toBe(
"node tests/support/browser/verify-tech-log-source-parity.ts",
);
expect(source).toContain("TECH_LOG_PUBLIC_FIXTURE_PATHS.map");
expect(source).toContain("element.childNodes");
expect(source).toContain("responseContractEqual");
expect(source).toContain("sourceChecksums(sourceRoot)");
expect(source).toContain("caseInventorySha256");
expect(source).toContain("evidenceSha256");
expect(source).toContain(
'"docs/operations/evidence/tech-log-source-parity.json"',
);
expect(source).not.toContain("tsx");
});
});
@@ -0,0 +1,44 @@
import { pathToFileURL } from "node:url";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { writeTechLogServingArtifact } from "../../scripts/lib/tech-log-serving-artifact.ts";
const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true })));
});
describe("TechLog serving artifact", () => {
it("emits a self-contained deployment server and its route contract", async () => {
const root = await mkdtemp(path.join(tmpdir(), "tech-log-serving-artifact-"));
roots.push(root);
const contract = {
schemaVersion: 1 as const,
publicSpaPaths: ["/", "/projects/auth-lab"],
studioPathPrefix: "/studio" as const,
studioSpaPathPatterns: ["^/studio$"],
notFound: {
status: 404 as const,
contentType: "text/plain;charset=UTF-8" as const,
body: "Not Found" as const,
},
};
await writeTechLogServingArtifact({ distRoot: root, contract });
expect(
JSON.parse(
await readFile(path.join(root, "tech-log-serving-contract.json"), "utf8"),
),
).toEqual(contract);
const serverModule = await import(
`${pathToFileURL(path.join(root, "server.mjs")).href}?test=${Date.now()}`
);
expect(serverModule.createTechLogProductionServer).toEqual(expect.any(Function));
});
});
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { createTechLogServingContract } from "../../scripts/lib/tech-log-serving-contract.ts";
import {
projects,
publicRecords,
releases,
} from "../../src/features/tech-log/adapters/static/public-content.ts";
describe("TechLog production serving contract", () => {
it("derives every known Public path from the installed static content", () => {
const contract = createTechLogServingContract({
projects,
publicRecords,
releases,
});
expect(contract.publicSpaPaths).toEqual([
"/",
"/cases/collection-fetch-join-pagination",
"/cases/redis-adapter-ttl-boundary",
"/explore",
"/explore/cases",
"/explore/questions",
"/explore/references",
"/profile",
"/projects",
"/projects/auth-lab",
"/projects/auth-lab/activity",
"/projects/auth-lab/decisions",
"/projects/auth-lab/records",
"/projects/backend-skeleton",
"/projects/backend-skeleton/activity",
"/projects/backend-skeleton/decisions",
"/projects/backend-skeleton/records",
"/questions/collection-fetch-join-with-pagination",
"/questions/validate-edge-token-again",
"/references/jpa-list-fetch-strategy",
"/references/state-and-nonce-boundary",
"/releases",
"/releases/0.1.0",
"/search",
"/topics/authentication",
"/topics/jpa",
"/topics/redis",
]);
expect(contract.studioPathPrefix).toBe("/studio");
expect(contract.studioSpaPathPatterns).toEqual([
"^/studio$",
"^/studio/documents$",
"^/studio/documents/new$",
"^/studio/documents/[^/]+/(edit|validation|preview|publish)$",
"^/studio/publications$",
"^/studio/publications/[^/]+/preview$",
]);
expect(contract.notFound).toEqual({
status: 404,
contentType: "text/plain;charset=UTF-8",
body: "Not Found",
});
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+17
View File
@@ -3,10 +3,15 @@ import {
gotoTechLog,
TECH_LOG_BREAKPOINT_WIDTHS,
TECH_LOG_CANONICAL_ROUTES,
TECH_LOG_PUBLIC_FIXTURE_PATHS,
TECH_LOG_STUDIO_STATE_PATHS,
TECH_LOG_UNKNOWN_PUBLIC_PATHS,
} from "../support/browser/tech-log-fixtures.ts";
function fixtureSnapshotName(path: string) {
return `tech-log-public-fixture-${path.slice(1).replaceAll("/", "-")}-1440.png`;
}
for (const route of TECH_LOG_CANONICAL_ROUTES) {
for (const width of [360, 1440] as const) {
test(`${route.routeId} matches the reviewed ${width}px visual`, async ({ page }) => {
@@ -19,6 +24,18 @@ for (const route of TECH_LOG_CANONICAL_ROUTES) {
}
}
for (const path of TECH_LOG_PUBLIC_FIXTURE_PATHS) {
test(`known Public fixture ${path} matches its reviewed wide visual`, async ({
page,
}) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await gotoTechLog(page, path);
await expect(page).toHaveScreenshot(fixtureSnapshotName(path), {
fullPage: true,
});
});
}
for (const width of TECH_LOG_BREAKPOINT_WIDTHS) {
test(`Public home matches the reviewed ${width}px breakpoint visual`, async ({ page }) => {
await page.setViewportSize({ width, height: 1000 });