Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
236909be64 | ||
|
|
68d9efbda3 |
@@ -53,9 +53,9 @@ jobs:
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
- name: Install Chromium
|
||||
- name: Install Playwright browsers
|
||||
if: ${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||
- name: Upload gate evidence
|
||||
@@ -91,9 +91,9 @@ jobs:
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
- name: Install Chromium
|
||||
- name: Install Playwright browsers
|
||||
if: ${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||
- name: Upload gate evidence
|
||||
|
||||
@@ -16,6 +16,27 @@ corepack pnpm dev
|
||||
Runtime-public settings live in `public/config.json` and are validated before
|
||||
the product tree mounts. Client secrets are forbidden.
|
||||
|
||||
## Included starter experience
|
||||
|
||||
The default build mounts a domain-neutral application shell with a header,
|
||||
responsive sidebar, route focus management, session integration status, and a
|
||||
persistent `system` / `light` / `dark` theme selector.
|
||||
|
||||
| Route | Purpose |
|
||||
| --- | --- |
|
||||
| `/` | implementation readiness and starter links |
|
||||
| `/examples/ui` | buttons, fields, cards, alerts, badges, modal, and tokens |
|
||||
| `/examples/states` | loading, refresh, empty, error, auth, forbidden, and not-found states |
|
||||
| `/examples/auth` | reactive external-auth integration seam |
|
||||
| `/sample/resources` | protected, domain-neutral integration route |
|
||||
|
||||
`AUTH_MODE=demo` is credential-free and accepted only in local/development
|
||||
environments. Deployments use `AUTH_MODE=external` and provide the opaque auth
|
||||
owner described in
|
||||
[`docs/architecture/starter-experience.md`](docs/architecture/starter-experience.md).
|
||||
The client route policy is user experience only; server authorization remains
|
||||
authoritative.
|
||||
|
||||
## Architecture
|
||||
|
||||
Dependencies point inward:
|
||||
@@ -27,9 +48,11 @@ bootstrap composes concrete adapters
|
||||
contracts own cross-cutting registries
|
||||
```
|
||||
|
||||
See `docs/architecture/overview.md` and `docs/architecture/layers.md`. The
|
||||
removable sample slice is under `src/sample/contract-fixture`; product code is
|
||||
not allowed to import it.
|
||||
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||
`docs/architecture/starter-experience.md`. The removable sample slice is under
|
||||
`src/sample/contract-fixture`; product code is not allowed to import it. The
|
||||
visible starter routes do not depend on that fixture and continue to build
|
||||
after it is removed.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -52,9 +75,17 @@ corepack pnpm drill:runbooks
|
||||
corepack pnpm check:ci
|
||||
```
|
||||
|
||||
Install the pinned Playwright browser engines before the first cross-browser
|
||||
run:
|
||||
|
||||
```bash
|
||||
corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
```
|
||||
|
||||
Two gates intentionally need external evidence:
|
||||
|
||||
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review.
|
||||
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
|
||||
for all six registered routes.
|
||||
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
||||
eligible-sample threshold and 28 days of production data exist.
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,18 @@
|
||||
# EXAMPLES_UI accessibility review
|
||||
|
||||
Status: pending-manual-review
|
||||
Route ID: EXAMPLES_UI
|
||||
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: Review the text-field error association and modal focus containment/restoration.
|
||||
@@ -125,6 +125,9 @@
|
||||
"evidence": [
|
||||
"artifacts/tests/a11y.json",
|
||||
"artifacts/tests/a11y-manual/APP_HOME.md",
|
||||
"artifacts/tests/a11y-manual/EXAMPLES_UI.md",
|
||||
"artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
|
||||
"artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
|
||||
"artifacts/tests/a11y-manual/SAMPLE_RESOURCE_LIST.md",
|
||||
"artifacts/tests/a11y-manual/NOT_FOUND.md",
|
||||
"artifacts/tests/a11y-manual/report.json"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Manual accessibility review checklist
|
||||
|
||||
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
||||
review all three route records in `artifacts/tests/a11y-manual/` against one
|
||||
release candidate and sign them. Copy the template fields exactly; the gate
|
||||
rejects blank identity/timestamp/signature fields, pending verdicts, mismatched
|
||||
release IDs, or missing routes.
|
||||
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`,
|
||||
`SAMPLE_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.
|
||||
|
||||
Allowed item verdicts:
|
||||
|
||||
@@ -43,5 +45,7 @@ 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
|
||||
|
||||
Passing automated evidence means only that tested pages had no critical or
|
||||
serious axe findings under the recorded browser run.
|
||||
`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
|
||||
only that tested pages had no critical or serious axe findings under the
|
||||
recorded Chromium, Firefox, and WebKit runs.
|
||||
|
||||
@@ -8,6 +8,8 @@ in `review-ledger.json`, not automatically to edits in this file.
|
||||
flowchart LR
|
||||
Bootstrap[bootstrap / composition root] --> Presentation[presentation]
|
||||
Bootstrap --> Adapters[adapters]
|
||||
Presentation --> Shell[app shell and route surfaces]
|
||||
Shell --> Providers[session and theme providers]
|
||||
Presentation --> Application[application]
|
||||
Adapters --> Application
|
||||
Application --> Domain[domain]
|
||||
@@ -21,3 +23,9 @@ implement application ports, and only the composition root selects concrete
|
||||
adapters. Contract registries are the single named source for routes, API
|
||||
operations, environment values, storage keys, errors, queries, telemetry, and
|
||||
release tokens.
|
||||
|
||||
The executable route tree is mounted only after runtime configuration and
|
||||
release-manifest coherence pass. It receives the composed query client,
|
||||
credential-opaque session port, storage port, telemetry port, and immutable
|
||||
build ID. Visible starter pages depend on those ports and contracts, never on a
|
||||
concrete adapter or the removable sample fixture.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Starter experience contract
|
||||
|
||||
The repository provides a runnable, domain-neutral application rather than
|
||||
only infrastructure contracts. The starter experience is intentionally
|
||||
replaceable at the page level while the shell, providers, boundaries,
|
||||
primitives, and state surfaces remain reusable.
|
||||
|
||||
## Runtime composition
|
||||
|
||||
```text
|
||||
validated config + coherent release manifest
|
||||
-> concrete adapters
|
||||
-> QueryClientProvider
|
||||
-> ThemeProvider
|
||||
-> SessionProvider
|
||||
-> BrowserRouter
|
||||
-> AppShell
|
||||
-> lazy route boundary
|
||||
```
|
||||
|
||||
Boot stops before product mount when configuration or release coherence fails.
|
||||
Each lazy page renders inside Suspense and a telemetry-aware route boundary.
|
||||
Route changes move focus to the new page heading; the skip link and landmarks
|
||||
remain stable in the shell.
|
||||
|
||||
## Registered routes
|
||||
|
||||
| ID | Path | Access | Surface |
|
||||
| --- | --- | --- | --- |
|
||||
| `APP_HOME` | `/` | public | readiness dashboard |
|
||||
| `EXAMPLES_UI` | `/examples/ui` | public | interactive primitives and tokens |
|
||||
| `EXAMPLES_STATES` | `/examples/states` | public | async and access state matrix |
|
||||
| `EXAMPLES_AUTH` | `/examples/auth` | public | session integration controls |
|
||||
| `SAMPLE_RESOURCE_LIST` | `/sample/resources` | integration-defined | protected integration seam |
|
||||
| `NOT_FOUND` | `*` | public | safe navigation recovery |
|
||||
|
||||
Navigation labels and order come from `ROUTE_REGISTRY`; the sidebar does not
|
||||
maintain a second route list. The client access decision never claims to be
|
||||
authorization.
|
||||
|
||||
## Authentication seam
|
||||
|
||||
The session port exposes state subscription, sign-in start, sign-out,
|
||||
credential attachment, recovery, and unauthenticated notification. Credentials
|
||||
remain opaque to the application and presentation layers.
|
||||
|
||||
- `demo`: local/development-only state transition with no credentials
|
||||
- `external`: delegates to `globalThis.__CA_FRONTEND_AUTH_OWNER__`
|
||||
- missing/invalid external owner: fails closed as `integration-failed`
|
||||
|
||||
An external owner implements `readState`, `subscribe`, `beginSignIn`,
|
||||
`signOut`, `attachCredential`, `recoverSession`, and
|
||||
`notifyUnauthenticated`. It owns token acquisition and storage.
|
||||
|
||||
## Extending the starter
|
||||
|
||||
1. Register the route path, access hint, title, chunk, loading surface, error
|
||||
surface, and optional navigation metadata in `src/contracts/routes.js`.
|
||||
2. Add a lazy page in `src/presentation/` and render it through `RouteSurface`.
|
||||
3. Use application ports or use cases; do not import concrete adapters.
|
||||
4. Use the semantic tokens, UI primitives, and state surfaces before adding a
|
||||
project-specific variant.
|
||||
5. Add component behavior, all-engine E2E, automated axe, and signed manual
|
||||
route evidence.
|
||||
6. Run `test:sample-removal` to prove the visible starter still builds without
|
||||
`src/sample/contract-fixture`.
|
||||
|
||||
Theme preference is the public `COLOR_SCHEME` storage contract. Authentication
|
||||
tokens and other secrets remain forbidden storage keys.
|
||||
@@ -30,6 +30,11 @@ registry: merge evidence through the PR decision, coherent release evidence
|
||||
through the next release promotion, drill evidence through the next production
|
||||
promotion, and field evidence through aggregation.
|
||||
|
||||
Browser-backed merge gates install and execute the pinned Chromium, Firefox,
|
||||
and WebKit engines. This makes route behavior, reflow, native dialog semantics,
|
||||
theme persistence, and automated accessibility a cross-engine contract rather
|
||||
than a Chromium-only smoke check.
|
||||
|
||||
Repository variables required by higher tiers:
|
||||
|
||||
- `HOSTING_BASE_URL` for live header verification
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
# Design-token styling contract
|
||||
|
||||
`src/presentation/styles/theme.css` is the styling SSOT. Components consume
|
||||
semantic color, spacing, typography, and radius tokens through static Tailwind
|
||||
classes.
|
||||
semantic color, spacing, typography, status, and radius tokens through static
|
||||
classes. `/examples/ui` is the executable token and primitive gallery.
|
||||
|
||||
## Theme contract
|
||||
|
||||
The supported public preference is the closed set `system`, `light`, and
|
||||
`dark`. `ThemeProvider` reads and writes `COLOR_SCHEME` through the application
|
||||
storage port; it does not access a raw storage key. `system` subscribes to
|
||||
`prefers-color-scheme` changes. Bootstrap applies the persisted preference
|
||||
before React paints.
|
||||
|
||||
Components use semantic tokens such as `--color-panel`, `--color-content`,
|
||||
`--color-border`, `--color-action`, and status surface/content/border triples.
|
||||
They must not hard-code a light-only panel or text color. Both light and dark
|
||||
surfaces are included in automated axe checks.
|
||||
|
||||
## Included primitives
|
||||
|
||||
- `Button`: primary, secondary, danger, and ghost intent
|
||||
- `TextField`: label, help text, required state, and associated validation error
|
||||
- `Card`: labelled surface with optional footer
|
||||
- `Alert` and `Badge`: non-color-only status feedback
|
||||
- `Dialog`: native modal semantics, Escape/backdrop close, and trigger focus
|
||||
restoration
|
||||
- async and access state surfaces: loading, refresh, empty, terminal error,
|
||||
auth required, forbidden, and not found
|
||||
|
||||
Arbitrary-value policy:
|
||||
|
||||
@@ -13,5 +37,5 @@ Arbitrary-value policy:
|
||||
- user-controlled or runtime-composed class strings are forbidden
|
||||
- class variants must be selected from a closed static map
|
||||
|
||||
The removable sample may demonstrate tokens, but product modules must not
|
||||
import from `src/sample/contract-fixture`.
|
||||
The removable sample may demonstrate tokens, but production starter modules do
|
||||
not import from `src/sample/contract-fixture`.
|
||||
|
||||
@@ -12,6 +12,13 @@ Each gate is blocking in its declared scope. Failures are not downgraded with
|
||||
| end-to-end | `pnpm test:e2e` | `artifacts/tests/e2e/` |
|
||||
| accessibility | `pnpm test:a11y` | `artifacts/tests/a11y.json` |
|
||||
|
||||
End-to-end and automated accessibility scenarios run on the pinned Chromium,
|
||||
Firefox, and WebKit engines. The responsive contract explicitly exercises
|
||||
320px reflow, a mobile navigation drawer, and a wide two-column gallery.
|
||||
Theme scenarios verify persistence, operating-system changes, and dark-surface
|
||||
axe results. Native modal focus and validation association are exercised in
|
||||
both component and browser tests.
|
||||
|
||||
A control is verified only when a positive fixture passes and its deliberately
|
||||
failing negative fixture is rejected. Generated evidence is retained by CI;
|
||||
the repository tracks only the evidence directory structure.
|
||||
|
||||
@@ -19,5 +19,7 @@ export default defineConfig({
|
||||
},
|
||||
projects: [
|
||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
||||
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const MANUAL_A11Y_ROUTE_IDS = Object.freeze([
|
||||
"APP_HOME",
|
||||
"EXAMPLES_UI",
|
||||
"EXAMPLES_STATES",
|
||||
"EXAMPLES_AUTH",
|
||||
"SAMPLE_RESOURCE_LIST",
|
||||
"NOT_FOUND",
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
|
||||
import { MANUAL_A11Y_ROUTE_IDS } from "./lib/manual-a11y-evidence.mjs";
|
||||
|
||||
await mkdir("artifacts/tests", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/tests/a11y.json",
|
||||
@@ -7,7 +9,7 @@ await writeFile(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
scope: ["APP_HOME", "SAMPLE_RESOURCE_LIST", "NOT_FOUND"],
|
||||
scope: MANUAL_A11Y_ROUTE_IDS,
|
||||
threshold: { critical: 0, serious: 0 },
|
||||
automatedStatus: "passed",
|
||||
manualReview: "see artifacts/tests/a11y-manual/report.json",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 320, height: 720 });
|
||||
await page.goto("/examples/ui");
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 1, name: "UI 구성요소" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "메뉴", exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const viewport = await page.evaluate(() => ({
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
}));
|
||||
expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth);
|
||||
});
|
||||
|
||||
test("keeps desktop navigation and two-column examples at wide viewports", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto("/examples/ui");
|
||||
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "메뉴", exact: true }),
|
||||
).toBeHidden();
|
||||
await expect(page.locator(".component-grid--two").first()).toHaveCSS(
|
||||
"grid-template-columns",
|
||||
/.+px .+px/,
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { validateManualA11yEvidence } from "../../scripts/lib/manual-a11y-evidence.mjs";
|
||||
import {
|
||||
MANUAL_A11Y_ROUTE_IDS,
|
||||
validateManualA11yEvidence,
|
||||
} from "../../scripts/lib/manual-a11y-evidence.mjs";
|
||||
import { ROUTE_REGISTRY } from "../../src/contracts/routes.js";
|
||||
|
||||
const reviewed = `Status: reviewed
|
||||
Route ID: APP_HOME
|
||||
@@ -20,6 +24,12 @@ Screen reader: pass
|
||||
Notes: no defects`;
|
||||
|
||||
describe("manual accessibility evidence", () => {
|
||||
it("requires evidence for every registered user-visible route", () => {
|
||||
expect([...MANUAL_A11Y_ROUTE_IDS].sort()).toEqual(
|
||||
Object.keys(ROUTE_REGISTRY).sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a complete signed human review record", () => {
|
||||
expect(validateManualA11yEvidence(reviewed)).toMatchObject({
|
||||
failures: [],
|
||||
|
||||
Reference in New Issue
Block a user