fix: complete TechLog migration evidence
@@ -43,6 +43,18 @@ load. The exact route, dependency, stylesheet, asset, and parity inventory is
|
|||||||
recorded in
|
recorded in
|
||||||
[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md).
|
[`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
|
## Architecture
|
||||||
|
|
||||||
Dependencies point inward:
|
Dependencies point inward:
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# APP_HOME accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: APP_HOME
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Automated axe, keyboard-focus, and reduced-motion evidence is available; human review pending.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# EXAMPLES_AUTH accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: EXAMPLES_AUTH
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Review session state announcements and unavailable external-integration behavior.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# EXAMPLES_PLATFORM accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: EXAMPLES_PLATFORM
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Review the horizontally scrollable registry tables for keyboard reachability of the scroll container, table caption and header association announced per row, capability status badges carrying their meaning in text rather than colour alone, and the release identity region announcing its update through aria-live without interrupting a reader mid row.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# EXAMPLES_STATES accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: EXAMPLES_STATES
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Review loading, refresh, empty, error, authentication, forbidden, and not-found announcements.
|
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# REFERENCE_RESOURCE_STATUS accessibility review
|
|
||||||
|
|
||||||
Status: pending-manual-review
|
|
||||||
Route ID: REFERENCE_RESOURCE_STATUS
|
|
||||||
Release ID:
|
|
||||||
Reviewer:
|
|
||||||
Reviewed at:
|
|
||||||
Signature:
|
|
||||||
Attestation: pending
|
|
||||||
M1 Keyboard: pending
|
|
||||||
M2 Visible focus: pending
|
|
||||||
M3 Route focus: pending
|
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
|
||||||
M6 Color signal: pending
|
|
||||||
M7 Reduced motion: pending
|
|
||||||
Screen reader: pending
|
|
||||||
Notes: Human review pending.
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_FORM accessibility review
|
# TECH_LOG_CASE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_FORM
|
Route ID: TECH_LOG_CASE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -15,4 +15,4 @@ M5 Error association: pending
|
|||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# EXAMPLES_UI accessibility review
|
# TECH_LOG_EXPLORE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: EXAMPLES_UI
|
Route ID: TECH_LOG_EXPLORE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -15,4 +15,4 @@ M5 Error association: pending
|
|||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Review form primitives, Menu/Tabs keyboard behavior, Toast announcements, Tooltip supplemental copy, text-field error association and modal focus containment/restoration.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_EXPLORE_KIND accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_EXPLORE_KIND
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_LIST accessibility review
|
# TECH_LOG_HOME accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_LIST
|
Route ID: TECH_LOG_HOME
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# REFERENCE_RESOURCE_DETAIL accessibility review
|
# TECH_LOG_PROFILE accessibility review
|
||||||
|
|
||||||
Status: pending-manual-review
|
Status: pending-manual-review
|
||||||
Route ID: REFERENCE_RESOURCE_DETAIL
|
Route ID: TECH_LOG_PROFILE
|
||||||
Release ID:
|
Release ID:
|
||||||
Reviewer:
|
Reviewer:
|
||||||
Reviewed at:
|
Reviewed at:
|
||||||
@@ -10,9 +10,9 @@ Attestation: pending
|
|||||||
M1 Keyboard: pending
|
M1 Keyboard: pending
|
||||||
M2 Visible focus: pending
|
M2 Visible focus: pending
|
||||||
M3 Route focus: pending
|
M3 Route focus: pending
|
||||||
M4 Modal focus: not-applicable (no modal on this route)
|
M4 Modal focus: pending
|
||||||
M5 Error association: not-applicable (no form error on this route)
|
M5 Error association: pending
|
||||||
M6 Color signal: pending
|
M6 Color signal: pending
|
||||||
M7 Reduced motion: pending
|
M7 Reduced motion: pending
|
||||||
Screen reader: pending
|
Screen reader: pending
|
||||||
Notes: Human review pending.
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECTS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECTS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT_ACTIVITY accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT_ACTIVITY
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT_DECISIONS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT_DECISIONS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_PROJECT_RECORDS accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_PROJECT_RECORDS
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_QUESTION accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_QUESTION
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_REFERENCE accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_REFERENCE
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_RELEASE accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_RELEASE
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_RELEASES accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_RELEASES
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_SEARCH accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: TECH_LOG_SEARCH
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# TECH_LOG_STUDIO_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.
|
||||||
@@ -1006,58 +1006,166 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
|
||||||
"path": "artifacts/tests/a11y-manual/APP_HOME.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_HOME.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_UI.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_CASE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
|
||||||
"path": "artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md",
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
},
|
||||||
"review-a11y-manual"
|
{
|
||||||
]
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
|
||||||
|
"path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md",
|
||||||
|
"schemaId": "markdown",
|
||||||
|
"production": "source-controlled"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
||||||
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
|
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
|
||||||
"schemaId": "markdown",
|
"schemaId": "markdown",
|
||||||
"production": "command-generated",
|
"production": "source-controlled"
|
||||||
"producerCommandIds": [
|
|
||||||
"review-a11y-manual"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "artifact-artifacts-tests-a11y-manual-report-json",
|
"id": "artifact-artifacts-tests-a11y-manual-report-json",
|
||||||
@@ -1757,11 +1865,32 @@
|
|||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
|
||||||
"evidenceArtifactIds": [
|
"evidenceArtifactIds": [
|
||||||
"artifact-artifacts-tests-a11y-json",
|
"artifact-artifacts-tests-a11y-json",
|
||||||
"artifact-artifacts-tests-a11y-manual-APP-HOME-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md",
|
||||||
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
"artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
||||||
"artifact-artifacts-tests-a11y-manual-report-json"
|
"artifact-artifacts-tests-a11y-manual-report-json"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# Manual accessibility review checklist
|
# Manual accessibility review checklist
|
||||||
|
|
||||||
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
||||||
review all six route records in `artifacts/tests/a11y-manual/` against one
|
review all 27 route records in `artifacts/tests/a11y-manual/` against one
|
||||||
release candidate and sign them. The required scope is derived from the route
|
release candidate and sign them. The required TechLog Public and Studio scope is
|
||||||
registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`,
|
derived from the installed route registry, so the gate rejects stale, missing,
|
||||||
`REFERENCE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the
|
or additional route records as well as blank identity/timestamp/signature
|
||||||
gate rejects blank identity/timestamp/signature fields, pending verdicts,
|
fields, pending verdicts, and mismatched release IDs.
|
||||||
mismatched release IDs, or missing routes.
|
|
||||||
|
|
||||||
Allowed item verdicts:
|
Allowed item verdicts:
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ Required record:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
Status: reviewed
|
Status: reviewed
|
||||||
Route ID: APP_HOME
|
Route ID: <exact installed route ID>
|
||||||
Release ID: <immutable release ID>
|
Release ID: <immutable release ID>
|
||||||
Reviewer: <human reviewer identity>
|
Reviewer: <human reviewer identity>
|
||||||
Reviewed at: <RFC 3339 timestamp>
|
Reviewed at: <RFC 3339 timestamp>
|
||||||
@@ -45,7 +44,8 @@ The reviewer must verify:
|
|||||||
- M7: non-essential motion is suppressed with reduced-motion preference
|
- M7: non-essential motion is suppressed with reduced-motion preference
|
||||||
- Screen reader: headings, live regions, errors, and actions are announced once
|
- Screen reader: headings, live regions, errors, and actions are announced once
|
||||||
|
|
||||||
`EXAMPLES_UI` requires real M4 modal and M5 field-error review; those items must
|
Routes with dialogs or form errors require real M4 modal-focus or M5
|
||||||
not be marked not-applicable on that route. Passing automated evidence means
|
error-association review; those items must not be marked not-applicable when the
|
||||||
|
reviewed route exposes the relevant behavior. Passing automated evidence means
|
||||||
only that tested pages had no critical or serious axe findings under the
|
only that tested pages had no critical or serious axe findings under the
|
||||||
recorded Chromium, Firefox, and WebKit runs.
|
recorded browser runs.
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "node scripts/build-frontend.ts",
|
"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",
|
"build:release-candidate": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security && corepack pnpm verify:release && node scripts/verify-supply-chain-artifacts.ts && node scripts/create-release-candidate.ts",
|
||||||
"preview": "vite preview",
|
"preview": "node dist/server.mjs",
|
||||||
|
"verify:tech-log-source-parity": "node tests/support/browser/verify-tech-log-source-parity.ts",
|
||||||
"lint": "eslint src scripts tests recipes .storybook vite.config.ts vitest.config.ts playwright*.config.ts --max-warnings=0",
|
"lint": "eslint src scripts tests recipes .storybook vite.config.ts vitest.config.ts playwright*.config.ts --max-warnings=0",
|
||||||
"check:architecture": "node scripts/check-architecture.ts",
|
"check:architecture": "node scripts/check-architecture.ts",
|
||||||
"check:design-system": "node scripts/check-design-system.ts",
|
"check:design-system": "node scripts/check-design-system.ts",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim
|
|||||||
* 3. Vite app build (emptyOutDir = true)
|
* 3. Vite app build (emptyOutDir = true)
|
||||||
* 4. scan app dist and generate the static asset source
|
* 4. scan app dist and generate the static asset source
|
||||||
* 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
|
* 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`
|
* Steps 4 and 5 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
|
||||||
* and `null`: those modes never run an active worker build.
|
* and `null`: those modes never run an active worker build.
|
||||||
@@ -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"]);
|
run("node", ["scripts/generate-build-manifest.ts"]);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts";
|
import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts";
|
||||||
|
import { MANUAL_A11Y_ROUTE_IDS } from "../lib/manual-a11y-evidence.ts";
|
||||||
import {
|
import {
|
||||||
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
||||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||||
@@ -442,7 +443,7 @@ export type LoadCiGateContractOptions = Readonly<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
const CANONICAL_GATE_SHAPE_SHA256 =
|
const CANONICAL_GATE_SHAPE_SHA256 =
|
||||||
"a4a963d0b9deffb7a0a3d755bbbcb979d72610eb74751c3a2e5eca55251e12d4";
|
"492331de6d07926b1cdb569824c8feaa761eef76b9ac782c0404d40797383ef0";
|
||||||
|
|
||||||
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
||||||
const normalized = gates.map(
|
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`,
|
`command authority baseline must contain exactly 81 definitions and 93 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (contract.artifacts.length !== 105) {
|
if (contract.artifacts.length !== 126) {
|
||||||
failures.push(`artifact authority baseline must contain exactly 105 artifacts; received ${contract.artifacts.length}`);
|
failures.push(`artifact authority baseline must contain exactly 126 artifacts; received ${contract.artifacts.length}`);
|
||||||
}
|
}
|
||||||
if (contract.stages.length !== 5) {
|
if (contract.stages.length !== 5) {
|
||||||
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
|
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
|
||||||
@@ -723,6 +724,44 @@ function validateContractSemantics(
|
|||||||
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
|
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessibilityGate = contract.gates.find(
|
||||||
|
({ id: gateId }) => gateId === "FE-GATE-009",
|
||||||
|
);
|
||||||
|
const manualAccessibilityArtifacts = MANUAL_A11Y_ROUTE_IDS.map((routeId) => ({
|
||||||
|
id: `artifact-artifacts-tests-a11y-manual-${routeId.replaceAll("_", "-")}-md`,
|
||||||
|
path: `artifacts/tests/a11y-manual/${routeId}.md`,
|
||||||
|
}));
|
||||||
|
const expectedAccessibilityEvidenceArtifactIds = [
|
||||||
|
"artifact-artifacts-tests-a11y-json",
|
||||||
|
...manualAccessibilityArtifacts.map(({ id: artifactId }) => artifactId),
|
||||||
|
"artifact-artifacts-tests-a11y-manual-report-json",
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
!accessibilityGate ||
|
||||||
|
JSON.stringify(accessibilityGate.evidenceArtifactIds) !==
|
||||||
|
JSON.stringify(expectedAccessibilityEvidenceArtifactIds)
|
||||||
|
) {
|
||||||
|
issue(
|
||||||
|
"FE-GATE-009 manual accessibility evidence must exactly match the installed route scope",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const expected of manualAccessibilityArtifacts) {
|
||||||
|
const artifact = contract.artifacts.find(
|
||||||
|
({ id: artifactId }) => artifactId === expected.id,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!artifact ||
|
||||||
|
artifact.path !== expected.path ||
|
||||||
|
artifact.schemaId !== "markdown" ||
|
||||||
|
artifact.production !== "source-controlled"
|
||||||
|
) {
|
||||||
|
issue(
|
||||||
|
`FE-GATE-009 manual accessibility artifact registration is invalid: ${expected.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const referencedRetentionClasses = new Set(
|
const referencedRetentionClasses = new Set(
|
||||||
contract.gates.map(({ retentionClassId }) => retentionClassId),
|
contract.gates.map(({ retentionClassId }) => retentionClassId),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 });
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -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() {
|
export function PublicNotFoundPage() {
|
||||||
useSourceCompatiblePublicNotFound();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<main id="main-content" className="shell public-index-page">
|
||||||
<style>{`
|
<header className="public-page-header">
|
||||||
html { background: white; color: black; }
|
<p className="section-kicker">404</p>
|
||||||
body {
|
<h1>페이지를 찾을 수 없습니다.</h1>
|
||||||
min-width: 0;
|
<p>요청한 공개 페이지가 없습니다.</p>
|
||||||
margin: 8px;
|
</header>
|
||||||
overflow: visible;
|
</main>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { publicSiteConfig } from "../../contracts/public-site-config.ts";
|
import { publicSiteConfig } from "../../contracts/public-site-config.ts";
|
||||||
import { SiteHeader } from "./components/site-header.tsx";
|
import { SiteHeader } from "./components/site-header.tsx";
|
||||||
import { PublicNotFoundLayoutContext } from "./public-not-found-layout.tsx";
|
|
||||||
|
|
||||||
type PublicShellProps = {
|
type PublicShellProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -11,13 +10,7 @@ type PublicShellProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function PublicShell({ children, currentPath }: PublicShellProps) {
|
export function PublicShell({ children, currentPath }: PublicShellProps) {
|
||||||
const [nestedNotFound, setNestedNotFound] = useState(false);
|
|
||||||
const updateNestedNotFound = useCallback((active: boolean) => {
|
|
||||||
setNestedNotFound(active);
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<PublicNotFoundLayoutContext.Provider value={updateNestedNotFound}>
|
|
||||||
{nestedNotFound ? children : (
|
|
||||||
<div className="site-frame">
|
<div className="site-frame">
|
||||||
<a className="skip-link" href="#main-content">
|
<a className="skip-link" href="#main-content">
|
||||||
본문으로 건너뛰기
|
본문으로 건너뛰기
|
||||||
@@ -41,7 +34,5 @@ export function PublicShell({ children, currentPath }: PublicShellProps) {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</PublicNotFoundLayoutContext.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,12 +343,15 @@ describe("generic application router", () => {
|
|||||||
expect(window.location.pathname).toBe("/projects");
|
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");
|
window.history.pushState({}, "", "/missing");
|
||||||
renderRouter();
|
renderRouter();
|
||||||
|
|
||||||
expect(await screen.findByText("Not Found", { exact: true })).toBeVisible();
|
expect(
|
||||||
expect(screen.queryByRole("navigation", { name: "주요 탐색" })).not.toBeInTheDocument();
|
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 () => {
|
it("keeps Studio routes inside the persistent Studio layout", async () => {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
TECH_LOG_CANONICAL_ROUTES,
|
TECH_LOG_CANONICAL_ROUTES,
|
||||||
} from "../support/browser/tech-log-fixtures.ts";
|
} 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 ({
|
test(`@a11y ${route.routeId} has no critical or serious Axe violations`, async ({
|
||||||
page,
|
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 ({
|
test("@a11y keyboard reaches a visible Public navigation focus indicator", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -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");
|
).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(
|
const { router, container } = renderDiscoveryRoute(
|
||||||
"TECH_LOG_EXPLORE_KIND",
|
"TECH_LOG_EXPLORE_KIND",
|
||||||
"/explore/unknown",
|
"/explore/unknown",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(router.state.location.pathname).toBe("/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(
|
expect(
|
||||||
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
|
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
|
||||||
).not.toBeInTheDocument();
|
).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 () => {
|
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_REFERENCE" as const, "/references/not-registered"],
|
||||||
["TECH_LOG_QUESTION" as const, "/questions/not-registered"],
|
["TECH_LOG_QUESTION" as const, "/questions/not-registered"],
|
||||||
["TECH_LOG_TOPIC" as const, "/topics/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);
|
const { container, router } = renderDocumentRoute(routeId, path);
|
||||||
|
|
||||||
expect(router.state.location.pathname).toBe(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(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_PROJECT_RECORDS" as const, "/projects/missing-project/records"],
|
||||||
["TECH_LOG_RELEASE" as const, "/releases/9.9.9"],
|
["TECH_LOG_RELEASE" as const, "/releases/9.9.9"],
|
||||||
["TECH_LOG_CASE" as const, "/cases/not-registered"],
|
["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);
|
const { container, router } = renderPublicRoute(routeId, path);
|
||||||
|
|
||||||
expect(router.state.location.pathname).toBe(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(
|
expect(
|
||||||
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
|
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
|
||||||
).not.toBeInTheDocument();
|
).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(
|
const { container, router } = renderPublicRoute(
|
||||||
"NOT_FOUND",
|
"NOT_FOUND",
|
||||||
"/definitely-not-a-product-route",
|
"/definitely-not-a-product-route",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(router.state.location.pathname).toBe("/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(
|
||||||
expect(container.querySelector(".site-frame")).toBeNull();
|
screen.getByRole("heading", { name: "페이지를 찾을 수 없습니다." }),
|
||||||
|
).toBeVisible();
|
||||||
|
expect(container.querySelector(".site-frame")).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function ignoredConsole(message: ConsoleMessage) {
|
|||||||
export const test = base.extend({
|
export const test = base.extend({
|
||||||
page: async ({ page }, use) => {
|
page: async ({ page }, use) => {
|
||||||
const failures: string[] = [];
|
const failures: string[] = [];
|
||||||
|
let mainDocumentStatus: number | null = null;
|
||||||
const onConsole = (message: ConsoleMessage) => {
|
const onConsole = (message: ConsoleMessage) => {
|
||||||
if (
|
if (
|
||||||
["error", "warning"].includes(message.type()) &&
|
["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"}`,
|
`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("console", onConsole);
|
||||||
page.on("pageerror", onPageError);
|
page.on("pageerror", onPageError);
|
||||||
page.on("requestfailed", onRequestFailed);
|
page.on("requestfailed", onRequestFailed);
|
||||||
|
page.on("response", onResponse);
|
||||||
await use(page);
|
await use(page);
|
||||||
page.off("console", onConsole);
|
page.off("console", onConsole);
|
||||||
page.off("pageerror", onPageError);
|
page.off("pageerror", onPageError);
|
||||||
page.off("requestfailed", onRequestFailed);
|
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([]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -127,8 +127,12 @@ export async function settleTechLogPage(page: Page) {
|
|||||||
|
|
||||||
export async function gotoTechLog(page: Page, path: string) {
|
export async function gotoTechLog(page: Page, path: string) {
|
||||||
await prepareTechLogPage(page);
|
await prepareTechLogPage(page);
|
||||||
await page.goto(path);
|
const response = await page.goto(path, { waitUntil: "networkidle" });
|
||||||
await page.locator("body").waitFor({ state: "visible" });
|
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);
|
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 { createRequire } from "node:module";
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
import { lstat, mkdir, readFile, readdir, readlink, writeFile } from "node:fs/promises";
|
||||||
import { resolve } from "node:path";
|
import { relative, resolve } from "node:path";
|
||||||
|
|
||||||
import { chromium } from "@playwright/test";
|
import { chromium } from "@playwright/test";
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ import {
|
|||||||
TECH_LOG_BREAKPOINT_WIDTHS,
|
TECH_LOG_BREAKPOINT_WIDTHS,
|
||||||
TECH_LOG_CANONICAL_ROUTES,
|
TECH_LOG_CANONICAL_ROUTES,
|
||||||
TECH_LOG_FIXED_TIME,
|
TECH_LOG_FIXED_TIME,
|
||||||
|
TECH_LOG_PUBLIC_FIXTURE_PATHS,
|
||||||
TECH_LOG_STUDIO_STATE_PATHS,
|
TECH_LOG_STUDIO_STATE_PATHS,
|
||||||
TECH_LOG_UNKNOWN_PUBLIC_PATHS,
|
TECH_LOG_UNKNOWN_PUBLIC_PATHS,
|
||||||
} from "./tech-log-fixtures.ts";
|
} 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"),
|
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 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 fixedDate = new Date(TECH_LOG_FIXED_TIME);
|
||||||
const noMotionCss = `
|
const noMotionCss = `
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
@@ -35,6 +39,11 @@ const noMotionCss = `
|
|||||||
const allCases = [
|
const allCases = [
|
||||||
...TECH_LOG_CANONICAL_ROUTES.flatMap(({ routeId, path }) =>
|
...TECH_LOG_CANONICAL_ROUTES.flatMap(({ routeId, path }) =>
|
||||||
[360, 1440].map((width) => ({ name: `${routeId}-${width}`, path, width }))),
|
[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) => ({
|
...TECH_LOG_BREAKPOINT_WIDTHS.map((width) => ({
|
||||||
name: `TECH_LOG_HOME-BREAKPOINT-${width}`,
|
name: `TECH_LOG_HOME-BREAKPOINT-${width}`,
|
||||||
path: "/",
|
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.setViewportSize({ width: parityCase.width, height: 1000 });
|
||||||
await page.clock.setFixedTime(fixedDate);
|
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.locator("body").waitFor({ state: "visible" });
|
||||||
await page.evaluate(async () => document.fonts.ready);
|
await page.evaluate(async () => document.fonts.ready);
|
||||||
await page.addStyleTag({ content: noMotionCss });
|
await page.addStyleTag({ content: noMotionCss });
|
||||||
await interact(page, parityCase.action);
|
await interact(page, parityCase.action);
|
||||||
await page.evaluate(async () => document.fonts.ready);
|
await page.evaluate(async () => document.fonts.ready);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function projection(page) {
|
async function productSubtree(page) {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const normalizeReference = (value) => value
|
const normalize = (value) => value
|
||||||
.split(" ")
|
.replace(/_[rR][^\s"']*?(?=-|\s|$)/g, "<generated-id>")
|
||||||
.map((token) => token.replace(/^_[rR].*?_(?=-|$)/, "<generated-id>"))
|
.replace(/_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)/g, "_$1_<generated-module>_$2");
|
||||||
.join(" ");
|
const frameworkGeneratedAttributes = new Set([
|
||||||
const attributes = (element) => Object.fromEntries(
|
"data-discover", // React Router link discovery metadata.
|
||||||
[...element.attributes]
|
"data-nimg", // Next/Image metadata derived from source props.
|
||||||
.filter(({ name }) => name === "role" || name.startsWith("aria-"))
|
"decoding", // Next/Image output derived from priority=false.
|
||||||
.map(({ name, value }) => [name, normalizeReference(value)])
|
"srcset", // Next/Image widths derived from one source SVG.
|
||||||
.sort(([left], [right]) => left.localeCompare(right)),
|
"selected", // Next SSR output for a controlled <select> value.
|
||||||
);
|
]);
|
||||||
return [...document.querySelectorAll("header, main, footer, dialog")].map((element) => ({
|
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(),
|
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(" "),
|
classes: [...element.classList].map(normalize),
|
||||||
text: element.textContent?.replace(/\s+/g, " ").trim() ?? "",
|
attributes: [...element.attributes]
|
||||||
aria: attributes(element),
|
.filter(({ name }) =>
|
||||||
descendants: [...element.querySelectorAll("[role], [aria-label], [aria-labelledby], [aria-describedby], [aria-current], [aria-expanded], [aria-controls]")]
|
name !== "class" && !frameworkGeneratedAttributes.has(name))
|
||||||
.map((child) => ({
|
.map(({ name, value }) => [name, normalize(value)])
|
||||||
tag: child.tagName.toLowerCase(),
|
.sort(([left], [right]) => left.localeCompare(right)),
|
||||||
className: [...child.classList].map((name) => name.replace(/^_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)$/, "_$1_<generated-module>_$2")).sort().join(" "),
|
children: [...element.childNodes].map(serialize).filter(Boolean),
|
||||||
text: child.textContent?.replace(/\s+/g, " ").trim() ?? "",
|
};
|
||||||
aria: attributes(child),
|
};
|
||||||
})),
|
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) {
|
async function layoutProjection(page) {
|
||||||
return page.evaluate(() => [...document.querySelectorAll("header, main, main *, footer, dialog, .studio-app *")].map((element) => {
|
return page.evaluate(() => [...document.querySelectorAll("header, main, main *, footer, dialog, .studio-app *")].map((element) => {
|
||||||
const box = element.getBoundingClientRect();
|
const box = element.getBoundingClientRect();
|
||||||
@@ -194,6 +281,44 @@ function pixelDifference(sourceBuffer, targetBuffer) {
|
|||||||
return { pixels, sourceSize: [source.width, source.height], targetSize: [target.width, target.height] };
|
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 browser = await chromium.launch();
|
||||||
const contextOptions = {
|
const contextOptions = {
|
||||||
colorScheme: "light",
|
colorScheme: "light",
|
||||||
@@ -203,40 +328,40 @@ const contextOptions = {
|
|||||||
serviceWorkers: "block",
|
serviceWorkers: "block",
|
||||||
timezoneId: "Asia/Seoul",
|
timezoneId: "Asia/Seoul",
|
||||||
};
|
};
|
||||||
const sourceContext = await browser.newContext(contextOptions);
|
|
||||||
const targetContext = await browser.newContext(contextOptions);
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const parityCase of cases) {
|
for (const parityCase of cases) {
|
||||||
|
const sourceContext = await browser.newContext(contextOptions);
|
||||||
|
const targetContext = await browser.newContext(contextOptions);
|
||||||
const sourcePage = await sourceContext.newPage();
|
const sourcePage = await sourceContext.newPage();
|
||||||
const targetPage = await targetContext.newPage();
|
const targetPage = await targetContext.newPage();
|
||||||
const sourceErrors = [];
|
const sourceErrors = [];
|
||||||
const targetErrors = [];
|
const targetErrors = [];
|
||||||
for (const [page, errors] of [[sourcePage, sourceErrors], [targetPage, targetErrors]]) {
|
const sourceBootRequests = observePage(sourcePage, sourceErrors);
|
||||||
page.on("console", (message) => { if (message.type() === "error") errors.push(`console: ${message.text()}`); });
|
const targetBootRequests = observePage(targetPage, targetErrors);
|
||||||
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
|
try {
|
||||||
page.on("requestfailed", (request) => errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText ?? ""}`));
|
const [sourceResponse, targetResponse] = await Promise.all([
|
||||||
}
|
settle(sourcePage, sourceBaseUrl, parityCase, false),
|
||||||
await Promise.all([
|
settle(targetPage, targetBaseUrl, parityCase, true),
|
||||||
settle(sourcePage, sourceBaseUrl, parityCase),
|
|
||||||
settle(targetPage, targetBaseUrl, parityCase),
|
|
||||||
]);
|
]);
|
||||||
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 }),
|
sourcePage.screenshot({ fullPage: true }),
|
||||||
targetPage.screenshot({ fullPage: true }),
|
targetPage.screenshot({ fullPage: true }),
|
||||||
projection(sourcePage),
|
productSubtree(sourcePage),
|
||||||
projection(targetPage),
|
productSubtree(targetPage),
|
||||||
layoutProjection(sourcePage),
|
layoutProjection(sourcePage),
|
||||||
layoutProjection(targetPage),
|
layoutProjection(targetPage),
|
||||||
sourcePage.evaluate(() => [...document.fonts].map(({ family, status }) => ({ family, status })).filter(({ family }) => /Pretendard|IBM Plex Mono/.test(family))),
|
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))),
|
targetPage.evaluate(() => [...document.fonts].map(({ family, status }) => ({ family, status })).filter(({ family }) => /Pretendard|IBM Plex Mono/.test(family))),
|
||||||
]);
|
]);
|
||||||
const pixel = pixelDifference(sourceShot, targetShot);
|
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 fontsEqual = JSON.stringify(sourceFonts) === JSON.stringify(targetFonts);
|
||||||
const isPlainNotFound =
|
const responseEqual = responseContractEqual(sourceResponse, targetResponse);
|
||||||
parityCase.name.includes("NOT_FOUND") || parityCase.name.includes("UNKNOWN");
|
const isPlainNotFound = sourceResponse.mimeType === "text/plain";
|
||||||
const expectsSourceNotFoundResponse =
|
const expectsSourceNotFoundResponse =
|
||||||
isPlainNotFound || parityCase.path.includes("unknown");
|
isPlainNotFound || parityCase.path.includes("unknown");
|
||||||
const expectedNotFoundConsole = expectsSourceNotFoundResponse
|
const expectedNotFoundConsole = expectsSourceNotFoundResponse
|
||||||
@@ -245,36 +370,81 @@ try {
|
|||||||
const unexplainedSourceErrors = sourceErrors.filter((message) => !expectedNotFoundConsole(message));
|
const unexplainedSourceErrors = sourceErrors.filter((message) => !expectedNotFoundConsole(message));
|
||||||
const unexplainedTargetErrors = targetErrors.filter((message) => !expectedNotFoundConsole(message));
|
const unexplainedTargetErrors = targetErrors.filter((message) => !expectedNotFoundConsole(message));
|
||||||
const fontContractApplies = !isPlainNotFound;
|
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) {
|
if (!passed) {
|
||||||
await mkdir(diagnosticDirectory, { recursive: true });
|
await mkdir(diagnosticDirectory, { recursive: true });
|
||||||
const stem = parityCase.name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
const stem = parityCase.name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
writeFile(resolve(diagnosticDirectory, `${stem}-source.png`), sourceShot),
|
writeFile(resolve(diagnosticDirectory, `${stem}-source.png`), sourceShot),
|
||||||
writeFile(resolve(diagnosticDirectory, `${stem}-target.png`), targetShot),
|
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 });
|
results.push({
|
||||||
console.log(`${passed ? "PASS" : "FAIL"} ${parityCase.name} pixels=${pixel.pixels ?? "SIZE"} dom=${domEqual} fonts=${fontContractApplies ? fontsEqual : "source-plain-text"} errors=${unexplainedSourceErrors.length}/${unexplainedTargetErrors.length}`);
|
name: parityCase.name,
|
||||||
await Promise.all([sourcePage.close(), targetPage.close()]);
|
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 {
|
} finally {
|
||||||
await Promise.all([sourceContext.close(), targetContext.close()]);
|
|
||||||
await browser.close();
|
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(),
|
generatedAt: new Date().toISOString(),
|
||||||
sourceBaseUrl,
|
schemaVersion: 1,
|
||||||
targetBaseUrl,
|
source: {
|
||||||
conditions: { ...contextOptions, fixedTime: TECH_LOG_FIXED_TIME, viewportHeight: 1000, screenshots: "fullPage", masks: 0 },
|
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,
|
total: results.length,
|
||||||
passed: results.filter((result) => result.passed).length,
|
passed: results.filter((result) => result.passed).length,
|
||||||
failed: results.filter((result) => !result.passed).length,
|
failed: results.filter((result) => !result.passed).length,
|
||||||
totalDifferentPixels: results.reduce((sum, result) => sum + (result.pixel.pixels ?? 0), 0),
|
totalDifferentPixels: results.reduce((sum, result) => sum + (result.pixel.pixels ?? 0), 0),
|
||||||
results,
|
results,
|
||||||
};
|
};
|
||||||
|
const evidence = {
|
||||||
|
...evidenceWithoutDigest,
|
||||||
|
evidenceSha256: sha256(JSON.stringify(evidenceWithoutDigest)),
|
||||||
|
};
|
||||||
await mkdir(resolve(outputPath, ".."), { recursive: true });
|
await mkdir(resolve(outputPath, ".."), { recursive: true });
|
||||||
await writeFile(outputPath, `${JSON.stringify(evidence, null, 2)}\n`);
|
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 }));
|
console.log(JSON.stringify({ outputPath, total: evidence.total, passed: evidence.passed, failed: evidence.failed, totalDifferentPixels: evidence.totalDifferentPixels }));
|
||||||
|
|||||||
@@ -192,14 +192,35 @@ describe("CI gate contract", () => {
|
|||||||
expect(contract.commands).toHaveLength(81);
|
expect(contract.commands).toHaveLength(81);
|
||||||
expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(93);
|
expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(93);
|
||||||
expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23);
|
expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23);
|
||||||
expect(contract.gates.reduce((total, gate) => total + gate.evidenceArtifactIds.length, 0)).toBe(85);
|
expect(contract.gates.reduce((total, gate) => total + gate.evidenceArtifactIds.length, 0)).toBe(106);
|
||||||
expect(contract.artifacts).toHaveLength(105);
|
expect(contract.artifacts).toHaveLength(126);
|
||||||
expect(contract.stages).toHaveLength(5);
|
expect(contract.stages).toHaveLength(5);
|
||||||
expect(contract.retention.classes).toHaveLength(5);
|
expect(contract.retention.classes).toHaveLength(5);
|
||||||
expect(index.gates.get("FE-GATE-015")?.commandIds).toHaveLength(2);
|
expect(index.gates.get("FE-GATE-015")?.commandIds).toHaveLength(2);
|
||||||
expect(index.gates.get("FE-GATE-020")?.name).toBe("removability");
|
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", () => {
|
it("keeps the action registry recursively immutable and resolves only known actions", () => {
|
||||||
expect(Object.isFrozen(CI_ACTION_REGISTRY)).toBe(true);
|
expect(Object.isFrozen(CI_ACTION_REGISTRY)).toBe(true);
|
||||||
expect(Object.values(CI_ACTION_REGISTRY).every((action) => Object.isFrozen(action))).toBe(true);
|
expect(Object.values(CI_ACTION_REGISTRY).every((action) => Object.isFrozen(action))).toBe(true);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
|
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
|
||||||
|
|
||||||
const reviewed = `Status: reviewed
|
const reviewed = `Status: reviewed
|
||||||
Route ID: APP_HOME
|
Route ID: TECH_LOG_HOME
|
||||||
Release ID: release-1
|
Release ID: release-1
|
||||||
Reviewer: reviewer@example.test
|
Reviewer: reviewer@example.test
|
||||||
Reviewed at: 2026-07-25T12:00:00Z
|
Reviewed at: 2026-07-25T12:00:00Z
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ describe("selective Task 3 contract closure", () => {
|
|||||||
expect(canonical.gates).toHaveLength(26);
|
expect(canonical.gates).toHaveLength(26);
|
||||||
expect(canonical.commands).toHaveLength(81);
|
expect(canonical.commands).toHaveLength(81);
|
||||||
expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(93);
|
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.stages).toHaveLength(5);
|
||||||
expect(canonical.retention.classes).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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 28 KiB |
@@ -3,10 +3,15 @@ import {
|
|||||||
gotoTechLog,
|
gotoTechLog,
|
||||||
TECH_LOG_BREAKPOINT_WIDTHS,
|
TECH_LOG_BREAKPOINT_WIDTHS,
|
||||||
TECH_LOG_CANONICAL_ROUTES,
|
TECH_LOG_CANONICAL_ROUTES,
|
||||||
|
TECH_LOG_PUBLIC_FIXTURE_PATHS,
|
||||||
TECH_LOG_STUDIO_STATE_PATHS,
|
TECH_LOG_STUDIO_STATE_PATHS,
|
||||||
TECH_LOG_UNKNOWN_PUBLIC_PATHS,
|
TECH_LOG_UNKNOWN_PUBLIC_PATHS,
|
||||||
} from "../support/browser/tech-log-fixtures.ts";
|
} 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 route of TECH_LOG_CANONICAL_ROUTES) {
|
||||||
for (const width of [360, 1440] as const) {
|
for (const width of [360, 1440] as const) {
|
||||||
test(`${route.routeId} matches the reviewed ${width}px visual`, async ({ page }) => {
|
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) {
|
for (const width of TECH_LOG_BREAKPOINT_WIDTHS) {
|
||||||
test(`Public home matches the reviewed ${width}px breakpoint visual`, async ({ page }) => {
|
test(`Public home matches the reviewed ${width}px breakpoint visual`, async ({ page }) => {
|
||||||
await page.setViewportSize({ width, height: 1000 });
|
await page.setViewportSize({ width, height: 1000 });
|
||||||
|
|||||||