diff --git a/README.md b/README.md index 4ffaf73..8e9efcc 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,18 @@ load. The exact route, dependency, stylesheet, asset, and parity inventory is recorded in [`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md). +`corepack pnpm build` emits a self-contained `dist/server.mjs` production +boundary. `corepack pnpm preview --host 127.0.0.1 --port 4174` serves known +Public and Studio routes as SPA documents, preserves the in-shell Studio 404, +and returns source-exact raw `404 text/plain` responses for missing Public +content. With the read-only source temp-copy server on `4375`, run: + +```bash +TECH_LOG_SOURCE_URL=http://127.0.0.1:4375 \ +TECH_LOG_TARGET_URL=http://127.0.0.1:4174 \ +corepack pnpm verify:tech-log-source-parity +``` + ## Architecture Dependencies point inward: diff --git a/artifacts/tests/a11y-manual/APP_HOME.md b/artifacts/tests/a11y-manual/APP_HOME.md deleted file mode 100644 index 12a847a..0000000 --- a/artifacts/tests/a11y-manual/APP_HOME.md +++ /dev/null @@ -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. diff --git a/artifacts/tests/a11y-manual/EXAMPLES_AUTH.md b/artifacts/tests/a11y-manual/EXAMPLES_AUTH.md deleted file mode 100644 index 8c6efa2..0000000 --- a/artifacts/tests/a11y-manual/EXAMPLES_AUTH.md +++ /dev/null @@ -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. diff --git a/artifacts/tests/a11y-manual/EXAMPLES_PLATFORM.md b/artifacts/tests/a11y-manual/EXAMPLES_PLATFORM.md deleted file mode 100644 index d6280cb..0000000 --- a/artifacts/tests/a11y-manual/EXAMPLES_PLATFORM.md +++ /dev/null @@ -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. diff --git a/artifacts/tests/a11y-manual/EXAMPLES_STATES.md b/artifacts/tests/a11y-manual/EXAMPLES_STATES.md deleted file mode 100644 index b071073..0000000 --- a/artifacts/tests/a11y-manual/EXAMPLES_STATES.md +++ /dev/null @@ -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. diff --git a/artifacts/tests/a11y-manual/NOT_FOUND.md b/artifacts/tests/a11y-manual/NOT_FOUND.md index f69a783..0e6763b 100644 --- a/artifacts/tests/a11y-manual/NOT_FOUND.md +++ b/artifacts/tests/a11y-manual/NOT_FOUND.md @@ -10,9 +10,9 @@ Attestation: pending M1 Keyboard: pending M2 Visible focus: pending M3 Route focus: pending -M4 Modal focus: not-applicable (no modal on this route) -M5 Error association: not-applicable (no form error on this route) +M4 Modal focus: pending +M5 Error association: pending M6 Color signal: pending M7 Reduced motion: pending Screen reader: pending -Notes: Human review pending. +Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review. diff --git a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_STATUS.md b/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_STATUS.md deleted file mode 100644 index 93e37c4..0000000 --- a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_STATUS.md +++ /dev/null @@ -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. diff --git a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_FORM.md b/artifacts/tests/a11y-manual/TECH_LOG_CASE.md similarity index 62% rename from artifacts/tests/a11y-manual/REFERENCE_RESOURCE_FORM.md rename to artifacts/tests/a11y-manual/TECH_LOG_CASE.md index e78d185..73118fe 100644 --- a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_FORM.md +++ b/artifacts/tests/a11y-manual/TECH_LOG_CASE.md @@ -1,7 +1,7 @@ -# REFERENCE_RESOURCE_FORM accessibility review +# TECH_LOG_CASE accessibility review Status: pending-manual-review -Route ID: REFERENCE_RESOURCE_FORM +Route ID: TECH_LOG_CASE Release ID: Reviewer: Reviewed at: @@ -15,4 +15,4 @@ M5 Error association: pending M6 Color signal: pending M7 Reduced motion: pending Screen reader: pending -Notes: Human review pending. +Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review. diff --git a/artifacts/tests/a11y-manual/EXAMPLES_UI.md b/artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md similarity index 55% rename from artifacts/tests/a11y-manual/EXAMPLES_UI.md rename to artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md index 6f89235..468995d 100644 --- a/artifacts/tests/a11y-manual/EXAMPLES_UI.md +++ b/artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md @@ -1,7 +1,7 @@ -# EXAMPLES_UI accessibility review +# TECH_LOG_EXPLORE accessibility review Status: pending-manual-review -Route ID: EXAMPLES_UI +Route ID: TECH_LOG_EXPLORE Release ID: Reviewer: Reviewed at: @@ -15,4 +15,4 @@ M5 Error association: pending M6 Color signal: pending M7 Reduced motion: pending Screen reader: pending -Notes: Review form primitives, Menu/Tabs keyboard behavior, Toast announcements, Tooltip supplemental copy, text-field error association and modal focus containment/restoration. +Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md b/artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md new file mode 100644 index 0000000..92b5b11 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md @@ -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. diff --git a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md b/artifacts/tests/a11y-manual/TECH_LOG_HOME.md similarity index 51% rename from artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md rename to artifacts/tests/a11y-manual/TECH_LOG_HOME.md index 74e9ae6..fdc45d6 100644 --- a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md +++ b/artifacts/tests/a11y-manual/TECH_LOG_HOME.md @@ -1,7 +1,7 @@ -# REFERENCE_RESOURCE_LIST accessibility review +# TECH_LOG_HOME accessibility review Status: pending-manual-review -Route ID: REFERENCE_RESOURCE_LIST +Route ID: TECH_LOG_HOME Release ID: Reviewer: Reviewed at: @@ -10,9 +10,9 @@ Attestation: pending M1 Keyboard: pending M2 Visible focus: pending M3 Route focus: pending -M4 Modal focus: not-applicable (no modal on this route) -M5 Error association: not-applicable (no form error on this route) +M4 Modal focus: pending +M5 Error association: pending M6 Color signal: pending M7 Reduced motion: pending Screen reader: pending -Notes: Human review pending. +Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review. diff --git a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_DETAIL.md b/artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md similarity index 50% rename from artifacts/tests/a11y-manual/REFERENCE_RESOURCE_DETAIL.md rename to artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md index 81ebaae..6407bea 100644 --- a/artifacts/tests/a11y-manual/REFERENCE_RESOURCE_DETAIL.md +++ b/artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md @@ -1,7 +1,7 @@ -# REFERENCE_RESOURCE_DETAIL accessibility review +# TECH_LOG_PROFILE accessibility review Status: pending-manual-review -Route ID: REFERENCE_RESOURCE_DETAIL +Route ID: TECH_LOG_PROFILE Release ID: Reviewer: Reviewed at: @@ -10,9 +10,9 @@ Attestation: pending M1 Keyboard: pending M2 Visible focus: pending M3 Route focus: pending -M4 Modal focus: not-applicable (no modal on this route) -M5 Error association: not-applicable (no form error on this route) +M4 Modal focus: pending +M5 Error association: pending M6 Color signal: pending M7 Reduced motion: pending Screen reader: pending -Notes: Human review pending. +Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md new file mode 100644 index 0000000..bb6475a --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md b/artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md new file mode 100644 index 0000000..1e8591f --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md new file mode 100644 index 0000000..94864cb --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md new file mode 100644 index 0000000..d5db6f8 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md new file mode 100644 index 0000000..608ef5c --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md b/artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md new file mode 100644 index 0000000..464a127 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md b/artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md new file mode 100644 index 0000000..43d2000 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md b/artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md new file mode 100644 index 0000000..6dc8008 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md b/artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md new file mode 100644 index 0000000..67678d2 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md b/artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md new file mode 100644 index 0000000..c6c30b9 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md new file mode 100644 index 0000000..d7faca6 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md new file mode 100644 index 0000000..65364ad --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md new file mode 100644 index 0000000..a189752 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md new file mode 100644 index 0000000..01fb47f --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md new file mode 100644 index 0000000..cc97f25 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md new file mode 100644 index 0000000..0c3f899 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md new file mode 100644 index 0000000..99eeeae --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md new file mode 100644 index 0000000..eac3f22 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md new file mode 100644 index 0000000..bb312a0 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md new file mode 100644 index 0000000..d6594b4 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md @@ -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. diff --git a/artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md b/artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md new file mode 100644 index 0000000..0c9f729 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md @@ -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. diff --git a/config/ci/gates.json b/config/ci/gates.json index 2a690b6..e12647c 100644 --- a/config/ci/gates.json +++ b/config/ci/gates.json @@ -1006,58 +1006,166 @@ ] }, { - "id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md", - "path": "artifacts/tests/a11y-manual/APP_HOME.md", + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_HOME.md", "schemaId": "markdown", - "production": "command-generated", - "producerCommandIds": [ - "review-a11y-manual" - ] + "production": "source-controlled" }, { - "id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md", - "path": "artifacts/tests/a11y-manual/EXAMPLES_UI.md", + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE.md", "schemaId": "markdown", - "production": "command-generated", - "producerCommandIds": [ - "review-a11y-manual" - ] + "production": "source-controlled" }, { - "id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md", - "path": "artifacts/tests/a11y-manual/EXAMPLES_STATES.md", + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_EXPLORE_KIND.md", "schemaId": "markdown", - "production": "command-generated", - "producerCommandIds": [ - "review-a11y-manual" - ] + "production": "source-controlled" }, { - "id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md", - "path": "artifacts/tests/a11y-manual/EXAMPLES_AUTH.md", + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_CASE.md", "schemaId": "markdown", - "production": "command-generated", - "producerCommandIds": [ - "review-a11y-manual" - ] + "production": "source-controlled" }, { - "id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md", - "path": "artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md", + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_REFERENCE.md", "schemaId": "markdown", - "production": "command-generated", - "producerCommandIds": [ - "review-a11y-manual" - ] + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_QUESTION.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_TOPIC.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECTS.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_RECORDS.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_DECISIONS.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_PROJECT_ACTIVITY.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASES.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_RELEASE.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_PROFILE.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_SEARCH.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_HOME.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENTS.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_NEW.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_EDIT.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_VALIDATION.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PREVIEW.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_DOCUMENT_PUBLISH.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATIONS.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_PUBLICATION_PREVIEW.md", + "schemaId": "markdown", + "production": "source-controlled" + }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md", + "schemaId": "markdown", + "production": "source-controlled" }, { "id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md", "path": "artifacts/tests/a11y-manual/NOT_FOUND.md", "schemaId": "markdown", - "production": "command-generated", - "producerCommandIds": [ - "review-a11y-manual" - ] + "production": "source-controlled" }, { "id": "artifact-artifacts-tests-a11y-manual-report-json", @@ -1757,11 +1865,32 @@ "logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-009-txt", "evidenceArtifactIds": [ "artifact-artifacts-tests-a11y-json", - "artifact-artifacts-tests-a11y-manual-APP-HOME-md", - "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md", - "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md", - "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md", - "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-EXPLORE-KIND-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-CASE-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-REFERENCE-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-QUESTION-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-TOPIC-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECTS-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-RECORDS-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-DECISIONS-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROJECT-ACTIVITY-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASES-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-RELEASE-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-PROFILE-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-SEARCH-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-HOME-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENTS-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-NEW-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-EDIT-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-VALIDATION-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PREVIEW-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-DOCUMENT-PUBLISH-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATIONS-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PUBLICATION-PREVIEW-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md", "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md", "artifact-artifacts-tests-a11y-manual-report-json" ], diff --git a/docs/accessibility/manual-checklist.md b/docs/accessibility/manual-checklist.md index 6bfac32..cf4f8fa 100644 --- a/docs/accessibility/manual-checklist.md +++ b/docs/accessibility/manual-checklist.md @@ -1,12 +1,11 @@ # Manual accessibility review checklist Automated axe checks do not establish WCAG conformance. A human reviewer must -review all six route records in `artifacts/tests/a11y-manual/` against one -release candidate and sign them. The required scope is derived from the route -registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`, -`REFERENCE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the -gate rejects blank identity/timestamp/signature fields, pending verdicts, -mismatched release IDs, or missing routes. +review all 27 route records in `artifacts/tests/a11y-manual/` against one +release candidate and sign them. The required TechLog Public and Studio scope is +derived from the installed route registry, so the gate rejects stale, missing, +or additional route records as well as blank identity/timestamp/signature +fields, pending verdicts, and mismatched release IDs. Allowed item verdicts: @@ -17,7 +16,7 @@ Required record: ```text Status: reviewed -Route ID: APP_HOME +Route ID: Release ID: Reviewer: Reviewed at: @@ -45,7 +44,8 @@ The reviewer must verify: - M7: non-essential motion is suppressed with reduced-motion preference - Screen reader: headings, live regions, errors, and actions are announced once -`EXAMPLES_UI` requires real M4 modal and M5 field-error review; those items must -not be marked not-applicable on that route. Passing automated evidence means +Routes with dialogs or form errors require real M4 modal-focus or M5 +error-association review; those items must not be marked not-applicable when the +reviewed route exposes the relevant behavior. Passing automated evidence means only that tested pages had no critical or serious axe findings under the -recorded Chromium, Firefox, and WebKit runs. +recorded browser runs. diff --git a/package.json b/package.json index 6b025de..2f4a131 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "dev": "vite", "build": "node scripts/build-frontend.ts", "build:release-candidate": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security && corepack pnpm verify:release && node scripts/verify-supply-chain-artifacts.ts && node scripts/create-release-candidate.ts", - "preview": "vite preview", + "preview": "node dist/server.mjs", + "verify:tech-log-source-parity": "node tests/support/browser/verify-tech-log-source-parity.ts", "lint": "eslint src scripts tests recipes .storybook vite.config.ts vitest.config.ts playwright*.config.ts --max-warnings=0", "check:architecture": "node scripts/check-architecture.ts", "check:design-system": "node scripts/check-design-system.ts", diff --git a/scripts/build-frontend.ts b/scripts/build-frontend.ts index 26657cc..d200ba5 100644 --- a/scripts/build-frontend.ts +++ b/scripts/build-frontend.ts @@ -15,7 +15,8 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim * 3. Vite app build (emptyOutDir = true) * 4. scan app dist and generate the static asset source * 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false) - * 6. generate Release Manifest V2 and the build manifest + * 6. generate the self-contained TechLog production serving boundary + * 7. generate Release Manifest V2 and the build manifest * * Steps 4 and 5 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES` * and `null`: those modes never run an active worker build. @@ -56,5 +57,8 @@ if (buildsActiveWorker) { ); } -// 6. release + build manifest +// 6. production serving boundary +run("node", ["scripts/generate-tech-log-serving-artifact.ts"]); + +// 7. release + build manifest run("node", ["scripts/generate-build-manifest.ts"]); diff --git a/scripts/contracts/ci-gates.ts b/scripts/contracts/ci-gates.ts index dff8946..ac5687b 100644 --- a/scripts/contracts/ci-gates.ts +++ b/scripts/contracts/ci-gates.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { z } from "zod"; import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts"; +import { MANUAL_A11Y_ROUTE_IDS } from "../lib/manual-a11y-evidence.ts"; import { RELEASE_CANDIDATE_EVIDENCE_PATHS, RELEASE_CANDIDATE_MANIFEST_PATH, @@ -442,7 +443,7 @@ export type LoadCiGateContractOptions = Readonly<{ }>; const CANONICAL_GATE_SHAPE_SHA256 = - "a4a963d0b9deffb7a0a3d755bbbcb979d72610eb74751c3a2e5eca55251e12d4"; + "492331de6d07926b1cdb569824c8feaa761eef76b9ac782c0404d40797383ef0"; function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string { const normalized = gates.map( @@ -481,8 +482,8 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] `command authority baseline must contain exactly 81 definitions and 93 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`, ); } - if (contract.artifacts.length !== 105) { - failures.push(`artifact authority baseline must contain exactly 105 artifacts; received ${contract.artifacts.length}`); + if (contract.artifacts.length !== 126) { + failures.push(`artifact authority baseline must contain exactly 126 artifacts; received ${contract.artifacts.length}`); } if (contract.stages.length !== 5) { failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`); @@ -723,6 +724,44 @@ function validateContractSemantics( issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`); } } + + const accessibilityGate = contract.gates.find( + ({ id: gateId }) => gateId === "FE-GATE-009", + ); + const manualAccessibilityArtifacts = MANUAL_A11Y_ROUTE_IDS.map((routeId) => ({ + id: `artifact-artifacts-tests-a11y-manual-${routeId.replaceAll("_", "-")}-md`, + path: `artifacts/tests/a11y-manual/${routeId}.md`, + })); + const expectedAccessibilityEvidenceArtifactIds = [ + "artifact-artifacts-tests-a11y-json", + ...manualAccessibilityArtifacts.map(({ id: artifactId }) => artifactId), + "artifact-artifacts-tests-a11y-manual-report-json", + ]; + if ( + !accessibilityGate || + JSON.stringify(accessibilityGate.evidenceArtifactIds) !== + JSON.stringify(expectedAccessibilityEvidenceArtifactIds) + ) { + issue( + "FE-GATE-009 manual accessibility evidence must exactly match the installed route scope", + ); + } + for (const expected of manualAccessibilityArtifacts) { + const artifact = contract.artifacts.find( + ({ id: artifactId }) => artifactId === expected.id, + ); + if ( + !artifact || + artifact.path !== expected.path || + artifact.schemaId !== "markdown" || + artifact.production !== "source-controlled" + ) { + issue( + `FE-GATE-009 manual accessibility artifact registration is invalid: ${expected.id}`, + ); + } + } + const referencedRetentionClasses = new Set( contract.gates.map(({ retentionClassId }) => retentionClassId), ); diff --git a/scripts/generate-tech-log-serving-artifact.ts b/scripts/generate-tech-log-serving-artifact.ts new file mode 100644 index 0000000..b28d02b --- /dev/null +++ b/scripts/generate-tech-log-serving-artifact.ts @@ -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 }); diff --git a/scripts/lib/tech-log-production-server.ts b/scripts/lib/tech-log-production-server.ts new file mode 100644 index 0000000..66de008 --- /dev/null +++ b/scripts/lib/tech-log-production-server.ts @@ -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(); +} diff --git a/scripts/lib/tech-log-serving-artifact.ts b/scripts/lib/tech-log-serving-artifact.ts new file mode 100644 index 0000000..9e2beb4 --- /dev/null +++ b/scripts/lib/tech-log-serving-artifact.ts @@ -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 { + 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), + ]); +} diff --git a/scripts/lib/tech-log-serving-contract.ts b/scripts/lib/tech-log-serving-contract.ts new file mode 100644 index 0000000..aa93419 --- /dev/null +++ b/scripts/lib/tech-log-serving-contract.ts @@ -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", + }), + }); +} diff --git a/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx b/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx index 4627d8e..8559ef8 100644 --- a/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx +++ b/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx @@ -1,34 +1,11 @@ -import { useSourceCompatiblePublicNotFound } from "../public-not-found-layout.tsx"; - export function PublicNotFoundPage() { - useSourceCompatiblePublicNotFound(); return ( - <> - -
Not Found
- +
+
+

404

+

페이지를 찾을 수 없습니다.

+

요청한 공개 페이지가 없습니다.

+
+
); } diff --git a/src/features/tech-log/presentation/public/public-not-found-layout.tsx b/src/features/tech-log/presentation/public/public-not-found-layout.tsx deleted file mode 100644 index c5dd3c1..0000000 --- a/src/features/tech-log/presentation/public/public-not-found-layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { - createContext, - useContext, - useLayoutEffect, -} from "react"; - -type SetNestedNotFound = (active: boolean) => void; - -export const PublicNotFoundLayoutContext = - createContext(null); - -export function useSourceCompatiblePublicNotFound(): void { - const setNestedNotFound = useContext(PublicNotFoundLayoutContext); - useLayoutEffect(() => { - setNestedNotFound?.(true); - return () => setNestedNotFound?.(false); - }, [setNestedNotFound]); -} diff --git a/src/features/tech-log/presentation/public/public-shell.tsx b/src/features/tech-log/presentation/public/public-shell.tsx index 9ce15a2..2a8d72e 100644 --- a/src/features/tech-log/presentation/public/public-shell.tsx +++ b/src/features/tech-log/presentation/public/public-shell.tsx @@ -1,9 +1,8 @@ -import { type ReactNode, useCallback, useState } from "react"; +import { type ReactNode } from "react"; import { Link } from "react-router-dom"; import { publicSiteConfig } from "../../contracts/public-site-config.ts"; import { SiteHeader } from "./components/site-header.tsx"; -import { PublicNotFoundLayoutContext } from "./public-not-found-layout.tsx"; type PublicShellProps = { children: ReactNode; @@ -11,37 +10,29 @@ type PublicShellProps = { }; export function PublicShell({ children, currentPath }: PublicShellProps) { - const [nestedNotFound, setNestedNotFound] = useState(false); - const updateNestedNotFound = useCallback((active: boolean) => { - setNestedNotFound(active); - }, []); return ( - - {nestedNotFound ? children : ( -
- - 본문으로 건너뛰기 - - +
+ + 본문으로 건너뛰기 + + - {children} + {children} -
-
-
-

{publicSiteConfig.operator}

-

{publicSiteConfig.identityStatement}

-
-
- - {publicSiteConfig.contactLabel} - - 최신 Release -
-
-
+
+
+
+

{publicSiteConfig.operator}

+

{publicSiteConfig.identityStatement}

+
+
+ + {publicSiteConfig.contactLabel} + + 최신 Release +
- )} - +
+
); } diff --git a/tests/component/router.test.tsx b/tests/component/router.test.tsx index bed450e..41cb125 100644 --- a/tests/component/router.test.tsx +++ b/tests/component/router.test.tsx @@ -343,12 +343,15 @@ describe("generic application router", () => { expect(window.location.pathname).toBe("/projects"); }); - it("renders the source-compatible plain TechLog not-found response", async () => { + it("keeps the unreachable client-side Public fallback inside its accessible shell", async () => { window.history.pushState({}, "", "/missing"); renderRouter(); - expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); - expect(screen.queryByRole("navigation", { name: "주요 탐색" })).not.toBeInTheDocument(); + expect( + await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }), + ).toBeVisible(); + expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); + expect(screen.queryByText("Not Found", { exact: true })).not.toBeInTheDocument(); }); it("keeps Studio routes inside the persistent Studio layout", async () => { diff --git a/tests/e2e/accessibility.spec.ts b/tests/e2e/accessibility.spec.ts index 1c9ca97..ebf8b80 100644 --- a/tests/e2e/accessibility.spec.ts +++ b/tests/e2e/accessibility.spec.ts @@ -6,7 +6,9 @@ import { TECH_LOG_CANONICAL_ROUTES, } from "../support/browser/tech-log-fixtures.ts"; -for (const route of TECH_LOG_CANONICAL_ROUTES) { +for (const route of TECH_LOG_CANONICAL_ROUTES.filter( + ({ routeId }) => routeId !== "NOT_FOUND", +)) { test(`@a11y ${route.routeId} has no critical or serious Axe violations`, async ({ page, }) => { @@ -22,6 +24,15 @@ for (const route of TECH_LOG_CANONICAL_ROUTES) { }); } +test("@a11y NOT_FOUND preserves the source raw non-HTML response", async ({ + request, +}) => { + const response = await request.get("/definitely-not-a-product-route"); + expect(response.status()).toBe(404); + expect(response.headers()["content-type"]).toBe("text/plain;charset=UTF-8"); + expect(await response.text()).toBe("Not Found"); +}); + test("@a11y keyboard reaches a visible Public navigation focus indicator", async ({ page, }) => { diff --git a/tests/e2e/tech-log-http-contract.spec.ts b/tests/e2e/tech-log-http-contract.spec.ts new file mode 100644 index 0000000..9563094 --- /dev/null +++ b/tests/e2e/tech-log-http-contract.spec.ts @@ -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("")).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("")).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, + ); + }); +} diff --git a/tests/features/tech-log/public-discovery-screens.test.tsx b/tests/features/tech-log/public-discovery-screens.test.tsx index ce0b544..14dee1c 100644 --- a/tests/features/tech-log/public-discovery-screens.test.tsx +++ b/tests/features/tech-log/public-discovery-screens.test.tsx @@ -372,18 +372,20 @@ describe("TechLog explore discovery", () => { ).toHaveAttribute("href", "/explore"); }); - it("renders an unknown kind as the source production plain 404", async () => { + it("keeps the unreachable unknown-kind client fallback accessible", async () => { const { router, container } = renderDiscoveryRoute( "TECH_LOG_EXPLORE_KIND", "/explore/unknown", ); expect(router.state.location.pathname).toBe("/explore/unknown"); - expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); + expect( + await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }), + ).toBeVisible(); expect( screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }), ).not.toBeInTheDocument(); - expect(container.querySelector(".site-frame")).toBeNull(); + expect(container.querySelector(".site-frame")).not.toBeNull(); }); it("recovers a rejecting registered not-found runtime under its own chunk contract", async () => { diff --git a/tests/features/tech-log/public-document-screens.test.tsx b/tests/features/tech-log/public-document-screens.test.tsx index c3883f7..c7d5add 100644 --- a/tests/features/tech-log/public-document-screens.test.tsx +++ b/tests/features/tech-log/public-document-screens.test.tsx @@ -337,12 +337,14 @@ describe("TechLog topics and Public not-found routing", () => { ["TECH_LOG_REFERENCE" as const, "/references/not-registered"], ["TECH_LOG_QUESTION" as const, "/questions/not-registered"], ["TECH_LOG_TOPIC" as const, "/topics/not-registered"], - ])("uses the source production plain 404 for %s", async (routeId, path) => { + ])("keeps the unreachable client fallback accessible for %s", async (routeId, path) => { const { container, router } = renderDocumentRoute(routeId, path); expect(router.state.location.pathname).toBe(path); - expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); + expect( + await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }), + ).toBeVisible(); expect(screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." })).not.toBeInTheDocument(); - expect(container.querySelector(".site-frame")).toBeNull(); + expect(container.querySelector(".site-frame")).not.toBeNull(); }); }); diff --git a/tests/features/tech-log/public-index-screens.test.tsx b/tests/features/tech-log/public-index-screens.test.tsx index 736e7c1..04dfc3b 100644 --- a/tests/features/tech-log/public-index-screens.test.tsx +++ b/tests/features/tech-log/public-index-screens.test.tsx @@ -360,25 +360,29 @@ describe("TechLog Public not-found runtime", () => { ["TECH_LOG_PROJECT_RECORDS" as const, "/projects/missing-project/records"], ["TECH_LOG_RELEASE" as const, "/releases/9.9.9"], ["TECH_LOG_CASE" as const, "/cases/not-registered"], - ])("renders the source-compatible plain fallback for %s", async (routeId, path) => { + ])("keeps the client-only fallback accessible for %s", async (routeId, path) => { const { container, router } = renderPublicRoute(routeId, path); expect(router.state.location.pathname).toBe(path); - expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); + expect( + await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }), + ).toBeVisible(); expect( screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }), ).not.toBeInTheDocument(); - expect(container.querySelector(".site-frame")).toBeNull(); + expect(container.querySelector(".site-frame")).not.toBeNull(); }); - it("renders the source-compatible plain fallback for the Public catch-all", () => { + it("keeps the client-only Public catch-all accessible", () => { const { container, router } = renderPublicRoute( "NOT_FOUND", "/definitely-not-a-product-route", ); expect(router.state.location.pathname).toBe("/definitely-not-a-product-route"); - expect(screen.getByText("Not Found", { exact: true })).toBeVisible(); - expect(container.querySelector(".site-frame")).toBeNull(); + expect( + screen.getByRole("heading", { name: "페이지를 찾을 수 없습니다." }), + ).toBeVisible(); + expect(container.querySelector(".site-frame")).not.toBeNull(); }); }); diff --git a/tests/support/browser/strict-browser-test.ts b/tests/support/browser/strict-browser-test.ts index 93e22a8..43e1cdb 100644 --- a/tests/support/browser/strict-browser-test.ts +++ b/tests/support/browser/strict-browser-test.ts @@ -15,6 +15,7 @@ function ignoredConsole(message: ConsoleMessage) { export const test = base.extend({ page: async ({ page }, use) => { const failures: string[] = []; + let mainDocumentStatus: number | null = null; const onConsole = (message: ConsoleMessage) => { if ( ["error", "warning"].includes(message.type()) && @@ -31,14 +32,33 @@ export const test = base.extend({ `requestfailed:${request.method()}:${new URL(request.url()).pathname}:${request.failure()?.errorText ?? "unknown"}`, ); }; + const onResponse = (response: import("@playwright/test").Response) => { + if ( + response.request().resourceType() === "document" && + response.frame() === page.mainFrame() + ) { + mainDocumentStatus = response.status(); + } + }; page.on("console", onConsole); page.on("pageerror", onPageError); page.on("requestfailed", onRequestFailed); + page.on("response", onResponse); await use(page); page.off("console", onConsole); page.off("pageerror", onPageError); page.off("requestfailed", onRequestFailed); - expect(failures, "unexpected browser console/page errors").toEqual([]); + page.off("response", onResponse); + const expectedDocumentNotFoundConsole = + "console:error:Failed to load resource: the server responded with a status of 404 (Not Found)"; + expect( + failures.filter( + (failure) => + failure !== expectedDocumentNotFoundConsole || + mainDocumentStatus !== 404, + ), + "unexpected browser console/page errors", + ).toEqual([]); }, }); diff --git a/tests/support/browser/tech-log-fixtures.ts b/tests/support/browser/tech-log-fixtures.ts index de19416..45fc049 100644 --- a/tests/support/browser/tech-log-fixtures.ts +++ b/tests/support/browser/tech-log-fixtures.ts @@ -127,8 +127,12 @@ export async function settleTechLogPage(page: Page) { export async function gotoTechLog(page: Page, path: string) { await prepareTechLogPage(page); - await page.goto(path); + const response = await page.goto(path, { waitUntil: "networkidle" }); await page.locator("body").waitFor({ state: "visible" }); + if (response?.headers()["content-type"]?.startsWith("text/html")) { + await page.locator("html[data-build-id][data-release-id]").waitFor(); + await page.locator(".site-frame, .studio-app").waitFor(); + } await settleTechLogPage(page); } diff --git a/tests/support/browser/verify-tech-log-source-parity.ts b/tests/support/browser/verify-tech-log-source-parity.ts index 95c15f4..c87ee0c 100644 --- a/tests/support/browser/verify-tech-log-source-parity.ts +++ b/tests/support/browser/verify-tech-log-source-parity.ts @@ -1,7 +1,9 @@ -// @ts-nocheck -- standalone evidence runner executed directly with tsx. +// @ts-nocheck -- standalone evidence runner executed directly by Node 24. +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; import { createRequire } from "node:module"; -import { mkdir, writeFile } from "node:fs/promises"; -import { resolve } from "node:path"; +import { lstat, mkdir, readFile, readdir, readlink, writeFile } from "node:fs/promises"; +import { relative, resolve } from "node:path"; import { chromium } from "@playwright/test"; @@ -9,6 +11,7 @@ import { TECH_LOG_BREAKPOINT_WIDTHS, TECH_LOG_CANONICAL_ROUTES, TECH_LOG_FIXED_TIME, + TECH_LOG_PUBLIC_FIXTURE_PATHS, TECH_LOG_STUDIO_STATE_PATHS, TECH_LOG_UNKNOWN_PUBLIC_PATHS, } from "./tech-log-fixtures.ts"; @@ -18,9 +21,10 @@ const { PNG } = require( resolve("node_modules/.pnpm/playwright-core@1.62.0/node_modules/playwright-core/lib/utilsBundle.js"), ); -const sourceBaseUrl = process.env.TECH_LOG_SOURCE_URL ?? "http://127.0.0.1:4175"; +const sourceBaseUrl = process.env.TECH_LOG_SOURCE_URL ?? "http://127.0.0.1:4375"; const targetBaseUrl = process.env.TECH_LOG_TARGET_URL ?? "http://127.0.0.1:4174"; -const outputPath = process.env.TECH_LOG_PARITY_OUTPUT ?? "artifacts/quality/tech-log-source-parity.json"; +const sourceRoot = process.env.TECH_LOG_SOURCE_ROOT ?? "/home/donghyeon/workspace/techlog-studio-frontend"; +const outputPath = process.env.TECH_LOG_PARITY_OUTPUT ?? "docs/operations/evidence/tech-log-source-parity.json"; const fixedDate = new Date(TECH_LOG_FIXED_TIME); const noMotionCss = ` *, *::before, *::after { @@ -35,6 +39,11 @@ const noMotionCss = ` const allCases = [ ...TECH_LOG_CANONICAL_ROUTES.flatMap(({ routeId, path }) => [360, 1440].map((width) => ({ name: `${routeId}-${width}`, path, width }))), + ...TECH_LOG_PUBLIC_FIXTURE_PATHS.map((path) => ({ + name: `TECH_LOG_PUBLIC_FIXTURE-${path.slice(1).replaceAll("/", "-").toUpperCase()}`, + path, + width: 1440, + })), ...TECH_LOG_BREAKPOINT_WIDTHS.map((width) => ({ name: `TECH_LOG_HOME-BREAKPOINT-${width}`, path: "/", @@ -117,45 +126,123 @@ async function interact(page, action) { } } -async function settle(page, baseUrl, parityCase) { +async function settle(page, baseUrl, parityCase, waitForTargetBootstrap) { + page.setDefaultNavigationTimeout(15_000); + page.setDefaultTimeout(15_000); await page.setViewportSize({ width: parityCase.width, height: 1000 }); await page.clock.setFixedTime(fixedDate); - await page.goto(new URL(parityCase.path, baseUrl).href, { waitUntil: "networkidle" }); + const navigationResponse = await page.goto( + new URL(parityCase.path, baseUrl).href, + { waitUntil: "networkidle" }, + ); + if (!navigationResponse) throw new Error(`Missing navigation response: ${parityCase.name}`); + const response = await responseMetadata(navigationResponse); + if (response.mimeType === "text/html" && waitForTargetBootstrap) { + // The target sets these only after config + release-manifest bodies have + // been fully read, validated, and composed. This is the product's boot + // completion signal, not a delay or retry. + await page.locator("html[data-build-id][data-release-id]").waitFor(); + } await page.locator("body").waitFor({ state: "visible" }); await page.evaluate(async () => document.fonts.ready); await page.addStyleTag({ content: noMotionCss }); await interact(page, parityCase.action); await page.evaluate(async () => document.fonts.ready); + return response; } -async function projection(page) { +async function productSubtree(page) { return page.evaluate(() => { - const normalizeReference = (value) => value - .split(" ") - .map((token) => token.replace(/^_[rR].*?_(?=-|$)/, "")) - .join(" "); - const attributes = (element) => Object.fromEntries( - [...element.attributes] - .filter(({ name }) => name === "role" || name.startsWith("aria-")) - .map(({ name, value }) => [name, normalizeReference(value)]) - .sort(([left], [right]) => left.localeCompare(right)), - ); - return [...document.querySelectorAll("header, main, footer, dialog")].map((element) => ({ - tag: element.tagName.toLowerCase(), - className: [...element.classList].map((name) => name.replace(/^_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)$/, "_$1__$2")).sort().join(" "), - text: element.textContent?.replace(/\s+/g, " ").trim() ?? "", - aria: attributes(element), - descendants: [...element.querySelectorAll("[role], [aria-label], [aria-labelledby], [aria-describedby], [aria-current], [aria-expanded], [aria-controls]")] - .map((child) => ({ - tag: child.tagName.toLowerCase(), - className: [...child.classList].map((name) => name.replace(/^_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)$/, "_$1__$2")).sort().join(" "), - text: child.textContent?.replace(/\s+/g, " ").trim() ?? "", - aria: attributes(child), - })), - })); + const normalize = (value) => value + .replace(/_[rR][^\s"']*?(?=-|\s|$)/g, "") + .replace(/_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)/g, "_$1__$2"); + const frameworkGeneratedAttributes = new Set([ + "data-discover", // React Router link discovery metadata. + "data-nimg", // Next/Image metadata derived from source props. + "decoding", // Next/Image output derived from priority=false. + "srcset", // Next/Image widths derived from one source SVG. + "selected", // Next SSR output for a controlled