From 2f166cb3244f4d5ef398e7b89ea61003eee136af Mon Sep 17 00:00:00 2001 From: victor Date: Mon, 13 Jul 2026 20:48:37 +0900 Subject: [PATCH 01/44] Add portable token artifact validation --- .github/CODEOWNERS | 5 + .github/workflows/validate.yml | 14 + .gitignore | 1 + GOVERNANCE.md | 16 + .../adapters/style-dictionary.config.mjs | 36 + .../token-portability/invalid-cycle.json | 7 + .../invalid-dangling-alias.json | 6 + .../token-portability/invalid-extends.json | 4 + .../invalid-json-pointer.json | 7 + .../token-portability/invalid-resolver.json | 9 + .../invalid-type-mismatch.json | 10 + .../invalid-unknown-reserved.json | 6 + .../invalid-untested-type.json | 6 + .../token-portability/valid-border.json | 14 + .../token-portability/valid-reference.json | 32 + .../token-portability/valid-whole-alias.json | 7 + consumer-reference/generated/manifest.json | 40 + consumer-reference/generated/tokens.css | 12 + .../schema/portable-tokens.schema.json | 104 ++ package-lock.json | 1570 +++++++++++++++++ package.json | 16 + quality/evidence/executable-evidence.md | 1 + scripts/build-reference-artifacts.mjs | 115 ++ scripts/reference-token-contract.mjs | 156 ++ scripts/test-reference-adapters.mjs | 159 ++ scripts/validate-reference-artifacts.mjs | 115 ++ 26 files changed, 2468 insertions(+) create mode 100644 .gitignore create mode 100644 consumer-reference/adapters/style-dictionary.config.mjs create mode 100644 consumer-reference/fixtures/token-portability/invalid-cycle.json create mode 100644 consumer-reference/fixtures/token-portability/invalid-dangling-alias.json create mode 100644 consumer-reference/fixtures/token-portability/invalid-extends.json create mode 100644 consumer-reference/fixtures/token-portability/invalid-json-pointer.json create mode 100644 consumer-reference/fixtures/token-portability/invalid-resolver.json create mode 100644 consumer-reference/fixtures/token-portability/invalid-type-mismatch.json create mode 100644 consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json create mode 100644 consumer-reference/fixtures/token-portability/invalid-untested-type.json create mode 100644 consumer-reference/fixtures/token-portability/valid-border.json create mode 100644 consumer-reference/fixtures/token-portability/valid-reference.json create mode 100644 consumer-reference/fixtures/token-portability/valid-whole-alias.json create mode 100644 consumer-reference/generated/manifest.json create mode 100644 consumer-reference/generated/tokens.css create mode 100644 consumer-reference/schema/portable-tokens.schema.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/build-reference-artifacts.mjs create mode 100644 scripts/reference-token-contract.mjs create mode 100644 scripts/test-reference-adapters.mjs create mode 100644 scripts/validate-reference-artifacts.mjs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 68bd308..e69aaad 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,8 @@ /design-engineering/ @changeroa /platform-guides/ @changeroa /consumer-reference/ @changeroa +/consumer-reference/adapters/ @changeroa +/consumer-reference/generated/ @changeroa /GUIDE.md @changeroa /guides/ @changeroa @@ -30,3 +32,6 @@ /scripts/ @changeroa /scripts/validate-*.mjs @changeroa /scripts/test-validate-*.mjs @changeroa +/scripts/build-reference-artifacts.mjs @changeroa +/scripts/validate-reference-artifacts.mjs @changeroa +/scripts/test-reference-adapters.mjs @changeroa diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ae0c9ff..7efb6ce 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -22,6 +22,9 @@ jobs: with: node-version: 22 + - name: Install pinned validation dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + - name: Syntax and whitespace run: | node -c scripts/generate-patterns.mjs @@ -44,11 +47,17 @@ jobs: node -c scripts/test-validate-webpage-workflow.mjs node -c scripts/validate-consumer-reference.mjs node -c scripts/test-validate-consumer-reference.mjs + node -c consumer-reference/adapters/style-dictionary.config.mjs + node -c scripts/reference-token-contract.mjs + node -c scripts/build-reference-artifacts.mjs + node -c scripts/validate-reference-artifacts.mjs + node -c scripts/test-reference-adapters.mjs git diff --check - name: Generated artifacts are current run: | node scripts/generate-patterns.mjs + node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json git diff --exit-code - name: OKF structure validation @@ -88,6 +97,11 @@ jobs: node scripts/validate-consumer-reference.mjs --json node scripts/test-validate-consumer-reference.mjs --json + - name: Portable token artifact integrity + run: | + node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json + node scripts/test-reference-adapters.mjs --json + - name: Webpage workflow check run: | node scripts/validate-webpage-workflow.mjs --json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 2d072bb..58c0a13 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -22,6 +22,7 @@ Use this file before editing repository documentation. It names which file is au | Layout recipes | `recipes/*.md` | Manual | None | `stable` | Pattern-stack changes, route changes, or broken recipe links. | `scripts/validate-okf.mjs`, `scripts/validate-links.mjs`, `scripts/validate-ia.mjs` | Recipe owner | | Quality gates and evidence | `quality/**/*.md` | Manual | None | `stable` | Claim-boundary changes, evidence-family changes, or broken quality links. | `scripts/validate-okf.mjs`, `scripts/validate-links.mjs`, `scripts/validate-ia.mjs` | Quality owner | | Consumer reference contract | `consumer-reference/contract.md`, `consumer-reference/schema/*.json` | Manual | None | `stable` contract with related fixtures | Handoff shape, path boundary, lifecycle, ownership, or dependency-direction changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Repository governance owner with Validation owner | +| Portable token source | `consumer-reference/fixtures/token-portability/valid-reference.json`, `consumer-reference/schema/portable-tokens.schema.json` | `scripts/build-reference-artifacts.mjs` with Style Dictionary `5.5.0` | `consumer-reference/generated/tokens.css`, `consumer-reference/generated/manifest.json` | `stable` restricted adapter contract, `generated` output | Allowed token shape, adapter/version pin, source token count, warning, declaration, or content hash changes. | `scripts/validate-reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | | Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner | | Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner | | Motion domain guidance | `motion/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Motion domain owner | @@ -48,6 +49,10 @@ Current generated artifacts: - `patterns/index.md` - `patterns/**/index.md` - `patterns/**/*.md` +- `consumer-reference/generated/tokens.css` +- `consumer-reference/generated/manifest.json` + +Portable token artifacts are regenerated only from the restricted fixture through the pinned adapter. Run `npm run build`; never broaden the allowed token subset to accommodate an adapter false-success, and revert the adapter with both generated files if the pin regresses. ## Lifecycle States @@ -81,6 +86,7 @@ Consumer-reference ownership records the current truth as `owner.enforcement: "p | `guides/**`, `GUIDE.md`, `recipes/**` | Planning-doc owner | Planning flow, task routes, recipe composition boundaries. | | `quality/**` | Quality owner | Claim boundaries, executable evidence, review gates. | | `consumer-reference/**` | Repository governance owner with Validation owner | Required handoff, repository-local record safety, lifecycle separation, ownership truth, and reverse-dependency guard. | +| `consumer-reference/adapters/**`, `consumer-reference/generated/**`, `scripts/*reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | Restricted token ingress, exact adapter pin, warning handling, token/declaration counts, hashes, preserved aliases, and generated drift. | | `DOMAINS.md`, `layout/**` | Repository governance owner with Pattern-data owner | Domain routing and preservation of the stable Layout path contract. | | `motion/**` | Motion domain owner | Motion terminology, review procedure, practice classification, and evidence boundaries. | | `design-engineering/**` | Design Engineering domain owner | Separation of product heuristics from shared quality gates. | @@ -121,3 +127,13 @@ git diff --exit-code -- CATALOG.md patterns node scripts/validate-patterns.mjs --min-count 46 --json node scripts/validate-catalog.mjs --json ``` + +For portable token source, adapter, or generated artifact changes, run: + +```sh +npm ci --ignore-scripts --no-audit --no-fund +node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json +node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json +node scripts/test-reference-adapters.mjs --json +git diff --exit-code -- consumer-reference/generated +``` diff --git a/consumer-reference/adapters/style-dictionary.config.mjs b/consumer-reference/adapters/style-dictionary.config.mjs new file mode 100644 index 0000000..d25e5ab --- /dev/null +++ b/consumer-reference/adapters/style-dictionary.config.mjs @@ -0,0 +1,36 @@ +import path from "node:path"; +import StyleDictionary from "style-dictionary"; + +export const adapter = Object.freeze({ name: "style-dictionary", version: "5.5.0" }); + +const durationTransform = "stylegallery/duration-css"; +if (!Object.hasOwn(StyleDictionary.hooks.transforms, durationTransform)) { + StyleDictionary.registerTransform({ + filter: (token) => token.$type === "duration", + name: durationTransform, + transform: (token) => `${token.$value.value}${token.$value.unit}`, + transitive: true, + type: "value", + }); +} + +export function createStyleDictionaryConfig(source, output) { + return { + log: { verbosity: "verbose", warnings: "warn" }, + platforms: { + css: { + buildPath: `${path.dirname(output)}${path.sep}`, + files: [ + { + destination: path.basename(output), + format: "css/variables", + options: { outputReferences: true }, + }, + ], + transforms: [...StyleDictionary.hooks.transformGroups.css, durationTransform], + }, + }, + source: [source], + usesDtcg: true, + }; +} diff --git a/consumer-reference/fixtures/token-portability/invalid-cycle.json b/consumer-reference/fixtures/token-portability/invalid-cycle.json new file mode 100644 index 0000000..a92c451 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-cycle.json @@ -0,0 +1,7 @@ +{ + "space": { + "$type": "dimension", + "first": { "$value": "{space.second}" }, + "second": { "$value": "{space.first}" } + } +} diff --git a/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json b/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json new file mode 100644 index 0000000..4b3ca97 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json @@ -0,0 +1,6 @@ +{ + "space": { + "$type": "dimension", + "large": { "$value": "{space.missing}" } + } +} diff --git a/consumer-reference/fixtures/token-portability/invalid-extends.json b/consumer-reference/fixtures/token-portability/invalid-extends.json new file mode 100644 index 0000000..103c5a8 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-extends.json @@ -0,0 +1,4 @@ +{ + "base": { "$type": "dimension", "small": { "$value": { "value": 4, "unit": "px" } } }, + "derived": { "$extends": "{base}", "large": { "$type": "dimension", "$value": { "value": 8, "unit": "px" } } } +} diff --git a/consumer-reference/fixtures/token-portability/invalid-json-pointer.json b/consumer-reference/fixtures/token-portability/invalid-json-pointer.json new file mode 100644 index 0000000..4fd7dca --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-json-pointer.json @@ -0,0 +1,7 @@ +{ + "color": { + "$type": "color", + "base": { "$value": { "colorSpace": "srgb", "components": [0, 0, 0] } }, + "linked": { "$ref": "#/color/base" } + } +} diff --git a/consumer-reference/fixtures/token-portability/invalid-resolver.json b/consumer-reference/fixtures/token-portability/invalid-resolver.json new file mode 100644 index 0000000..843ad17 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-resolver.json @@ -0,0 +1,9 @@ +{ + "themes": { + "dark": { "resolver": { "mode": "dark" } } + }, + "color": { + "$type": "color", + "base": { "$value": { "colorSpace": "srgb", "components": [0, 0, 0] }, "$extensions": { "modifier": "dark" } } + } +} diff --git a/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json b/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json new file mode 100644 index 0000000..73e7979 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json @@ -0,0 +1,10 @@ +{ + "color": { + "$type": "color", + "base": { "$value": { "colorSpace": "srgb", "components": [0, 0, 0] } } + }, + "space": { + "$type": "dimension", + "wrong": { "$value": "{color.base}" } + } +} diff --git a/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json b/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json new file mode 100644 index 0000000..3a82ca6 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json @@ -0,0 +1,6 @@ +{ + "size": { + "$type": "dimension", + "small": { "$value": { "value": 4, "unit": "px" }, "$future": true } + } +} diff --git a/consumer-reference/fixtures/token-portability/invalid-untested-type.json b/consumer-reference/fixtures/token-portability/invalid-untested-type.json new file mode 100644 index 0000000..e859220 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/invalid-untested-type.json @@ -0,0 +1,6 @@ +{ + "font": { + "$type": "fontFamily", + "body": { "$value": ["Inter", "sans-serif"] } + } +} diff --git a/consumer-reference/fixtures/token-portability/valid-border.json b/consumer-reference/fixtures/token-portability/valid-border.json new file mode 100644 index 0000000..01e1fab --- /dev/null +++ b/consumer-reference/fixtures/token-portability/valid-border.json @@ -0,0 +1,14 @@ +{ + "color": { + "$type": "color", + "line": { "$value": { "colorSpace": "srgb", "components": [0.2, 0.2, 0.2] } } + }, + "size": { + "$type": "dimension", + "line": { "$value": { "value": 1, "unit": "px" } } + }, + "border": { + "$type": "border", + "default": { "$value": { "color": "{color.line}", "style": "solid", "width": "{size.line}" } } + } +} diff --git a/consumer-reference/fixtures/token-portability/valid-reference.json b/consumer-reference/fixtures/token-portability/valid-reference.json new file mode 100644 index 0000000..837c7c3 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/valid-reference.json @@ -0,0 +1,32 @@ +{ + "color": { + "$type": "color", + "accent": { + "$value": { + "colorSpace": "srgb", + "components": [0.125, 0.375, 0.875], + "alpha": 1 + } + }, + "action": { "$value": "{color.accent}" } + }, + "duration": { + "$type": "duration", + "quick": { "$value": { "value": 120, "unit": "ms" } } + }, + "space": { + "$type": "dimension", + "small": { "$value": { "value": 8, "unit": "px" } }, + "control": { "$value": "{space.small}" } + }, + "border": { + "$type": "border", + "focus": { + "$value": { + "color": "{color.action}", + "style": "solid", + "width": "{space.control}" + } + } + } +} diff --git a/consumer-reference/fixtures/token-portability/valid-whole-alias.json b/consumer-reference/fixtures/token-portability/valid-whole-alias.json new file mode 100644 index 0000000..c4609e8 --- /dev/null +++ b/consumer-reference/fixtures/token-portability/valid-whole-alias.json @@ -0,0 +1,7 @@ +{ + "color": { + "$type": "color", + "base": { "$value": { "colorSpace": "srgb", "components": [0.1, 0.2, 0.3] } }, + "linked": { "$value": "{color.base}" } + } +} diff --git a/consumer-reference/generated/manifest.json b/consumer-reference/generated/manifest.json new file mode 100644 index 0000000..d75f19e --- /dev/null +++ b/consumer-reference/generated/manifest.json @@ -0,0 +1,40 @@ +{ + "adapter": { + "name": "style-dictionary", + "version": "5.5.0" + }, + "declarations": [ + { + "name": "--color-accent", + "value": "#2060df" + }, + { + "name": "--duration-quick", + "value": "120ms" + }, + { + "name": "--space-small", + "value": "8px" + }, + { + "name": "--color-action", + "value": "var(--color-accent)" + }, + { + "name": "--space-control", + "value": "var(--space-small)" + }, + { + "name": "--border-focus", + "value": "var(--space-control) solid var(--color-action)" + } + ], + "inputHash": "sha256:660f1b752eccd4045af6ed0ae37f17f1eb436aec95302bf161687d9fa5e79a5f", + "outputCount": 6, + "outputFile": "tokens.css", + "outputHash": "sha256:a479ba780046bb5c1c393766ee47292049ca0515210c495081ef8cb3853f52a4", + "schemaVersion": "1.0", + "sourceCount": 6, + "sourceFile": "../fixtures/token-portability/valid-reference.json", + "warnings": [] +} diff --git a/consumer-reference/generated/tokens.css b/consumer-reference/generated/tokens.css new file mode 100644 index 0000000..6dcc19b --- /dev/null +++ b/consumer-reference/generated/tokens.css @@ -0,0 +1,12 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +:root { + --color-accent: #2060df; + --duration-quick: 120ms; + --space-small: 8px; + --color-action: var(--color-accent); + --space-control: var(--space-small); + --border-focus: var(--space-control) solid var(--color-action); +} diff --git a/consumer-reference/schema/portable-tokens.schema.json b/consumer-reference/schema/portable-tokens.schema.json new file mode 100644 index 0000000..f74551a --- /dev/null +++ b/consumer-reference/schema/portable-tokens.schema.json @@ -0,0 +1,104 @@ +{ + "$id": "https://stylegallery.example/consumer-reference/schema/portable-tokens.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "alias": { + "pattern": "^\\{[a-z0-9]+(?:[.-][a-z0-9]+)*\\}$", + "type": "string" + }, + "border": { + "additionalProperties": false, + "properties": { + "color": { + "oneOf": [ + { "$ref": "#/$defs/alias" }, + { "$ref": "#/$defs/color" } + ] + }, + "style": { "const": "solid" }, + "width": { + "oneOf": [ + { "$ref": "#/$defs/alias" }, + { "$ref": "#/$defs/dimension" } + ] + } + }, + "required": ["color", "style", "width"], + "type": "object" + }, + "color": { + "additionalProperties": false, + "properties": { + "alpha": { "maximum": 1, "minimum": 0, "type": "number" }, + "colorSpace": { "const": "srgb" }, + "components": { + "items": { "maximum": 1, "minimum": 0, "type": "number" }, + "maxItems": 3, + "minItems": 3, + "type": "array" + } + }, + "required": ["colorSpace", "components"], + "type": "object" + }, + "dimension": { + "additionalProperties": false, + "properties": { + "unit": { "enum": ["px", "rem"] }, + "value": { "type": "number" } + }, + "required": ["unit", "value"], + "type": "object" + }, + "duration": { + "additionalProperties": false, + "properties": { + "unit": { "enum": ["ms", "s"] }, + "value": { "minimum": 0, "type": "number" } + }, + "required": ["unit", "value"], + "type": "object" + }, + "group": { + "additionalProperties": false, + "patternProperties": { + "^(?!\\$)(?!(?:__proto__|prototype|constructor)$)[a-z0-9]+(?:-[a-z0-9]+)*$": { "$ref": "#/$defs/node" } + }, + "properties": { + "$description": { "type": "string" }, + "$type": { "enum": ["border", "color", "dimension", "duration"] } + }, + "type": "object" + }, + "node": { + "oneOf": [ + { "$ref": "#/$defs/token" }, + { "$ref": "#/$defs/group" } + ] + }, + "token": { + "additionalProperties": false, + "properties": { + "$description": { "type": "string" }, + "$type": { "enum": ["border", "color", "dimension", "duration"] }, + "$value": { + "oneOf": [ + { "$ref": "#/$defs/alias" }, + { "$ref": "#/$defs/border" }, + { "$ref": "#/$defs/color" }, + { "$ref": "#/$defs/dimension" }, + { "$ref": "#/$defs/duration" } + ] + } + }, + "required": ["$value"], + "type": "object" + } + }, + "additionalProperties": false, + "patternProperties": { + "^(?!\\$)(?!(?:__proto__|prototype|constructor)$)[a-z0-9]+(?:-[a-z0-9]+)*$": { "$ref": "#/$defs/node" } + }, + "title": "StyleGallery portable design tokens", + "type": "object" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0eb15a1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1570 @@ +{ + "name": "stylegallery-validation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stylegallery-validation", + "dependencies": { + "style-dictionary": "5.5.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@bundled-es-modules/deepmerge": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/deepmerge/-/deepmerge-4.3.2.tgz", + "integrity": "sha512-q8doe7ndrY2IolUOFIn0A0++JBX38pMhN7kFhTF4cnjIcILf6X6H2yWczInyv8ZFdR0lrE8088X8XS5efxXz8A==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1" + } + }, + "node_modules/@bundled-es-modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-x9nR2e1pt8LF0yLPC6yz/aUoiN7qJJwZ1znLxIXCxGyH+8BI+yO/sklBdn1+QbUyWXQBM+CjfZz3IhqtgIoDVg==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "events": "^3.3.0", + "glob": "^13.0.6", + "path": "^0.12.7", + "stream": "^0.0.3", + "string_decoder": "^1.3.0", + "url": "^0.11.4" + } + }, + "node_modules/@bundled-es-modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-ykdrkEmQr9BV804yd37ikXfNnvxrwYfY9Z2/EtMHFEFadEjsQXJ1zL9bVZrKNLDtm91UdUOEHso6Aweg93K6xQ==", + "license": "Apache-2.0", + "dependencies": { + "assert": "^2.1.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "memfs": "^4.17.0", + "path": "^0.12.7", + "stream": "^0.0.3", + "util": "^0.12.5" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.64.0.tgz", + "integrity": "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.64.0.tgz", + "integrity": "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.64.0.tgz", + "integrity": "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.64.0.tgz", + "integrity": "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.64.0.tgz", + "integrity": "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.64.0.tgz", + "integrity": "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.64.0.tgz", + "integrity": "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.64.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.64.0.tgz", + "integrity": "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.26", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", + "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/component-emitter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", + "integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memfs": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.64.0.tgz", + "integrity": "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-to-fsa": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-unified": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/path-unified/-/path-unified-0.2.0.tgz", + "integrity": "sha512-MNKqvrKbbbb5p7XHXV6ZAsf/1f/yJQa13S/fcX0uua8ew58Tgc6jXV+16JyAbnR/clgCH+euKDxrF2STxMHdrg==", + "license": "MIT" + }, + "node_modules/path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/path/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stream": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz", + "integrity": "sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==", + "license": "MIT", + "dependencies": { + "component-emitter": "^2.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/style-dictionary": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/style-dictionary/-/style-dictionary-5.5.0.tgz", + "integrity": "sha512-AGkOZtAc3OTz99wlzstrmj5OM5BWOW2IbmXD74sf0MXFPi271TGBdywokgd7bS3L0tKOk9M0FR+R9gnbXRSSfg==", + "license": "Apache-2.0", + "dependencies": { + "@bundled-es-modules/deepmerge": "^4.3.2", + "@bundled-es-modules/glob": "^13.0.6", + "@bundled-es-modules/memfs": "^4.17.0", + "@zip.js/zip.js": "^2.7.44", + "chalk": "^5.3.0", + "change-case": "^5.3.0", + "colorjs.io": "^0.5.2", + "commander": "^12.1.0", + "is-plain-obj": "^4.1.0", + "json5": "^2.2.2", + "path-unified": "^0.2.0", + "prettier": "^3.3.3", + "tinycolor2": "^1.6.0" + }, + "bin": { + "style-dictionary": "bin/style-dictionary.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/thingies": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0187ce3 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "stylegallery-validation", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json", + "test": "node scripts/test-reference-adapters.mjs --json", + "validate": "node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json" + }, + "dependencies": { + "style-dictionary": "5.5.0" + } +} diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index 19fe3ea..36a87e8 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -30,6 +30,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Governance, lifecycle, generated-file, ownership, and stale-content policy remain discoverable and CI-enforced. | `scripts/validate-governance.mjs` and `scripts/test-validate-governance.mjs` | `node scripts/validate-governance.mjs --json`; `node scripts/test-validate-governance.mjs --json` | `GOVERNANCE.md`, `.github/CODEOWNERS`, generated warnings, generated metadata, root link targets, lifecycle states, `scheduled_stale_audit` metadata, and CI wiring are present. | Missing governance file, generated warning, generated metadata, CODEOWNERS coverage, or stale policy fixtures must fail. Preferred governance prose and link labels emit warnings. | Proves governance policy is present and linked, not that CODEOWNERS users have verified repository write access. | | Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity, source ownership, or local quality. | | Consumer-reference handoffs, schema/runtime parity, repository-local record paths, lifecycle separation, ownership truth, and dependency direction remain enforced. | `scripts/validate-consumer-reference.mjs` and `scripts/test-validate-consumer-reference.mjs` | `node scripts/validate-consumer-reference.mjs --json`; `node scripts/test-validate-consumer-reference.mjs --json` | Declared and reasoned-not-applicable fixtures exit zero, every shipped schema rule has accepted/rejected parity coverage, all repository handoffs declare applicability, fixtures remain related, and ownership discloses placeholder single-account review. | Unknown top-level or nested properties, invalid schema fields, missing handoff or reason, stable with ended support, boolean independence, unresolved/absolute/scheme/network/parent/item-or-record-symlink/non-JSON paths, scaffold success, fifth-domain classification, literal or supported computed reverse imports, and repository handoff omissions must fail with named finding codes and non-zero child exit. | Proves receiver-contract consistency, bounded static dependency detection, and path containment, not general JavaScript data-flow safety, visual quality, independent adoption, verified owner permissions, or consumer implementation conformance. | +| Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | ## Validator Severity Contract diff --git a/scripts/build-reference-artifacts.mjs b/scripts/build-reference-artifacts.mjs new file mode 100644 index 0000000..65463a3 --- /dev/null +++ b/scripts/build-reference-artifacts.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import StyleDictionary from "style-dictionary"; +import { adapter, createStyleDictionaryConfig } from "../consumer-reference/adapters/style-dictionary.config.mjs"; +import { validatePortableTokens } from "./reference-token-contract.mjs"; + +const root = process.cwd(); +const defaults = { + adapter: "style-dictionary", + manifest: "consumer-reference/generated/manifest.json", + output: "consumer-reference/generated/tokens.css", + source: "consumer-reference/fixtures/token-portability/valid-reference.json", +}; + +function parseArguments() { + const options = { ...defaults, failOnWarning: false, json: false }; + for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (argument === "--fail-on-warning") options.failOnWarning = true; + else if (["--adapter", "--manifest", "--output", "--source"].includes(argument)) { + const value = process.argv[index + 1]; + if (!value) return { failure: { code: "argument_value_required", message: `${argument} requires a value`, path: "" }, options }; + options[argument.slice(2)] = value; + index += 1; + } else return { failure: { code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }, options }; + } + return { options }; +} + +function hash(content) { + return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`; +} + +function declarations(css) { + return [...css.matchAll(/^\s*(--[a-z0-9-]+):\s*([^;]+);\s*$/gm)].map((match) => ({ name: match[1], value: match[2].trim() })); +} + +function emit(report, json) { + const text = json ? JSON.stringify(report, null, 2) : report.ok ? `ok: ${report.outputCount} token declarations` : report.failures.map((item) => `${item.code}: ${item.message}`).join("\n"); + (json || report.ok ? console.log : console.error)(text); +} + +const parsed = parseArguments(); +if (parsed.failure) { + emit({ failures: [parsed.failure], ok: false, warnings: [] }, parsed.options.json); + process.exit(1); +} +const options = parsed.options; +const source = path.resolve(root, options.source); +const output = path.resolve(root, options.output); +const manifestPath = path.resolve(root, options.manifest); +const failures = []; +const warnings = []; +let sourceBytes; +let document; +try { + sourceBytes = fs.readFileSync(source); + document = JSON.parse(sourceBytes.toString("utf8")); +} catch (error) { + const code = error instanceof SyntaxError ? "token_json_invalid" : "token_source_unresolved"; + failures.push({ code, message: error instanceof Error ? error.message : String(error), path: options.source }); +} +let contract = { failures: [], tokens: [] }; +if (document !== undefined) { + contract = validatePortableTokens(document); + failures.push(...contract.failures); +} +if (options.adapter !== adapter.name) failures.push({ code: "adapter_unsupported", message: `adapter must be ${adapter.name}`, path: "" }); +if (failures.length === 0) { + fs.mkdirSync(path.dirname(output), { recursive: true }); + const dictionary = new StyleDictionary(createStyleDictionaryConfig(source, output), { verbosity: "silent", warnings: options.failOnWarning ? "error" : "warn" }); + try { + await dictionary.buildAllPlatforms(); + } catch (error) { + failures.push({ code: "adapter_build_failed", message: error instanceof Error ? error.message : String(error), path: options.source }); + } +} +let css = ""; +if (failures.length === 0 && !fs.existsSync(output)) failures.push({ code: "artifact_output_missing", message: "adapter produced no CSS output", path: options.output }); +if (failures.length === 0) { + css = fs.readFileSync(output, "utf8"); + const emitted = declarations(css); + const expected = contract.tokens.map((token) => `--${token.path.replaceAll(".", "-")}`); + const names = emitted.map((item) => item.name); + if (emitted.length === 0) failures.push({ code: "artifact_zero_count", message: "adapter produced zero declarations", path: options.output }); + if (new Set(names).size !== names.length) failures.push({ code: "artifact_declaration_duplicate", message: "adapter produced a duplicate declaration", path: options.output }); + for (const name of expected.filter((name) => !names.includes(name))) failures.push({ code: "artifact_declaration_missing", message: `adapter omitted ${name}`, path: options.output }); + if (css.includes("[object Object]")) failures.push({ code: "artifact_object_sentinel", message: "adapter emitted [object Object]", path: options.output }); + if (/\{[a-z0-9.-]+\}|\b(?:null|undefined)\b/.test(css)) failures.push({ code: "artifact_unresolved_value", message: "adapter emitted an unresolved value", path: options.output }); + if (contract.tokens.length !== emitted.length) failures.push({ code: "artifact_count_mismatch", message: "source and output token counts differ", path: options.output }); + if (warnings.length > 0 && options.failOnWarning) failures.push({ code: "adapter_warning", message: "adapter emitted a warning", path: options.output }); + if (failures.length === 0) { + const manifest = { + adapter, + declarations: emitted, + inputHash: hash(sourceBytes), + outputCount: emitted.length, + outputFile: path.relative(path.dirname(manifestPath), output).replaceAll(path.sep, "/"), + outputHash: hash(css), + schemaVersion: "1.0", + sourceCount: contract.tokens.length, + sourceFile: path.relative(path.dirname(manifestPath), source).replaceAll(path.sep, "/"), + warnings, + }; + fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); + fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + } +} +const report = { adapter, failures, manifest: options.manifest, ok: failures.length === 0, output: options.output, outputCount: failures.length === 0 ? contract.tokens.length : 0, warnings }; +emit(report, options.json); +process.exit(report.ok ? 0 : 1); diff --git a/scripts/reference-token-contract.mjs b/scripts/reference-token-contract.mjs new file mode 100644 index 0000000..e905712 --- /dev/null +++ b/scripts/reference-token-contract.mjs @@ -0,0 +1,156 @@ +const allowedTypes = new Set(["border", "color", "dimension", "duration"]); +const aliasPattern = /^\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}$/; +const tokenNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const unsafePathSegments = new Set(["__proto__", "constructor", "prototype"]); + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function finding(code, tokenPath, message) { + return { code, message, path: tokenPath || "" }; +} + +function validatePathSegment(segment, tokenPath) { + if (unsafePathSegments.has(segment)) return [finding("token_path_segment_unsafe", tokenPath, `path segment ${segment} is unsafe for the pinned adapter`)]; + if (!tokenNamePattern.test(segment)) return [finding("token_name_invalid", tokenPath, "token names must use lowercase kebab-case")]; + return []; +} + +function exactKeys(value, allowed, tokenPath) { + return Object.keys(value) + .filter((key) => !allowed.has(key)) + .map((key) => finding("token_value_property_unknown", tokenPath, `unsupported value property ${key}`)); +} + +function validateDimension(value, tokenPath) { + if (!isObject(value)) return [finding("token_value_invalid", tokenPath, "dimension must be an object")]; + const failures = exactKeys(value, new Set(["unit", "value"]), tokenPath); + if (typeof value.value !== "number" || !Number.isFinite(value.value)) failures.push(finding("token_value_invalid", tokenPath, "dimension value must be finite")); + if (!["px", "rem"].includes(value.unit)) failures.push(finding("token_unit_unsupported", tokenPath, "dimension unit must be px or rem")); + return failures; +} + +function validateDuration(value, tokenPath) { + if (!isObject(value)) return [finding("token_value_invalid", tokenPath, "duration must be an object")]; + const failures = exactKeys(value, new Set(["unit", "value"]), tokenPath); + if (typeof value.value !== "number" || !Number.isFinite(value.value) || value.value < 0) failures.push(finding("token_value_invalid", tokenPath, "duration value must be non-negative and finite")); + if (!["ms", "s"].includes(value.unit)) failures.push(finding("token_unit_unsupported", tokenPath, "duration unit must be ms or s")); + return failures; +} + +function validateColor(value, tokenPath) { + if (!isObject(value)) return [finding("token_value_invalid", tokenPath, "color must be an object")]; + const failures = exactKeys(value, new Set(["alpha", "colorSpace", "components"]), tokenPath); + const validComponents = Array.isArray(value.components) && value.components.length === 3 && value.components.every((component) => typeof component === "number" && component >= 0 && component <= 1); + if (value.colorSpace !== "srgb" || !validComponents || (value.alpha !== undefined && (typeof value.alpha !== "number" || value.alpha < 0 || value.alpha > 1))) { + failures.push(finding("token_value_invalid", tokenPath, "color must use bounded sRGB components and optional alpha")); + } + return failures; +} + +function parseReference(value, expectedType, tokenPath) { + const match = typeof value === "string" ? aliasPattern.exec(value) : null; + if (!match) return { failures: [finding("alias_whole_required", tokenPath, "references must be whole-token curly aliases")], references: [] }; + return { failures: [], references: [{ expectedType, source: tokenPath, target: match[1] }] }; +} + +function validateBorder(value, tokenPath) { + if (!isObject(value)) return { failures: [finding("token_value_invalid", tokenPath, "border must be an object")], references: [] }; + const failures = exactKeys(value, new Set(["color", "style", "width"]), tokenPath); + const references = []; + if (value.style !== "solid") failures.push(finding("token_value_invalid", tokenPath, "only the tested solid border style is supported")); + if (typeof value.color === "string") { + const result = parseReference(value.color, "color", tokenPath); + failures.push(...result.failures); + references.push(...result.references); + } else failures.push(...validateColor(value.color, tokenPath)); + if (typeof value.width === "string") { + const result = parseReference(value.width, "dimension", tokenPath); + failures.push(...result.failures); + references.push(...result.references); + } else failures.push(...validateDimension(value.width, tokenPath)); + return { failures, references }; +} + +function validateDirectValue(token) { + if (typeof token.value === "string") return parseReference(token.value, token.type, token.path); + if (token.type === "border") return validateBorder(token.value, token.path); + if (token.type === "color") return { failures: validateColor(token.value, token.path), references: [] }; + if (token.type === "dimension") return { failures: validateDimension(token.value, token.path), references: [] }; + return { failures: validateDuration(token.value, token.path), references: [] }; +} + +export function validatePortableTokens(document) { + const failures = []; + const references = []; + const tokens = []; + if (!isObject(document)) return { failures: [finding("token_document_object_required", "", "token document must be an object")], tokens }; + + function walk(node, segments, inheritedType) { + if (!isObject(node)) { + failures.push(finding("token_node_object_required", segments.join("."), "groups and tokens must be objects")); + return; + } + const tokenPath = segments.join("."); + if (Object.hasOwn(node, "$description") && typeof node.$description !== "string") failures.push(finding("token_description_invalid", tokenPath, "$description must be a string")); + const serialized = JSON.stringify(node); + if (/"(?:resolver|modifiers?|themes?)"\s*:/.test(serialized)) failures.push(finding("token_resolver_forbidden", tokenPath, "resolvers, modifiers, and themes are outside the portable subset")); + const reserved = Object.keys(node).filter((key) => key.startsWith("$")); + if (reserved.includes("$extends")) failures.push(finding("token_extends_forbidden", tokenPath, "$extends is outside the portable subset")); + if (reserved.includes("$ref")) failures.push(finding("token_json_pointer_forbidden", tokenPath, "JSON Pointer references are outside the portable subset")); + for (const key of reserved.filter((key) => !["$description", "$type", "$value", "$extends", "$ref"].includes(key))) { + failures.push(finding("token_unknown_reserved", tokenPath, `unsupported reserved field ${key}`)); + } + const declaredType = node.$type; + if (declaredType !== undefined && !allowedTypes.has(declaredType)) failures.push(finding("token_type_unsupported", tokenPath, `unsupported token type ${String(declaredType)}`)); + const effectiveType = allowedTypes.has(declaredType) ? declaredType : inheritedType; + if (Object.hasOwn(node, "$value")) { + const allowed = new Set(["$description", "$type", "$value"]); + for (const key of Object.keys(node).filter((key) => !allowed.has(key) && !key.startsWith("$"))) failures.push(finding("token_property_unknown", tokenPath, `token contains child property ${key}`)); + if (!effectiveType) failures.push(finding("token_type_required", tokenPath, "token requires an allowed explicit or inherited type")); + else tokens.push({ path: tokenPath, type: effectiveType, value: node.$value }); + return; + } + const children = Object.entries(node).filter(([key]) => !key.startsWith("$")); + if (children.length === 0) failures.push(finding("token_group_empty", tokenPath, "group must contain at least one token")); + for (const [key, child] of children) { + failures.push(...validatePathSegment(key, [...segments, key].join("."))); + walk(child, [...segments, key], effectiveType); + } + } + + for (const [key, node] of Object.entries(document)) { + if (key.startsWith("$")) failures.push(finding("token_unknown_reserved", "", `root field ${key} is unsupported`)); + else { + failures.push(...validatePathSegment(key, key)); + walk(node, [key], undefined); + } + } + for (const token of tokens) { + const result = validateDirectValue(token); + failures.push(...result.failures); + references.push(...result.references); + } + const byPath = new Map(tokens.map((token) => [token.path, token])); + for (const reference of references) { + const target = byPath.get(reference.target); + if (!target) failures.push(finding("alias_dangling", reference.source, `alias target ${reference.target} does not exist`)); + else if (target.type !== reference.expectedType) failures.push(finding("alias_type_mismatch", reference.source, `alias target ${reference.target} has type ${target.type}`)); + } + const edges = new Map(references.filter((reference) => byPath.has(reference.target)).map((reference) => [reference.source, reference.target])); + for (const start of edges.keys()) { + const seen = new Set(); + let cursor = start; + while (edges.has(cursor)) { + if (seen.has(cursor)) { + failures.push(finding("alias_cycle", start, "alias graph contains a cycle")); + break; + } + seen.add(cursor); + cursor = edges.get(cursor); + } + } + const uniqueFailures = [...new Map(failures.map((item) => [`${item.code}:${item.path}:${item.message}`, item])).values()]; + return { failures: uniqueFailures, tokens: tokens.sort((left, right) => left.path.localeCompare(right.path)) }; +} diff --git a/scripts/test-reference-adapters.mjs b/scripts/test-reference-adapters.mjs new file mode 100644 index 0000000..af132e5 --- /dev/null +++ b/scripts/test-reference-adapters.mjs @@ -0,0 +1,159 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const builder = path.join(repositoryRoot, "scripts", "build-reference-artifacts.mjs"); +const validator = path.join(repositoryRoot, "scripts", "validate-reference-artifacts.mjs"); +const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference/schema/portable-tokens.schema.json"), "utf8")); + +function parseJson(child) { + try { + return JSON.parse(child.stdout); + } catch (error) { + return { failures: [{ code: "invalid_json_output", message: error instanceof Error ? error.message : String(error) }], ok: false }; + } +} + +function codes(output) { + return Array.isArray(output.failures) + ? output.failures.flatMap((failure) => typeof failure?.code === "string" ? [failure.code] : []) + : []; +} + +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-reference-adapter-")); +const results = []; + +function run(command, args) { + const child = spawnSync(process.execPath, [command, ...args, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + return { child, output: parseJson(child) }; +} + +function recordAccepted(name, execution, predicate = () => true) { + const actualCodes = codes(execution.output); + const accepted = execution.child.status === 0 && execution.output.ok === true && execution.output.scaffold !== true; + results.push({ + actual: { codes: actualCodes, ok: execution.output.ok, scaffold: execution.output.scaffold === true, status: execution.child.status }, + expected: "ok:true and exit:0", + name, + ok: accepted && predicate(), + }); +} + +function recordRejected(name, execution, expectedCode) { + const actualCodes = codes(execution.output); + results.push({ + actual: { codes: actualCodes, ok: execution.output.ok, scaffold: execution.output.scaffold === true, status: execution.child.status }, + expected: expectedCode, + name, + ok: execution.child.status !== 0 && execution.output.ok === false && actualCodes.includes(expectedCode), + }); +} + +try { + const groupPattern = new RegExp(Object.keys(schema.$defs.group.patternProperties)[0]); + const rootPattern = new RegExp(Object.keys(schema.patternProperties)[0]); + const unsafeSegments = ["__proto__", "prototype", "constructor"]; + results.push({ + actual: { + groupDescription: schema.$defs.group.properties.$description.type, + tokenDescription: schema.$defs.token.properties.$description.type, + unsafeGroupMatches: unsafeSegments.filter((segment) => groupPattern.test(segment)), + unsafeRootMatches: unsafeSegments.filter((segment) => rootPattern.test(segment)), + }, + expected: "string descriptions and no unsafe path segments", + name: "schema_runtime_boundary_parity", + ok: schema.$defs.group.properties.$description.type === "string" + && schema.$defs.token.properties.$description.type === "string" + && unsafeSegments.every((segment) => !groupPattern.test(segment) && !rootPattern.test(segment)), + }); + const output = path.join(tempRoot, "tokens.css"); + const manifest = path.join(tempRoot, "manifest.json"); + const validBuild = run(builder, ["--source", "consumer-reference/fixtures/token-portability/valid-reference.json", "--output", output, "--manifest", manifest, "--adapter", "style-dictionary", "--fail-on-warning"]); + recordAccepted("valid_reference_build", validBuild, () => { + const css = fs.readFileSync(output, "utf8"); + return css.includes("var(--color-accent)") && css.includes("var(--space-small)") && !css.includes("[object Object]"); + }); + recordAccepted("valid_reference_manifest", run(validator, ["--manifest", manifest])); + + for (const fixture of ["valid-whole-alias.json", "valid-border.json"]) { + recordAccepted(fixture.replace(".json", ""), run(builder, ["--source", `consumer-reference/fixtures/token-portability/${fixture}`, "--output", output, "--manifest", manifest, "--fail-on-warning"])); + } + const invalidFixtures = [ + ["invalid-extends.json", "token_extends_forbidden"], + ["invalid-json-pointer.json", "token_json_pointer_forbidden"], + ["invalid-resolver.json", "token_resolver_forbidden"], + ["invalid-unknown-reserved.json", "token_unknown_reserved"], + ["invalid-untested-type.json", "token_type_unsupported"], + ["invalid-dangling-alias.json", "alias_dangling"], + ["invalid-cycle.json", "alias_cycle"], + ["invalid-type-mismatch.json", "alias_type_mismatch"], + ]; + for (const [fixture, expected] of invalidFixtures) { + recordRejected(fixture.replace(".json", ""), run(builder, ["--source", `consumer-reference/fixtures/token-portability/${fixture}`, "--output", output, "--manifest", manifest, "--fail-on-warning"]), expected); + } + + const invalidUnit = path.join(tempRoot, "invalid-unit.json"); + fs.writeFileSync(invalidUnit, '{"space":{"$type":"dimension","bad":{"$value":{"value":1,"unit":"vh"}}}}\n'); + recordRejected("invalid_untested_unit", run(builder, ["--source", invalidUnit, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_unit_unsupported"); + const partialAlias = path.join(tempRoot, "invalid-partial-alias.json"); + fs.writeFileSync(partialAlias, '{"space":{"$type":"dimension","bad":{"$value":"calc({space.base} * 2)"}}}\n'); + recordRejected("invalid_partial_alias", run(builder, ["--source", partialAlias, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "alias_whole_required"); + const invalidGroupDescription = path.join(tempRoot, "invalid-group-description.json"); + fs.writeFileSync(invalidGroupDescription, '{"space":{"$description":{"unsafe":true},"$type":"dimension","small":{"$value":{"value":1,"unit":"px"}}}}\n'); + recordRejected("invalid_group_description", run(builder, ["--source", invalidGroupDescription, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_description_invalid"); + const invalidTokenDescription = path.join(tempRoot, "invalid-token-description.json"); + fs.writeFileSync(invalidTokenDescription, '{"space":{"$type":"dimension","small":{"$description":42,"$value":{"value":1,"unit":"px"}}}}\n'); + recordRejected("invalid_token_description", run(builder, ["--source", invalidTokenDescription, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_description_invalid"); + for (const [name, sourceText] of [ + ["constructor", '{"constructor":{"$type":"dimension","small":{"$value":{"value":1,"unit":"px"}}}}\n'], + ["prototype", '{"prototype":{"$type":"dimension","small":{"$value":{"value":1,"unit":"px"}}}}\n'], + ["__proto__", '{"__proto__":{"$type":"dimension","small":{"$value":{"value":1,"unit":"px"}}}}\n'], + ]) { + const unsafePath = path.join(tempRoot, `invalid-path-${name}.json`); + fs.writeFileSync(unsafePath, sourceText); + recordRejected(`invalid_path_${name}`, run(builder, ["--source", unsafePath, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_path_segment_unsafe"); + } + + const baselineSource = fs.readFileSync(path.join(repositoryRoot, "consumer-reference/fixtures/token-portability/valid-reference.json"), "utf8"); + fs.writeFileSync(path.join(tempRoot, "source.json"), baselineSource); + const baselineBuild = run(builder, ["--source", path.join(tempRoot, "source.json"), "--output", output, "--manifest", manifest, "--fail-on-warning"]); + recordAccepted("artifact_baseline", baselineBuild); + const baselineCss = fs.readFileSync(output, "utf8"); + const baselineManifest = fs.readFileSync(manifest, "utf8"); + const artifactCases = [ + ["no_output", "artifact_output_missing", () => fs.rmSync(output)], + ["zero_count", "artifact_zero_count", (item) => { item.sourceCount = 0; item.outputCount = 0; item.declarations = []; }], + ["warning", "artifact_warning", (item) => { item.warnings = ["synthetic warning"]; }], + ["missing_declaration", "artifact_declaration_missing", (item) => { item.declarations = item.declarations.slice(1); }], + ["duplicate_declaration", "artifact_declaration_duplicate", (item) => { item.declarations.push(item.declarations[0]); }], + ["object_sentinel", "artifact_object_sentinel", () => fs.writeFileSync(output, baselineCss.replace("120ms", "[object Object]"))], + ["unresolved_alias", "artifact_unresolved_value", () => fs.writeFileSync(output, baselineCss.replace("120ms", "{duration.quick}"))], + ["count_mismatch", "artifact_count_mismatch", (item) => { item.outputCount += 1; }], + ["scaffold_manifest", "artifact_scaffold_forbidden", (item) => { item.scaffold = true; }], + ["forged_value", "artifact_source_output_mismatch", (item) => { + const changed = baselineCss.replace("120ms", "999999s !important"); + fs.writeFileSync(output, changed); + item.outputHash = `sha256:${crypto.createHash("sha256").update(changed).digest("hex")}`; + item.declarations.find((declaration) => declaration.name === "--duration-quick").value = "999999s !important"; + }], + ]; + for (const [name, expected, mutate] of artifactCases) { + fs.writeFileSync(output, baselineCss); + const item = JSON.parse(baselineManifest); + mutate(item); + if (fs.existsSync(output)) fs.writeFileSync(manifest, `${JSON.stringify(item, null, 2)}\n`); + recordRejected(name, run(validator, ["--manifest", manifest]), expected); + } +} finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); +} + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +console.log(JSON.stringify(report, null, 2)); +process.exit(report.ok ? 0 : 1); diff --git a/scripts/validate-reference-artifacts.mjs b/scripts/validate-reference-artifacts.mjs new file mode 100644 index 0000000..46f23e0 --- /dev/null +++ b/scripts/validate-reference-artifacts.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { adapter } from "../consumer-reference/adapters/style-dictionary.config.mjs"; +import { validatePortableTokens } from "./reference-token-contract.mjs"; + +function hash(content) { + return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`; +} + +function cssDeclarations(css) { + return [...css.matchAll(/^\s*(--[a-z0-9-]+):\s*([^;]+);\s*$/gm)].map((match) => ({ name: match[1], value: match[2].trim() })); +} + +function parseArguments() { + let json = false; + let manifest = "consumer-reference/generated/manifest.json"; + const failures = []; + for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") json = true; + else if (argument === "--manifest") { + manifest = process.argv[index + 1]; + if (!manifest) failures.push({ code: "argument_value_required", message: "--manifest requires a value", path: "" }); + else index += 1; + } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); + } + return { failures, json, manifest }; +} + +const options = parseArguments(); +const failures = [...options.failures]; +const warnings = []; +const manifestPath = path.resolve(process.cwd(), options.manifest || "missing.json"); +let manifest; +if (!fs.existsSync(manifestPath)) { + failures.push({ code: "artifact_manifest_missing", message: "manifest does not exist", path: options.manifest }); + failures.push({ code: "artifact_output_missing", message: "output cannot be located without a manifest", path: options.manifest }); +} else { + try { + manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + } catch (error) { + failures.push({ code: "artifact_manifest_invalid", message: error instanceof Error ? error.message : String(error), path: options.manifest }); + } +} +if (manifest !== undefined) { + const allowedManifestProperties = new Set(["adapter", "declarations", "inputHash", "outputCount", "outputFile", "outputHash", "schemaVersion", "sourceCount", "sourceFile", "warnings"]); + if (manifest.scaffold === true) failures.push({ code: "artifact_scaffold_forbidden", message: "manifest must not contain a scaffold success sentinel", path: options.manifest }); + for (const key of Object.keys(manifest).filter((key) => !allowedManifestProperties.has(key))) failures.push({ code: "artifact_manifest_property_unknown", message: `manifest contains unsupported property ${key}`, path: options.manifest }); + if (manifest.schemaVersion !== "1.0") failures.push({ code: "artifact_schema_version", message: "manifest schemaVersion must be 1.0", path: options.manifest }); + if (manifest.adapter?.name !== adapter.name || manifest.adapter?.version !== adapter.version) failures.push({ code: "artifact_adapter_mismatch", message: "manifest adapter pin is invalid", path: options.manifest }); + if (!Number.isInteger(manifest.sourceCount) || !Number.isInteger(manifest.outputCount) || manifest.sourceCount <= 0 || manifest.outputCount <= 0) failures.push({ code: "artifact_zero_count", message: "source and output counts must be positive integers", path: options.manifest }); + if (manifest.sourceCount !== manifest.outputCount) failures.push({ code: "artifact_count_mismatch", message: "source and output counts differ", path: options.manifest }); + if (!Array.isArray(manifest.warnings)) failures.push({ code: "artifact_warnings_invalid", message: "warnings must be an array", path: options.manifest }); + else if (manifest.warnings.length > 0) failures.push({ code: "artifact_warning", message: "manifest contains adapter warnings", path: options.manifest }); + const declared = Array.isArray(manifest.declarations) ? manifest.declarations : []; + if (!Array.isArray(manifest.declarations)) failures.push({ code: "artifact_declarations_invalid", message: "declarations must be an array", path: options.manifest }); + const declarationNames = declared.map((item) => item?.name).filter((name) => typeof name === "string"); + if (new Set(declarationNames).size !== declarationNames.length) failures.push({ code: "artifact_declaration_duplicate", message: "manifest contains duplicate declarations", path: options.manifest }); + const sourcePath = typeof manifest.sourceFile === "string" ? path.resolve(path.dirname(manifestPath), manifest.sourceFile) : ""; + const outputPath = typeof manifest.outputFile === "string" ? path.resolve(path.dirname(manifestPath), manifest.outputFile) : ""; + if (!sourcePath || !fs.existsSync(sourcePath)) failures.push({ code: "artifact_source_missing", message: "manifest source file does not exist", path: options.manifest }); + else { + const sourceBytes = fs.readFileSync(sourcePath); + if (manifest.inputHash !== hash(sourceBytes)) failures.push({ code: "artifact_input_hash_mismatch", message: "input hash does not match source", path: options.manifest }); + try { + const contract = validatePortableTokens(JSON.parse(sourceBytes.toString("utf8"))); + failures.push(...contract.failures); + if (contract.tokens.length !== manifest.sourceCount) failures.push({ code: "artifact_source_count_mismatch", message: "source count does not match canonical tokens", path: options.manifest }); + const expected = contract.tokens.map((token) => `--${token.path.replaceAll(".", "-")}`); + for (const name of expected.filter((name) => !declarationNames.includes(name))) failures.push({ code: "artifact_declaration_missing", message: `manifest omitted ${name}`, path: options.manifest }); + } catch (error) { + failures.push({ code: "artifact_source_invalid", message: error instanceof Error ? error.message : String(error), path: options.manifest }); + } + } + if (!outputPath || !fs.existsSync(outputPath)) failures.push({ code: "artifact_output_missing", message: "manifest output file does not exist", path: options.manifest }); + else { + const css = fs.readFileSync(outputPath, "utf8"); + const emitted = cssDeclarations(css); + const emittedNames = emitted.map((item) => item.name); + if (manifest.outputHash !== hash(css)) failures.push({ code: "artifact_output_hash_mismatch", message: "output hash does not match CSS", path: options.manifest }); + if (emitted.length === 0) failures.push({ code: "artifact_zero_count", message: "CSS contains zero declarations", path: options.manifest }); + if (new Set(emittedNames).size !== emittedNames.length) failures.push({ code: "artifact_declaration_duplicate", message: "CSS contains duplicate declarations", path: options.manifest }); + for (const name of declarationNames.filter((name) => !emittedNames.includes(name))) failures.push({ code: "artifact_declaration_missing", message: `CSS omitted ${name}`, path: options.manifest }); + for (const declaration of declared) { + const actual = emitted.find((item) => item.name === declaration?.name); + if (actual && actual.value !== declaration.value) failures.push({ code: "artifact_declaration_mismatch", message: `CSS value for ${declaration.name} differs from the manifest`, path: options.manifest }); + } + if (emitted.length !== manifest.outputCount || emitted.length !== declarationNames.length) failures.push({ code: "artifact_count_mismatch", message: "CSS, manifest count, and declarations differ", path: options.manifest }); + if (css.includes("[object Object]")) failures.push({ code: "artifact_object_sentinel", message: "CSS contains [object Object]", path: options.manifest }); + if (/\{[a-z0-9.-]+\}|\b(?:null|undefined)\b/.test(css)) failures.push({ code: "artifact_unresolved_value", message: "CSS contains an unresolved value", path: options.manifest }); + if (sourcePath && fs.existsSync(sourcePath)) { + const rebuildRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-reference-validation-")); + try { + const expectedOutput = path.join(rebuildRoot, "tokens.css"); + const expectedManifest = path.join(rebuildRoot, "manifest.json"); + const builder = path.join(path.dirname(new URL(import.meta.url).pathname), "build-reference-artifacts.mjs"); + const child = spawnSync(process.execPath, [builder, "--source", sourcePath, "--output", expectedOutput, "--manifest", expectedManifest, "--adapter", "style-dictionary", "--fail-on-warning", "--json"], { cwd: process.cwd(), encoding: "utf8" }); + if (child.status !== 0 || !fs.existsSync(expectedOutput)) failures.push({ code: "artifact_canonical_rebuild_failed", message: "canonical source could not be rebuilt with the pinned adapter", path: options.manifest }); + else if (fs.readFileSync(expectedOutput, "utf8") !== css) failures.push({ code: "artifact_source_output_mismatch", message: "CSS differs from the pinned adapter output derived from canonical source", path: options.manifest }); + } finally { + fs.rmSync(rebuildRoot, { force: true, recursive: true }); + } + } + } +} +const uniqueFailures = [...new Map(failures.map((item) => [`${item.code}:${item.path}:${item.message}`, item])).values()]; +const report = { failures: uniqueFailures, manifest: options.manifest, ok: uniqueFailures.length === 0, warnings }; +const text = options.json ? JSON.stringify(report, null, 2) : report.ok ? "ok: reference artifacts" : uniqueFailures.map((item) => `${item.code}: ${item.message}`).join("\n"); +(options.json || report.ok ? console.log : console.error)(text); +process.exit(report.ok ? 0 : 1); From 620d764e140da15f35830aadb27f3272aa2b52ed Mon Sep 17 00:00:00 2001 From: victor Date: Mon, 13 Jul 2026 21:31:24 +0900 Subject: [PATCH 02/44] Add adversarial design reference profiles --- .github/CODEOWNERS | 1 + DOMAINS.md | 2 +- GOVERNANCE.md | 2 + README.md | 1 + consumer-reference/schema/item.schema.json | 59 ++++++- design-engineering/index.md | 1 + .../external-adaptation/index.md | 12 ++ .../editorial/local-foundations.json | 12 ++ .../governed-local/editorial/profile.json | 38 ++++ .../governed-local/editorial/tokens.dtcg.json | 17 ++ .../governed-local/index.md | 17 ++ .../terminal/local-foundations.json | 12 ++ .../governed-local/terminal/profile.json | 38 ++++ .../governed-local/terminal/tokens.dtcg.json | 17 ++ .../reference-profiles/index.md | 19 ++ scripts/consumer-reference-schema.mjs | 43 +++++ scripts/reference-profile-contract.mjs | 113 ++++++++++++ scripts/test-validate-consumer-reference.mjs | 7 + scripts/test-validate-domains.mjs | 8 +- scripts/test-validate-ia.mjs | 19 +- scripts/test-validate-reference-profiles.mjs | 164 ++++++++++++++++++ scripts/validate-consumer-reference.mjs | 44 ++++- scripts/validate-domains.mjs | 34 +++- scripts/validate-ia.mjs | 12 +- 24 files changed, 679 insertions(+), 13 deletions(-) create mode 100644 design-engineering/reference-profiles/external-adaptation/index.md create mode 100644 design-engineering/reference-profiles/governed-local/editorial/local-foundations.json create mode 100644 design-engineering/reference-profiles/governed-local/editorial/profile.json create mode 100644 design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json create mode 100644 design-engineering/reference-profiles/governed-local/index.md create mode 100644 design-engineering/reference-profiles/governed-local/terminal/local-foundations.json create mode 100644 design-engineering/reference-profiles/governed-local/terminal/profile.json create mode 100644 design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json create mode 100644 design-engineering/reference-profiles/index.md create mode 100644 scripts/reference-profile-contract.mjs create mode 100644 scripts/test-validate-reference-profiles.mjs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e69aaad..0b75b97 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,7 @@ /layout/ @changeroa /motion/ @changeroa /design-engineering/ @changeroa +/design-engineering/reference-profiles/ @changeroa /platform-guides/ @changeroa /consumer-reference/ @changeroa /consumer-reference/adapters/ @changeroa diff --git a/DOMAINS.md b/DOMAINS.md index 9a659b4..6e7c392 100644 --- a/DOMAINS.md +++ b/DOMAINS.md @@ -25,7 +25,7 @@ This manifest is the source of truth for top-level StyleGallery domains. A domai | --- | --- | --- | | Layout | `layout/index.md` | Existing `GUIDE.md`, Layout-specific `guides/*.md`, and `recipes/*.md`; generated `patterns/**/*.md` and `CATALOG.md` remain at current paths. Shared `quality/**/*.md` infrastructure governs every domain without becoming a Layout leaf. | | Motion | `motion/index.md` | `motion/vocabulary.md`, `motion/review-workflow.md`, `motion/practice-reference.md` | -| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md` | +| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md`, `design-engineering/reference-profiles/index.md`, `design-engineering/reference-profiles/governed-local/index.md`, `design-engineering/reference-profiles/external-adaptation/index.md` | | Platform Guides | `platform-guides/index.md` | `platform-guides/apple-interaction.md` | ## Shared Non-Domain Infrastructure diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 58c0a13..7dfb1fa 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -23,6 +23,7 @@ Use this file before editing repository documentation. It names which file is au | Quality gates and evidence | `quality/**/*.md` | Manual | None | `stable` | Claim-boundary changes, evidence-family changes, or broken quality links. | `scripts/validate-okf.mjs`, `scripts/validate-links.mjs`, `scripts/validate-ia.mjs` | Quality owner | | Consumer reference contract | `consumer-reference/contract.md`, `consumer-reference/schema/*.json` | Manual | None | `stable` contract with related fixtures | Handoff shape, path boundary, lifecycle, ownership, or dependency-direction changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Repository governance owner with Validation owner | | Portable token source | `consumer-reference/fixtures/token-portability/valid-reference.json`, `consumer-reference/schema/portable-tokens.schema.json` | `scripts/build-reference-artifacts.mjs` with Style Dictionary `5.5.0` | `consumer-reference/generated/tokens.css`, `consumer-reference/generated/manifest.json` | `stable` restricted adapter contract, `generated` output | Allowed token shape, adapter/version pin, source token count, warning, declaration, or content hash changes. | `scripts/validate-reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | +| Governed local reference profiles | `design-engineering/reference-profiles/governed-local/**` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Layout revision, identity values, UA/reset assumptions, explicit selection, or fixture relationship changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Design Engineering owner with Validation owner | | Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner | | Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner | | Motion domain guidance | `motion/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Motion domain owner | @@ -90,6 +91,7 @@ Consumer-reference ownership records the current truth as `owner.enforcement: "p | `DOMAINS.md`, `layout/**` | Repository governance owner with Pattern-data owner | Domain routing and preservation of the stable Layout path contract. | | `motion/**` | Motion domain owner | Motion terminology, review procedure, practice classification, and evidence boundaries. | | `design-engineering/**` | Design Engineering domain owner | Separation of product heuristics from shared quality gates. | +| `design-engineering/reference-profiles/**` | Design Engineering owner with Validation owner | Profile-local identity and values, pinned Layout provenance, explicit non-default selection, and related-fixture truth. | | `platform-guides/**` | Platform Guides domain owner | Platform/source/version limits, comparison boundaries, and stale review. | | `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs`, `.github/workflows/validate.yml` | Validation owner | Validator scope, negative fixtures, CI parity. | diff --git a/README.md b/README.md index cdcb12c..5b6a224 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Each common task has one primary route. Use secondary links only after the prima | `browse reusable spatial guidance` | [Layout](layout/index.md) | It preserves the existing pattern, recipe, and planning routes. | | `name or review interface motion` | [Motion](motion/index.md) | It routes to bounded terminology and review guidance. | | `review product-level interface craft` | [Design Engineering](design-engineering/index.md) | It separates practitioner heuristics from shared quality gates. | +| `compare adversarial consumer identities` | [Reference Profiles](design-engineering/reference-profiles/index.md) | It keeps non-default product values in related Design Engineering examples over one pinned Layout source. | | `compare a named platform convention` | [Platform Guides](platform-guides/index.md) | It requires platform and evidence boundaries before adaptation. | | `turn raw content into a homepage or ordinary webpage` | [Webpage Generation Workflow](guides/webpage-generation-workflow.md) | It starts with use case, content-to-layout fit, harmony, and handoff. | | `plan a screen before the layout problem is obvious` | [Layout Planning Guide](GUIDE.md) | It sequences task, content, scroll, recipe, and verification choices. | diff --git a/consumer-reference/schema/item.schema.json b/consumer-reference/schema/item.schema.json index 1202c42..0d3f4c2 100644 --- a/consumer-reference/schema/item.schema.json +++ b/consumer-reference/schema/item.schema.json @@ -22,12 +22,55 @@ "required": ["maturity"] }, "then": { "required": ["removal_trigger", "replacement"] } + }, + { + "if": { "required": ["profile_kind"] }, + "then": { + "properties": { + "artifact_mode": { "const": "governed_local" }, + "default": { "const": false }, + "example_only": { "const": true }, + "maturity": { "const": "experimental" }, + "profile_kind": { "const": "governed_local" } + }, + "required": [ + "default", + "environment_assumptions", + "example_only", + "layout_source_sha", + "local_foundations", + "profile_kind", + "related_fixture_set_id", + "selection", + "tokens" + ] + } } ], "properties": { "artifact_mode": { "enum": ["consumer_local", "external_consumer", "governed_local", "schema_only"] }, + "default": { "const": false }, + "environment_assumptions": { + "additionalProperties": false, + "properties": { + "reset": { + "additionalProperties": false, + "properties": { + "body_margin": { "minLength": 1, "type": "string" }, + "box_sizing": { "minLength": 1, "type": "string" }, + "figure_margin": { "minLength": 1, "type": "string" } + }, + "required": ["body_margin", "box_sizing", "figure_margin"], + "type": "object" + }, + "user_agent_styles": { "minLength": 1, "type": "string" } + }, + "required": ["reset", "user_agent_styles"], + "type": "object" + }, + "example_only": { "const": true }, "fixture_independence": { "const": "related" }, "handoff": { "oneOf": [ @@ -59,6 +102,8 @@ ] }, "id": { "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "type": "string" }, + "layout_source_sha": { "const": "775430bbaf4ee208a642220f440f6926d79c90a3" }, + "local_foundations": { "const": "local-foundations.json" }, "maturity": { "enum": ["deprecated", "experimental", "stable"] }, "owner": { "additionalProperties": false, @@ -70,15 +115,27 @@ "type": "object" }, "removal_trigger": { "minLength": 1, "type": "string" }, + "profile_kind": { "const": "governed_local" }, + "related_fixture_set_id": { "minLength": 1, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "type": "string" }, "replacement": { "minLength": 1, "type": "string" }, "review_independence": { "const": "single_account" }, "schema_version": { "const": "1.0" }, + "selection": { + "additionalProperties": false, + "properties": { + "method": { "const": "profile_path" }, + "required": { "const": true } + }, + "required": ["method", "required"], + "type": "object" + }, "support": { "additionalProperties": false, "properties": { "status": { "enum": ["active", "ended"] } }, "required": ["status"], "type": "object" - } + }, + "tokens": { "const": "tokens.dtcg.json" } }, "required": [ "artifact_mode", diff --git a/design-engineering/index.md b/design-engineering/index.md index 018b719..5858fdb 100644 --- a/design-engineering/index.md +++ b/design-engineering/index.md @@ -11,6 +11,7 @@ Out of scope: a second universal principle set, unsupported claims of beauty or ## Documents - [Interface Craft Decisions](interface-craft.md) connects product purpose, implementation detail, and verification without treating taste as proof. +- [Reference Profiles](reference-profiles/index.md) provides two explicitly selected, related adversarial examples and a documentation-only external adaptation route. ## Relationship To Shared Quality diff --git a/design-engineering/reference-profiles/external-adaptation/index.md b/design-engineering/reference-profiles/external-adaptation/index.md new file mode 100644 index 0000000..8ab711a --- /dev/null +++ b/design-engineering/reference-profiles/external-adaptation/index.md @@ -0,0 +1,12 @@ +# External Consumer Adaptation + +Synthetic validator coverage only; no durable adopter record exists in this route. + +An external consumer keeps its canonical JSON record in its own repository, selects it explicitly there, and points only toward the pinned Layout source. StyleGallery documents that route without storing an adopter, attestation, production, independence, or promotion claim. + +The validator exercises a synthetic local-as-external mutation and rejects any JSON record placed below this documentation directory. + +## IA Navigation + +Parent: [Reference Profiles](../index.md). +Next: [Platform Guides](../../../platform-guides/index.md). diff --git a/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json b/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json new file mode 100644 index 0000000..c5db770 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json @@ -0,0 +1,12 @@ +{ + "bindings": { + "accent": "{color.accent}", + "body_font_family": "Georgia, Times New Roman, serif", + "canvas": "{color.paper}", + "content_rhythm": "{space.rhythm}", + "text": "{color.ink}" + }, + "identity": "Warm long-form editorial publication", + "profile_id": "editorial-reference-profile", + "schema_version": "1.0" +} diff --git a/design-engineering/reference-profiles/governed-local/editorial/profile.json b/design-engineering/reference-profiles/governed-local/editorial/profile.json new file mode 100644 index 0000000..b9b4365 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/editorial/profile.json @@ -0,0 +1,38 @@ +{ + "artifact_mode": "governed_local", + "default": false, + "environment_assumptions": { + "reset": { + "body_margin": "0", + "box_sizing": "border-box", + "figure_margin": "0" + }, + "user_agent_styles": "Preserve semantic browser defaults except for the declared reset." + }, + "example_only": true, + "fixture_independence": "related", + "handoff": { + "record": "design-engineering/reference-profiles/governed-local/editorial/profile.json", + "status": "declared" + }, + "id": "editorial-reference-profile", + "layout_source_sha": "775430bbaf4ee208a642220f440f6926d79c90a3", + "local_foundations": "local-foundations.json", + "maturity": "experimental", + "owner": { + "enforcement": "placeholder", + "name": "Design Engineering owner" + }, + "profile_kind": "governed_local", + "related_fixture_set_id": "layout-identity-adversarial-pair", + "review_independence": "single_account", + "schema_version": "1.0", + "selection": { + "method": "profile_path", + "required": true + }, + "support": { + "status": "active" + }, + "tokens": "tokens.dtcg.json" +} diff --git a/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json b/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json new file mode 100644 index 0000000..ba77da2 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json @@ -0,0 +1,17 @@ +{ + "color": { + "$type": "color", + "accent": { "$value": { "alpha": 1, "colorSpace": "srgb", "components": [0.58, 0.2, 0.12] } }, + "ink": { "$value": { "alpha": 1, "colorSpace": "srgb", "components": [0.12, 0.09, 0.07] } }, + "paper": { "$value": { "alpha": 1, "colorSpace": "srgb", "components": [0.97, 0.93, 0.84] } } + }, + "duration": { + "$type": "duration", + "feedback": { "$value": { "unit": "ms", "value": 160 } } + }, + "space": { + "$type": "dimension", + "page": { "$value": { "unit": "rem", "value": 1.5 } }, + "rhythm": { "$value": { "unit": "rem", "value": 0.75 } } + } +} diff --git a/design-engineering/reference-profiles/governed-local/index.md b/design-engineering/reference-profiles/governed-local/index.md new file mode 100644 index 0000000..a0b6f7f --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/index.md @@ -0,0 +1,17 @@ +# Governed Local Reference Profiles + +Editorial and terminal are experimental, example-only profiles that use the same Layout revision and related fixture set. Selection is always an explicit `profile.json` path; neither profile is a default. + +Their token and foundation values intentionally differ. Their shared relationship means they cannot count as independent consumers, adopters, or promotion evidence. + +## Profile Records + +- `editorial/profile.json` describes a warm publication identity. +- `terminal/profile.json` describes a dark operator-console identity. + +The sibling `tokens.dtcg.json` files use the restricted portable token subset. The sibling `local-foundations.json` files keep identity bindings local to each profile. + +## IA Navigation + +Parent: [Reference Profiles](../index.md). +Next: [External Adaptation](../external-adaptation/index.md). diff --git a/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json b/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json new file mode 100644 index 0000000..71107c5 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json @@ -0,0 +1,12 @@ +{ + "bindings": { + "accent": "{color.accent}", + "body_font_family": "SFMono-Regular, Consolas, monospace", + "canvas": "{color.screen}", + "content_rhythm": "{space.rhythm}", + "text": "{color.ink}" + }, + "identity": "Dark high-density operator terminal", + "profile_id": "terminal-reference-profile", + "schema_version": "1.0" +} diff --git a/design-engineering/reference-profiles/governed-local/terminal/profile.json b/design-engineering/reference-profiles/governed-local/terminal/profile.json new file mode 100644 index 0000000..6dc353c --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/profile.json @@ -0,0 +1,38 @@ +{ + "artifact_mode": "governed_local", + "default": false, + "environment_assumptions": { + "reset": { + "body_margin": "0", + "box_sizing": "border-box", + "figure_margin": "0" + }, + "user_agent_styles": "Preserve semantic browser defaults except for the declared reset." + }, + "example_only": true, + "fixture_independence": "related", + "handoff": { + "record": "design-engineering/reference-profiles/governed-local/terminal/profile.json", + "status": "declared" + }, + "id": "terminal-reference-profile", + "layout_source_sha": "775430bbaf4ee208a642220f440f6926d79c90a3", + "local_foundations": "local-foundations.json", + "maturity": "experimental", + "owner": { + "enforcement": "placeholder", + "name": "Design Engineering owner" + }, + "profile_kind": "governed_local", + "related_fixture_set_id": "layout-identity-adversarial-pair", + "review_independence": "single_account", + "schema_version": "1.0", + "selection": { + "method": "profile_path", + "required": true + }, + "support": { + "status": "active" + }, + "tokens": "tokens.dtcg.json" +} diff --git a/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json b/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json new file mode 100644 index 0000000..61c5f59 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json @@ -0,0 +1,17 @@ +{ + "color": { + "$type": "color", + "accent": { "$value": { "alpha": 1, "colorSpace": "srgb", "components": [0.25, 0.92, 0.55] } }, + "ink": { "$value": { "alpha": 1, "colorSpace": "srgb", "components": [0.76, 0.95, 0.83] } }, + "screen": { "$value": { "alpha": 1, "colorSpace": "srgb", "components": [0.02, 0.05, 0.04] } } + }, + "duration": { + "$type": "duration", + "feedback": { "$value": { "unit": "ms", "value": 80 } } + }, + "space": { + "$type": "dimension", + "page": { "$value": { "unit": "rem", "value": 1 } }, + "rhythm": { "$value": { "unit": "rem", "value": 0.5 } } + } +} diff --git a/design-engineering/reference-profiles/index.md b/design-engineering/reference-profiles/index.md new file mode 100644 index 0000000..c1943da --- /dev/null +++ b/design-engineering/reference-profiles/index.md @@ -0,0 +1,19 @@ +# Design Engineering Reference Profiles + +Domain classification: design-engineering. + +These experimental examples challenge the consumer-reference contract with intentionally different identities over the same pinned Layout source. They are related adversarial fixtures, not defaults, canonical guidance, production consumers, or independent adoption evidence. + +## Routes + +- [Governed Local Profiles](governed-local/index.md) explains the two explicitly selected repository-owned examples. +- [External Adaptation](external-adaptation/index.md) explains how external consumers retain their records without creating an in-repository adopter record. + +## Ownership Boundary + +Design Engineering owns profile identity and values. Consumer Reference validates shape and provenance without owning those values. Layout remains upstream and cannot import these records. + +## IA Navigation + +Parent: [Design Engineering](../index.md). +Next: [Governed Local Profiles](governed-local/index.md). diff --git a/scripts/consumer-reference-schema.mjs b/scripts/consumer-reference-schema.mjs index bd0e436..31bdff7 100644 --- a/scripts/consumer-reference-schema.mjs +++ b/scripts/consumer-reference-schema.mjs @@ -1,12 +1,19 @@ const fieldCodes = { artifact_mode: "artifact_mode_invalid", + default: "profile_default_forbidden", + example_only: "profile_example_only_required", fixture_independence: "fixture_independence_related", id: "item_id_invalid", + layout_source_sha: "profile_layout_source_sha_required", + local_foundations: "profile_foundations_path_required", maturity: "maturity_invalid", + profile_kind: "profile_kind_governed_local", + related_fixture_set_id: "profile_related_fixture_set_required", removal_trigger: "removal_trigger_invalid", replacement: "replacement_invalid", review_independence: "review_independence_single_account", schema_version: "schema_version_invalid", + tokens: "profile_tokens_path_required", }; export function isPlainObject(value) { @@ -29,6 +36,40 @@ function missingProperties(value, required) { return required.filter((key) => !Object.hasOwn(value, key)); } +function validateProfileObjects(item, schema, add) { + if (Object.hasOwn(item, "environment_assumptions")) { + const environment = item.environment_assumptions; + const definition = schema.properties.environment_assumptions; + if (!isPlainObject(environment)) { + add("profile_environment_object_required", "environment_assumptions must be an object"); + } else { + for (const property of unknownProperties(environment, definition.properties)) add("profile_environment_property_unknown", `unknown environment assumption ${property}`); + if (!Object.hasOwn(environment, "user_agent_styles") || !scalarMatches(environment.user_agent_styles, definition.properties.user_agent_styles)) add("profile_ua_assumption_required", "user_agent_styles must be a non-empty string"); + const reset = environment.reset; + if (!isPlainObject(reset)) { + add("profile_reset_assumptions_required", "reset assumptions must be an object"); + } else { + for (const property of unknownProperties(reset, definition.properties.reset.properties)) add("profile_reset_property_unknown", `unknown reset assumption ${property}`); + for (const property of definition.properties.reset.required) { + if (!Object.hasOwn(reset, property) || !scalarMatches(reset[property], definition.properties.reset.properties[property])) add("profile_reset_assumptions_required", `reset requires ${property}`); + } + } + } + } + if (Object.hasOwn(item, "selection")) { + const selection = item.selection; + const definition = schema.properties.selection; + if (!isPlainObject(selection)) { + add("profile_selection_object_required", "selection must be an object"); + } else { + for (const property of unknownProperties(selection, definition.properties)) add("profile_selection_property_unknown", `unknown selection property ${property}`); + for (const property of definition.required) { + if (!Object.hasOwn(selection, property) || !scalarMatches(selection[property], definition.properties[property])) add("profile_explicit_selection_required", `selection requires ${property}`); + } + } + } +} + export function validateItemSchema(item, schema) { const findings = []; const add = (code, message) => findings.push({ code, message }); @@ -89,6 +130,8 @@ export function validateItemSchema(item, schema) { } } + validateProfileObjects(item, schema, add); + if (item.maturity === "stable" && item.support?.status === "ended") add("stable_support_ended", "stable maturity cannot have ended support"); if (item.maturity === "deprecated") { if (!Object.hasOwn(item, "replacement") || !Object.hasOwn(item, "removal_trigger")) add("deprecated_migration_required", "deprecated items require replacement and removal_trigger"); diff --git a/scripts/reference-profile-contract.mjs b/scripts/reference-profile-contract.mjs new file mode 100644 index 0000000..da8f28d --- /dev/null +++ b/scripts/reference-profile-contract.mjs @@ -0,0 +1,113 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { isPlainObject } from "./consumer-reference-schema.mjs"; +import { validatePortableTokens } from "./reference-token-contract.mjs"; + +const layoutSourceSha = "775430bbaf4ee208a642220f440f6926d79c90a3"; +const requiredResetFields = ["body_margin", "box_sizing", "figure_margin"]; + +function hash(content) { + return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`; +} + +function finding(code, relative, message) { + return { code, message, path: relative }; +} + +function isInside(base, target) { + const relative = path.relative(base, target); + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); +} + +function readJson({ failures, kind, relative, root }) { + const absolute = path.resolve(root, relative); + if (!fs.existsSync(absolute)) { + failures.push(finding(`${kind}_unresolved`, relative, "profile artifact must exist beside profile.json")); + return null; + } + const realRoot = fs.realpathSync(root); + const realTarget = fs.realpathSync(absolute); + if (!isInside(realRoot, realTarget)) { + failures.push(finding(`${kind}_symlink_escape`, relative, "profile artifact resolves outside the repository root")); + return null; + } + if (realTarget !== path.join(realRoot, relative)) { + failures.push(finding(`${kind}_redirect`, relative, "profile artifact must remain at its exact sibling path")); + return null; + } + if (!fs.statSync(realTarget).isFile()) { + failures.push(finding(`${kind}_type_invalid`, relative, "profile artifact must be a regular JSON file")); + return null; + } + const bytes = fs.readFileSync(realTarget); + try { + return { bytes, value: JSON.parse(bytes.toString("utf8")) }; + } catch (error) { + failures.push(finding(`${kind}_json_invalid`, relative, error instanceof Error ? error.message : String(error))); + return null; + } +} + +export function validateReferenceProfile({ item, relative, root }) { + const failures = []; + const add = (code, message) => failures.push(finding(code, relative, message)); + if (item.profile_kind !== "governed_local") add("profile_kind_governed_local", "profile_kind must be governed_local"); + if (item.artifact_mode !== "governed_local") add("profile_artifact_mode_governed_local", "governed-local profiles cannot be external consumers"); + if (item.maturity !== "experimental") add("profile_maturity_experimental", "governed-local profiles must remain experimental"); + if (item.example_only !== true) add("profile_example_only_required", "governed-local profiles must be example_only"); + if (item.default !== false) add("profile_default_forbidden", "governed-local profiles cannot be implicit or default"); + if (item.layout_source_sha !== layoutSourceSha) add("profile_layout_source_sha_required", `layout_source_sha must pin ${layoutSourceSha}`); + if (typeof item.related_fixture_set_id !== "string" || item.related_fixture_set_id.length === 0) add("profile_related_fixture_set_required", "related_fixture_set_id is required"); + if (!isPlainObject(item.selection) || item.selection.required !== true || item.selection.method !== "profile_path") add("profile_explicit_selection_required", "selection must require an explicit profile_path"); + if (!isPlainObject(item.environment_assumptions) || typeof item.environment_assumptions.user_agent_styles !== "string" || item.environment_assumptions.user_agent_styles.length === 0) { + add("profile_ua_assumption_required", "user-agent style assumptions must be explicit"); + } + const reset = isPlainObject(item.environment_assumptions) ? item.environment_assumptions.reset : null; + if (!isPlainObject(reset) || requiredResetFields.some((field) => typeof reset[field] !== "string" || reset[field].length === 0)) { + add("profile_reset_assumptions_required", "body, box-sizing, and figure reset assumptions must be explicit"); + } + if (item.tokens !== "tokens.dtcg.json") add("profile_tokens_path_required", "tokens must resolve to the sibling tokens.dtcg.json"); + if (item.local_foundations !== "local-foundations.json") add("profile_foundations_path_required", "foundations must resolve to the sibling local-foundations.json"); + + const directory = path.posix.dirname(relative); + const tokenRelative = path.posix.join(directory, "tokens.dtcg.json"); + const foundationRelative = path.posix.join(directory, "local-foundations.json"); + const tokenDocument = readJson({ failures, kind: "profile_tokens", relative: tokenRelative, root }); + const foundations = readJson({ failures, kind: "profile_foundations", relative: foundationRelative, root }); + if (tokenDocument) { + for (const tokenFailure of validatePortableTokens(tokenDocument.value).failures) { + failures.push(finding(tokenFailure.code, tokenRelative, tokenFailure.message)); + } + } + if (foundations) { + if (!isPlainObject(foundations.value) + || foundations.value.profile_id !== item.id + || typeof foundations.value.identity !== "string" + || !isPlainObject(foundations.value.bindings)) { + failures.push(finding("profile_foundations_invalid", foundationRelative, "foundations require matching profile_id, identity, and bindings")); + } + } + + const summary = tokenDocument && foundations ? { + foundationHash: hash(foundations.bytes), + id: item.id, + identity: foundations.value.identity, + layoutSourceSha: item.layout_source_sha, + relatedFixtureSetId: item.related_fixture_set_id, + tokenHash: hash(tokenDocument.bytes), + } : null; + return { failures, summary }; +} + +export function validateReferenceProfileSet(summaries) { + if (summaries.length < 2) return []; + const failures = []; + const unique = (field) => new Set(summaries.map((summary) => summary[field])).size; + if (unique("layoutSourceSha") !== 1) failures.push(finding("profile_layout_source_mismatch", "", "profiles must share one Layout source SHA")); + if (unique("relatedFixtureSetId") !== 1) failures.push(finding("profile_related_set_mismatch", "", "profiles must share one related fixture set")); + if (unique("identity") !== summaries.length) failures.push(finding("profile_identity_not_distinct", "", "adversarial profiles must declare distinct identities")); + if (unique("tokenHash") !== summaries.length) failures.push(finding("profile_tokens_not_distinct", "", "adversarial profiles must have distinct token bytes")); + if (unique("foundationHash") !== summaries.length) failures.push(finding("profile_foundations_not_distinct", "", "adversarial profiles must have distinct foundation bytes")); + return failures; +} diff --git a/scripts/test-validate-consumer-reference.mjs b/scripts/test-validate-consumer-reference.mjs index 7ce54d1..2b97af6 100644 --- a/scripts/test-validate-consumer-reference.mjs +++ b/scripts/test-validate-consumer-reference.mjs @@ -224,9 +224,16 @@ const coveredSchemaRules = new Set(results.flatMap((result) => result.rules)); const expectedSchemaRules = new Set(expectedSchemaRuleNames); const missingSchemaRules = [...expectedSchemaRules].filter((rule) => !coveredSchemaRules.has(rule)); failures.push(...missingSchemaRules.map((rule) => `missing_schema_rule:${rule}`)); +const profileHarness = spawnSync(process.execPath, [path.join(repositoryRoot, "scripts", "test-validate-reference-profiles.mjs"), "--json"], { + cwd: repositoryRoot, + encoding: "utf8", +}); +const profileReport = JSON.parse(profileHarness.stdout); +if (profileHarness.status !== 0 || profileReport.ok !== true) failures.push("missing_semantic:governed_local_reference_profiles"); const report = { failures, ok: failures.length === 0, + profileReport, results, schemaParity: { coveredRules: [...coveredSchemaRules].sort(), missingRules: missingSchemaRules }, }; diff --git a/scripts/test-validate-domains.mjs b/scripts/test-validate-domains.mjs index a848063..9ea73f5 100644 --- a/scripts/test-validate-domains.mjs +++ b/scripts/test-validate-domains.mjs @@ -106,7 +106,7 @@ const baseFiles = { "| --- | --- | --- |", "| Layout | `layout/index.md` | Existing Layout corpus. |", "| Motion | `motion/index.md` | `motion/vocabulary.md`, `motion/review-workflow.md`, `motion/practice-reference.md` |", - "| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md` |", + "| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md`, `design-engineering/reference-profiles/index.md`, `design-engineering/reference-profiles/governed-local/index.md`, `design-engineering/reference-profiles/external-adaptation/index.md` |", "| Platform Guides | `platform-guides/index.md` | `platform-guides/apple-interaction.md` |", "", `Source snapshot \`${revision}\`.`, @@ -121,8 +121,11 @@ const baseFiles = { "motion/vocabulary.md": leafPage({ title: "Motion Vocabulary", domain: "motion", sourcePath: "skills/animation-vocabulary/SKILL.md", parent: "index.md", next: "review-workflow.md" }), "motion/review-workflow.md": leafPage({ title: "Motion Review Workflow", domain: "motion", sourcePath: "skills/review-animations/SKILL.md", parent: "index.md", next: "practice-reference.md" }), "motion/practice-reference.md": leafPage({ title: "Motion Practice Reference", domain: "motion", sourcePath: "skills/review-animations/STANDARDS.md", parent: "index.md", next: "../design-engineering/index.md" }), - "design-engineering/index.md": indexPage("Design Engineering", [["Interface Craft", "interface-craft.md"]]), + "design-engineering/index.md": indexPage("Design Engineering", [["Interface Craft", "interface-craft.md"], ["Reference Profiles", "reference-profiles/index.md"]]), "design-engineering/interface-craft.md": leafPage({ title: "Interface Craft", domain: "design-engineering", sourcePath: "skills/emil-design-eng/SKILL.md", parent: "index.md", next: "../platform-guides/index.md" }), + "design-engineering/reference-profiles/index.md": "# Reference Profiles\n\nDomain classification: design-engineering.\n\n- [Governed Local Profiles](governed-local/index.md)\n\nParent: [Design Engineering](../index.md).\nNext: [External adaptation](external-adaptation/index.md).\n", + "design-engineering/reference-profiles/governed-local/index.md": "# Governed Local Profiles\n\nParent: [Reference Profiles](../index.md).\nNext: [External adaptation](../external-adaptation/index.md).\n", + "design-engineering/reference-profiles/external-adaptation/index.md": "# External Adaptation\n\nSynthetic validator coverage only; no durable adopter record.\n\nParent: [Reference Profiles](../index.md).\nNext: [Platform Guides](../../../platform-guides/index.md).\n", "platform-guides/index.md": indexPage("Platform Guides", [["Apple Interaction", "apple-interaction.md"]]), "platform-guides/apple-interaction.md": leafPage({ title: "Apple Interaction", domain: "platform-guides", sourcePath: "skills/apple-design/SKILL.md", parent: "index.md", next: "../layout/index.md" }), "quality/claim-records/stylegallery-multidomain-scope.md": "# Scope Decision\n\nStyleGallery supersedes the layout-only repository identity.\n", @@ -133,6 +136,7 @@ const cases = [ { name: "empty_manifest", mutate: ["DOMAINS.md", baseFiles["DOMAINS.md"], "# Empty manifest\n"], expect: "DOMAINS.md: missing canonical domain contract" }, { name: "manifest_extra_domain", mutate: ["DOMAINS.md", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |\n| Other | [Other](other/index.md) | `experimental` |"], expect: "DOMAINS.md: missing canonical domain contract" }, { name: "consumer_reference_fifth_domain", mutate: ["DOMAINS.md", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |\n| Consumer Reference | [Consumer Reference](consumer-reference/index.md) | `stable` |"], expect: "DOMAINS.md: missing canonical domain contract" }, + { name: "reference_profiles_fifth_domain", mutate: ["design-engineering/reference-profiles/index.md", "Domain classification: design-engineering.", "Domain classification: reference-profiles."], expect: "design-engineering/reference-profiles/index.md: reference profiles must remain in the Design Engineering domain" }, { name: "manifest_extra_leaf", mutate: ["DOMAINS.md", "`motion/vocabulary.md`,", "`motion/vocabulary.md`, `motion/ghost.md`,"], expect: "DOMAINS.md: missing canonical domain contract" }, { name: "manifest_wrong_lifecycle", mutate: ["DOMAINS.md", "| Motion | [Motion](motion/index.md) | `experimental` |", "| Motion | [Motion](motion/index.md) | `stable` |"], expect: "DOMAINS.md: missing canonical domain contract" }, { name: "missing_domain_index", omit: ["design-engineering/index.md"], expect: "design-engineering/index.md: missing file" }, diff --git a/scripts/test-validate-ia.mjs b/scripts/test-validate-ia.mjs index 342bd3c..74352ff 100644 --- a/scripts/test-validate-ia.mjs +++ b/scripts/test-validate-ia.mjs @@ -73,8 +73,11 @@ const baseFiles = { "layout/index.md": "# Layout\n\n- [Catalog](../CATALOG.md)\n", "motion/index.md": "# Motion\n\n- [Review](review.md)\n", "motion/review.md": leaf("Motion review", "index.md", "../design-engineering/index.md"), - "design-engineering/index.md": "# Design Engineering\n\n- [Craft](craft.md)\n", + "design-engineering/index.md": "# Design Engineering\n\n- [Craft](craft.md)\n- [Reference Profiles](reference-profiles/index.md)\n", "design-engineering/craft.md": leaf("Craft", "index.md", "../platform-guides/index.md"), + "design-engineering/reference-profiles/index.md": "# Reference Profiles\n\n- [Governed Local Profiles](governed-local/index.md)\n- [External Adaptation](external-adaptation/index.md)\n\nParent: [Design Engineering](../index.md).\nNext: [Governed Local Profiles](governed-local/index.md).\n", + "design-engineering/reference-profiles/governed-local/index.md": "# Governed Local Profiles\n\nParent: [Reference Profiles](../index.md).\nNext: [External Adaptation](../external-adaptation/index.md).\n", + "design-engineering/reference-profiles/external-adaptation/index.md": "# External Adaptation\n\nParent: [Reference Profiles](../index.md).\nNext: [Platform Guides](../../../platform-guides/index.md).\n", "platform-guides/index.md": "# Platform Guides\n\n- [Apple](apple.md)\n", "platform-guides/apple.md": leaf("Apple", "index.md", "../layout/index.md"), "consumer-reference/index.md": "# Consumer Reference\n\n- [Contract](contract.md)\n", @@ -131,6 +134,20 @@ const cases = [ }, expect: "index.md: missing [Design Engineering](design-engineering/index.md)", }, + { + name: "missing_reference_profile_route", + mutate: { + "design-engineering/index.md": "# Design Engineering\n\n- [Craft](craft.md)\n", + }, + expect: "design-engineering/index.md: missing [Reference Profiles](reference-profiles/index.md)", + }, + { + name: "missing_external_adaptation_parent", + mutate: { + "design-engineering/reference-profiles/external-adaptation/index.md": "# External Adaptation\n\nNext: [Platform Guides](../../../platform-guides/index.md).\n", + }, + expect: "design-engineering/reference-profiles/external-adaptation/index.md: missing Parent navigation link", + }, { name: "missing_consumer_reference_route", mutate: { diff --git a/scripts/test-validate-reference-profiles.mjs b/scripts/test-validate-reference-profiles.mjs new file mode 100644 index 0000000..2e5a605 --- /dev/null +++ b/scripts/test-validate-reference-profiles.mjs @@ -0,0 +1,164 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const validator = path.join(repositoryRoot, "scripts", "validate-consumer-reference.mjs"); +const profilePath = "design-engineering/reference-profiles/governed-local/editorial/profile.json"; +const terminalProfilePath = "design-engineering/reference-profiles/governed-local/terminal/profile.json"; +const aliasProfilePath = "design-engineering/reference-profiles/governed-local/alias/profile.json"; +const layoutSha = "775430bbaf4ee208a642220f440f6926d79c90a3"; + +const baseProfile = { + artifact_mode: "governed_local", + default: false, + environment_assumptions: { + reset: { body_margin: "0", box_sizing: "border-box", figure_margin: "0" }, + user_agent_styles: "Preserve browser defaults except for the declared reset.", + }, + example_only: true, + fixture_independence: "related", + handoff: { record: profilePath, status: "declared" }, + id: "editorial-reference-profile", + layout_source_sha: layoutSha, + local_foundations: "local-foundations.json", + maturity: "experimental", + owner: { enforcement: "placeholder", name: "Design Engineering owner" }, + profile_kind: "governed_local", + related_fixture_set_id: "layout-identity-adversarial-pair", + review_independence: "single_account", + schema_version: "1.0", + selection: { method: "profile_path", required: true }, + support: { status: "active" }, + tokens: "tokens.dtcg.json", +}; + +const tokens = { + color: { + $type: "color", + accent: { $value: { alpha: 1, colorSpace: "srgb", components: [0.55, 0.18, 0.1] } }, + }, + space: { $type: "dimension", page: { $value: { unit: "rem", value: 1.5 } } }, +}; +const terminalTokens = { + color: { + $type: "color", + accent: { $value: { alpha: 1, colorSpace: "srgb", components: [0.1, 0.9, 0.5] } }, + }, + space: { $type: "dimension", page: { $value: { unit: "rem", value: 1 } } }, +}; +const editorialFoundations = { bindings: { accent: "{color.accent}" }, identity: "Warm editorial", profile_id: "editorial-reference-profile" }; +const terminalFoundations = { bindings: { accent: "{color.accent}" }, identity: "Dark terminal", profile_id: "terminal-reference-profile" }; + +const cases = [ + { name: "valid_governed_local_profile" }, + { expect: "profile_artifact_mode_governed_local", name: "local_as_external", mutate: (item) => { item.artifact_mode = "external_consumer"; } }, + { expect: "profile_default_forbidden", name: "implicit_default", mutate: (item) => { item.default = true; } }, + { expect: "profile_layout_source_sha_required", name: "missing_layout_sha", mutate: (item) => { delete item.layout_source_sha; } }, + { expect: "profile_ua_assumption_required", name: "missing_ua_assumption", mutate: (item) => { delete item.environment_assumptions.user_agent_styles; } }, + { expect: "profile_reset_assumptions_required", name: "missing_reset_assumptions", mutate: (item) => { delete item.environment_assumptions.reset; } }, + { expect: "profile_explicit_selection_required", name: "implicit_selection", mutate: (item) => { item.selection.required = false; } }, + { expect: "fixture_independence_related", name: "independence_independent", mutate: (item) => { item.fixture_independence = "independent"; } }, + { expect: "reverse_import", name: "reverse_profile_import", patternData: "export const related_fixture_set_id = 'layout-identity-adversarial-pair';\n" }, + { expect: "external_adaptation_record_forbidden", externalRecord: true, name: "durable_external_adopter" }, + { equalTokens: true, expect: "profile_tokens_not_distinct", name: "single_profile_equal_pair_tokens" }, + { equalFoundations: true, expect: "profile_foundations_not_distinct", name: "single_profile_equal_pair_foundations" }, + { equalIdentity: true, expect: "profile_identity_not_distinct", name: "single_profile_equal_pair_identity" }, + { expect: "profile_related_set_mismatch", name: "single_profile_related_set_mismatch", mutateTerminal: (item) => { item.related_fixture_set_id = "unrelated-terminal-set"; } }, + { expect: "profile_tokens_symlink_escape", linkedArtifact: "tokens", name: "linked_outside_tokens" }, + { expect: "profile_foundations_symlink_escape", linkedArtifact: "foundations", name: "linked_outside_foundations" }, + { expect: "profile_environment_property_unknown", name: "unknown_environment_field", mutate: (item) => { item.environment_assumptions.viewport = "wide"; } }, + { expect: "profile_reset_property_unknown", name: "unknown_reset_field", mutate: (item) => { item.environment_assumptions.reset.list_margin = "0"; } }, + { expect: "profile_selection_property_unknown", name: "unknown_selection_field", mutate: (item) => { item.selection.fallback = "editorial"; } }, + { expect: "item_property_unknown", name: "unknown_profile_top_level", mutate: (item) => { item.profile_fallback = true; } }, + { + equalTokens: true, + expect: "profile_tokens_not_distinct", + name: "normalized_equivalent_selector_loads_pair", + selector: "design-engineering/reference-profiles/governed-local/editorial/../editorial/profile.json", + }, + { absoluteSelector: true, expect: "item_unresolved", name: "absolute_profile_selector_rejected" }, + { expect: "item_unresolved", name: "scheme_profile_selector_rejected", selector: "https://example.com/editorial/profile.json" }, + { expect: "item_unresolved", name: "network_profile_selector_rejected", selector: "//server/editorial/profile.json" }, + { expect: "item_unresolved", name: "escaping_profile_selector_rejected", selector: "../editorial/profile.json" }, + { aliasSelector: true, expect: "item_redirect", name: "symlink_alias_profile_selector_rejected" }, +]; + +function writeFixture(testCase) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `stylegallery-reference-profile-${testCase.name}-`)); + const profile = structuredClone(baseProfile); + const terminalProfile = structuredClone(baseProfile); + terminalProfile.handoff.record = terminalProfilePath; + terminalProfile.id = "terminal-reference-profile"; + testCase.mutate?.(profile); + testCase.mutateTerminal?.(terminalProfile); + const terminalFoundation = structuredClone(terminalFoundations); + if (testCase.equalIdentity) terminalFoundation.identity = editorialFoundations.identity; + const files = { + "CATALOG.md": "# Catalog\n", + [profilePath]: `${JSON.stringify(profile, null, 2)}\n`, + "design-engineering/reference-profiles/governed-local/editorial/local-foundations.json": `${JSON.stringify(editorialFoundations)}\n`, + "design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json": `${JSON.stringify(tokens, null, 2)}\n`, + [terminalProfilePath]: `${JSON.stringify(terminalProfile, null, 2)}\n`, + "design-engineering/reference-profiles/governed-local/terminal/local-foundations.json": `${JSON.stringify(testCase.equalFoundations ? editorialFoundations : terminalFoundation)}\n`, + "design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json": `${JSON.stringify(testCase.equalTokens ? tokens : terminalTokens, null, 2)}\n`, + "layout/index.md": "# Layout\n", + "patterns/index.md": "# Patterns\n", + "quality/handoff.md": "Implementation handoff:\nConsumer reference: not_applicable\nConsumer reference reason: This fixture has no consumer-specific reference record.\n", + "scripts/pattern-data.mjs": testCase.patternData ?? "export const patterns = [];\n", + }; + if (testCase.externalRecord) files["design-engineering/reference-profiles/external-adaptation/adopter.json"] = "{}\n"; + for (const [relative, content] of Object.entries(files)) { + const target = path.join(root, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content); + } + let externalRoot; + if (testCase.linkedArtifact) { + externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-reference-profile-outside-")); + const filename = testCase.linkedArtifact === "tokens" ? "tokens.dtcg.json" : "local-foundations.json"; + const target = path.join(root, path.posix.dirname(profilePath), filename); + const external = path.join(externalRoot, filename); + fs.writeFileSync(external, files[path.posix.join(path.posix.dirname(profilePath), filename)]); + fs.rmSync(target); + fs.symlinkSync(external, target); + } + let selector = testCase.selector ?? profilePath; + if (testCase.absoluteSelector) selector = path.join(root, profilePath); + if (testCase.aliasSelector) { + const alias = path.join(root, aliasProfilePath); + fs.mkdirSync(path.dirname(alias), { recursive: true }); + fs.symlinkSync(path.join(root, profilePath), alias); + selector = aliasProfilePath; + } + return { externalRoot, root, selector }; +} + +function runCase(testCase) { + const fixture = writeFixture(testCase); + try { + const child = spawnSync(process.execPath, [validator, "--profile", fixture.selector, "--json"], { cwd: fixture.root, encoding: "utf8" }); + const output = JSON.parse(child.stdout); + const codes = Array.isArray(output.failures) ? output.failures.map((failure) => failure.code) : []; + const accepted = child.status === 0 && output.ok === true && output.scaffold !== true; + const rejected = child.status !== 0 && output.ok === false && codes.includes(testCase.expect); + return { + actual: { codes, ok: output.ok, status: child.status }, + expected: testCase.expect ?? "ok:true and exit:0", + name: testCase.name, + ok: testCase.expect ? rejected : accepted, + }; + } finally { + fs.rmSync(fixture.root, { force: true, recursive: true }); + if (fixture.externalRoot) fs.rmSync(fixture.externalRoot, { force: true, recursive: true }); + } +} + +const results = cases.map(runCase); +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +console.log(JSON.stringify(report, null, 2)); +process.exit(report.ok ? 0 : 1); diff --git a/scripts/validate-consumer-reference.mjs b/scripts/validate-consumer-reference.mjs index 9cdc328..0bf454f 100644 --- a/scripts/validate-consumer-reference.mjs +++ b/scripts/validate-consumer-reference.mjs @@ -3,22 +3,43 @@ import fs from "node:fs"; import path from "node:path"; import { isPlainObject, validateItemSchema } from "./consumer-reference-schema.mjs"; +import { validateReferenceProfile, validateReferenceProfileSet } from "./reference-profile-contract.mjs"; const root = process.cwd(); const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "item.schema.json"), "utf8")); const failures = []; const warnings = []; +const canonicalProfilePaths = [ + "design-engineering/reference-profiles/governed-local/editorial/profile.json", + "design-engineering/reference-profiles/governed-local/terminal/profile.json", +]; const defaultItems = [ "consumer-reference/fixtures/valid-experimental.json", "consumer-reference/fixtures/valid-deprecated.json", + ...canonicalProfilePaths, ]; -const reverseMarkers = ["consumerreference", "designengineeringreferenceprofiles", "profilejson", "tokensdtcgjson"]; +const reverseMarkers = ["consumerreference", "designengineeringreferenceprofiles", "profilejson", "tokensdtcgjson", "relatedfixturesetid"]; +const profileSummaries = []; function addFailure(code, relative, message) { failures.push({ code, message, path: relative }); } +function canonicalProfileIdentity(selected) { + if (selected.startsWith("//") + || selected.startsWith("\\\\") + || path.posix.isAbsolute(selected) + || path.win32.isAbsolute(selected) + || /^[A-Za-z][A-Za-z\d+.-]*:/.test(selected) + || selected.includes("\\") + || selected.includes("?") + || selected.includes("#")) return null; + const normalized = path.posix.normalize(selected); + if (normalized === ".." || normalized.startsWith("../") || !isInside(root, path.resolve(root, normalized))) return null; + return canonicalProfilePaths.includes(normalized) ? normalized : null; +} + function isInside(base, target) { const relative = path.relative(base, target); return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); @@ -45,7 +66,10 @@ function parseArguments() { } addFailure("argument_unknown", "", `unsupported argument ${argument}`); } - return { items: items.length > 0 ? items : defaultItems, json }; + const selected = items.length > 0 ? items : defaultItems; + const canonicalized = selected.map((item) => canonicalProfileIdentity(item) ?? item); + const requiresCanonicalPair = canonicalized.some((item) => canonicalProfilePaths.includes(item)); + return { items: requiresCanonicalPair ? [...new Set([...canonicalized, ...canonicalProfilePaths])] : canonicalized, json }; } function validateRecordPath(record, itemPath) { @@ -140,6 +164,11 @@ function validateItem(relative) { for (const finding of validateItemSchema(item, schema)) addFailure(finding.code, relative, finding.message); if (!isPlainObject(item)) return; validateHandoff(item, relative); + if (/^design-engineering\/reference-profiles\/governed-local\/[^/]+\/profile\.json$/.test(relative)) { + const result = validateReferenceProfile({ item, relative, root }); + for (const finding of result.failures) addFailure(finding.code, finding.path, finding.message); + if (result.summary) profileSummaries.push(result.summary); + } } function walkFiles(relative) { @@ -162,6 +191,13 @@ function rejectReverseImports() { } } +function rejectExternalAdaptationRecords() { + const directory = "design-engineering/reference-profiles/external-adaptation"; + for (const relative of walkFiles(directory).filter((file) => file.endsWith(".json"))) { + addFailure("external_adaptation_record_forbidden", relative, "external adaptation has documentation and synthetic validator coverage only"); + } +} + function isSentence(reason) { const variants = schema.properties.handoff.oneOf; const definition = variants.find((candidate) => candidate.properties.status.const === "not_applicable").properties.reason; @@ -193,10 +229,12 @@ function validateHandoffCoverage() { const options = parseArguments(); for (const item of options.items) validateItem(item); rejectReverseImports(); +rejectExternalAdaptationRecords(); +for (const finding of validateReferenceProfileSet(profileSummaries)) addFailure(finding.code, finding.path, finding.message); const checkedHandoffs = validateHandoffCoverage(); const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()]; -const result = { checkedHandoffs, checkedItems: options.items.length, failures: uniqueFailures, ok: uniqueFailures.length === 0, warnings }; +const result = { checkedHandoffs, checkedItems: options.items.length, failures: uniqueFailures, ok: uniqueFailures.length === 0, profiles: profileSummaries, warnings }; if (options.json) console.log(JSON.stringify(result, null, 2)); else if (result.ok) console.log(`ok: ${result.checkedItems} consumer reference items`); else console.error(result.failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")); diff --git a/scripts/validate-domains.mjs b/scripts/validate-domains.mjs index 33300a9..424fa3d 100644 --- a/scripts/validate-domains.mjs +++ b/scripts/validate-domains.mjs @@ -11,6 +11,11 @@ const failures = []; const repository = "https://github.com/emilkowalski/skills"; const revision = "220e8607c90b17337d210125777b7b695f26c221"; const revisionPattern = /^[0-9a-f]{40}$/; +const referenceDocuments = [ + "design-engineering/reference-profiles/index.md", + "design-engineering/reference-profiles/governed-local/index.md", + "design-engineering/reference-profiles/external-adaptation/index.md", +]; const domains = [ { slug: "layout", label: "Layout", leaves: [] }, @@ -23,7 +28,12 @@ const domains = [ ["motion/practice-reference.md", "skills/review-animations/STANDARDS.md"], ], }, - { slug: "design-engineering", label: "Design Engineering", leaves: [["design-engineering/interface-craft.md", "skills/emil-design-eng/SKILL.md"]] }, + { + slug: "design-engineering", + label: "Design Engineering", + leaves: [["design-engineering/interface-craft.md", "skills/emil-design-eng/SKILL.md"]], + referenceDocuments, + }, { slug: "platform-guides", label: "Platform Guides", leaves: [["platform-guides/apple-interaction.md", "skills/apple-design/SKILL.md"]] }, ]; @@ -109,7 +119,7 @@ function checkManifest() { if (!pageRow || pageRow[1] !== expectedManualHub) valid = false; if (domain.leaves.length > 0 && pageRow) { const declaredLeaves = [...pageRow[2].matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]).sort(); - const expectedLeaves = domain.leaves.map(([leaf]) => leaf).sort(); + const expectedLeaves = [...domain.leaves.map(([leaf]) => leaf), ...(domain.referenceDocuments ?? [])].sort(); if (JSON.stringify(declaredLeaves) !== JSON.stringify(expectedLeaves)) valid = false; } } @@ -134,6 +144,9 @@ function checkIndex(domain) { failures.push(`${relative}: missing leaf route ${target}`); } } + if (domain.referenceDocuments?.length > 0 && !content.includes("[Reference Profiles](reference-profiles/index.md)")) { + failures.push(`${relative}: missing reference profile route`); + } } function checkLeaf(domain, relative, expectedSourcePath, titles) { @@ -172,7 +185,7 @@ function checkLeaf(domain, relative, expectedSourcePath, titles) { function rejectUndeclaredDomainDocuments() { for (const domain of domains) { - const declared = new Set([`${domain.slug}/index.md`, ...domain.leaves.map(([leaf]) => leaf)]); + const declared = new Set([`${domain.slug}/index.md`, ...domain.leaves.map(([leaf]) => leaf), ...(domain.referenceDocuments ?? [])]); for (const absolute of walkMarkdown(path.join(root, domain.slug))) { const relative = path.relative(root, absolute); if (!declared.has(relative)) failures.push(`${relative}: undeclared governed domain document`); @@ -198,6 +211,20 @@ function rejectOmoDependencies() { } } +function checkReferenceDocuments() { + const index = stripFencedCodeBlocks(read(referenceDocuments[0])); + if (!/Domain classification:\s*design-engineering\./i.test(index)) { + failures.push(`${referenceDocuments[0]}: reference profiles must remain in the Design Engineering domain`); + } + if (!index.includes("[Governed Local Profiles](governed-local/index.md)")) { + failures.push(`${referenceDocuments[0]}: missing governed-local route`); + } + const external = stripFencedCodeBlocks(read(referenceDocuments[2])); + if (!external.includes("Synthetic validator coverage only") || !external.includes("no durable adopter record")) { + failures.push(`${referenceDocuments[1]}: external adaptation must remain documentation-only`); + } +} + checkManifest(); read("quality/claim-records/stylegallery-multidomain-scope.md"); requireRootRoutes(); @@ -212,6 +239,7 @@ for (const domain of domains) { } rejectUndeclaredDomainDocuments(); rejectOmoDependencies(); +checkReferenceDocuments(); const result = { ok: failures.length === 0, checkedDomains: domains.length, checkedLeaves, failures: [...new Set(failures)] }; if (json) console.log(JSON.stringify(result, null, 2)); diff --git a/scripts/validate-ia.mjs b/scripts/validate-ia.mjs index e21b3b9..a031cb4 100644 --- a/scripts/validate-ia.mjs +++ b/scripts/validate-ia.mjs @@ -22,6 +22,11 @@ const domainRoutes = [ ["Platform Guides", "platform-guides/index.md"], ]; const leafDirectories = ["patterns", "recipes", "quality", "motion", "design-engineering", "platform-guides", "consumer-reference"]; +const nestedIndexes = [ + "design-engineering/reference-profiles/index.md", + "design-engineering/reference-profiles/governed-local/index.md", + "design-engineering/reference-profiles/external-adaptation/index.md", +]; function read(relative) { const target = path.join(root, relative); @@ -60,6 +65,9 @@ function requireRootRoles() { requireIncludes("quality/index.md", "## Tree-Test Findability QA"); requireIncludes("README.md", "[Consumer Reference](consumer-reference/index.md)"); requireIncludes("index.md", "[Consumer reference](consumer-reference/index.md)"); + requireIncludes("design-engineering/index.md", "[Reference Profiles](reference-profiles/index.md)"); + requireIncludes("design-engineering/reference-profiles/index.md", "[External Adaptation](external-adaptation/index.md)"); + requireIncludes("design-engineering/reference-profiles/index.md", "[Governed Local Profiles](governed-local/index.md)"); } function requireTaskRoutes() { @@ -76,7 +84,7 @@ function requireDomainRoutes() { } function requireLeafNavigation() { - for (const file of leafDirectories.flatMap(walk)) { + for (const file of [...leafDirectories.flatMap(walk), ...nestedIndexes]) { const content = stripFencedCodeBlocks(read(file)); if (!/^Parent: \[[^\]]+\]\([^)]+\)/m.test(content)) failures.push(`${file}: missing Parent navigation link`); if (!/^Next: \[[^\]]+\]\([^)]+\)/m.test(content)) failures.push(`${file}: missing Next navigation link`); @@ -89,7 +97,7 @@ requireDomainRoutes(); requireLeafNavigation(); const result = { - checkedLeafFiles: leafDirectories.flatMap(walk).length, + checkedLeafFiles: leafDirectories.flatMap(walk).length + nestedIndexes.length, failures, ok: failures.length === 0, }; From 7422d31c9a69d172a88efde20529a5211ecebdbd Mon Sep 17 00:00:00 2001 From: victor Date: Mon, 13 Jul 2026 23:23:44 +0900 Subject: [PATCH 03/44] Add calibrated Chromium reference sentinel --- .github/CODEOWNERS | 3 + .github/workflows/validate.yml | 124 ++++++++++++ .gitignore | 3 + GOVERNANCE.md | 21 ++- consumer-reference/baselines/calibration.json | 23 +++ consumer-reference/baselines/manifest.json | 21 +++ consumer-reference/contract.md | 4 + consumer-reference/index.md | 3 + .../schema/baseline-manifest.schema.json | 55 ++++++ .../schema/calibration-record.schema.json | 139 ++++++++++++++ package-lock.json | 125 ++++++++++++ package.json | 5 + playwright.config.mjs | 22 +++ quality/evidence/executable-evidence.md | 1 + scripts/baseline-contract.mjs | 153 +++++++++++++++ scripts/calibration-raw-contract.mjs | 172 +++++++++++++++++ scripts/strict-json.mjs | 72 +++++++ scripts/summarize-sentinel-calibration.mjs | 96 ++++++++++ scripts/test-consumer-reference-sentinel.mjs | 30 +++ scripts/test-renderer-purity.mjs | 33 ++++ .../test-summarize-sentinel-calibration.mjs | 178 ++++++++++++++++++ scripts/test-validate-baseline-manifest.mjs | 151 +++++++++++++++ scripts/test-validate-governance.mjs | 34 ++++ scripts/validate-baseline-manifest.mjs | 43 +++++ scripts/validate-governance.mjs | 25 +++ scripts/validate-renderer-purity.mjs | 56 ++++++ tests/consumer-reference-sentinels.spec.mjs | 100 ++++++++++ tests/helpers/render-consumer-reference.mjs | 36 ++++ .../consumer-reference-card-grid.png | Bin 0 -> 713 bytes 29 files changed, 1727 insertions(+), 1 deletion(-) create mode 100644 consumer-reference/baselines/calibration.json create mode 100644 consumer-reference/baselines/manifest.json create mode 100644 consumer-reference/schema/baseline-manifest.schema.json create mode 100644 consumer-reference/schema/calibration-record.schema.json create mode 100644 playwright.config.mjs create mode 100644 scripts/baseline-contract.mjs create mode 100644 scripts/calibration-raw-contract.mjs create mode 100644 scripts/strict-json.mjs create mode 100644 scripts/summarize-sentinel-calibration.mjs create mode 100644 scripts/test-consumer-reference-sentinel.mjs create mode 100644 scripts/test-renderer-purity.mjs create mode 100644 scripts/test-summarize-sentinel-calibration.mjs create mode 100644 scripts/test-validate-baseline-manifest.mjs create mode 100644 scripts/validate-baseline-manifest.mjs create mode 100644 scripts/validate-renderer-purity.mjs create mode 100644 tests/consumer-reference-sentinels.spec.mjs create mode 100644 tests/helpers/render-consumer-reference.mjs create mode 100644 tests/snapshots/consumer-reference-card-grid.png diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0b75b97..d398067 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,6 +16,9 @@ /consumer-reference/ @changeroa /consumer-reference/adapters/ @changeroa /consumer-reference/generated/ @changeroa +/consumer-reference/baselines/ @changeroa +/tests/ @changeroa +/playwright.config.mjs @changeroa /GUIDE.md @changeroa /guides/ @changeroa diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7efb6ce..1c6137f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,6 +1,7 @@ name: Validate StyleGallery on: + workflow_dispatch: pull_request: push: branches: @@ -9,6 +10,10 @@ on: permissions: contents: read +concurrency: + group: stylegallery-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: validate: name: Validate StyleGallery contracts @@ -52,6 +57,21 @@ jobs: node -c scripts/build-reference-artifacts.mjs node -c scripts/validate-reference-artifacts.mjs node -c scripts/test-reference-adapters.mjs + node -c playwright.config.mjs + node -c tests/helpers/render-consumer-reference.mjs + node -c tests/consumer-reference-sentinels.spec.mjs + node -c scripts/baseline-contract.mjs + node -c scripts/strict-json.mjs + node -c scripts/calibration-raw-contract.mjs + node -c scripts/validate-baseline-manifest.mjs + node -c scripts/test-validate-baseline-manifest.mjs + node -c scripts/summarize-sentinel-calibration.mjs + node -c scripts/test-summarize-sentinel-calibration.mjs + node -c scripts/validate-renderer-purity.mjs + node -c scripts/test-renderer-purity.mjs + node -c scripts/test-consumer-reference-sentinel.mjs + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/baseline-manifest.schema.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/calibration-record.schema.json"))' git diff --check - name: Generated artifacts are current @@ -102,6 +122,14 @@ jobs: node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json node scripts/test-reference-adapters.mjs --json + - name: Chromium baseline manifest integrity + run: | + node scripts/validate-baseline-manifest.mjs --json + node scripts/test-validate-baseline-manifest.mjs --json + node scripts/test-summarize-sentinel-calibration.mjs + node scripts/validate-renderer-purity.mjs --json + node scripts/test-renderer-purity.mjs + - name: Webpage workflow check run: | node scripts/validate-webpage-workflow.mjs --json @@ -112,3 +140,99 @@ jobs: - name: Pattern adversarial fixtures run: node scripts/test-validate-patterns.mjs --json + + chromium-sentinel: + name: Chromium reference sentinel (nonblocking) + runs-on: ubuntu-24.04 + continue-on-error: true + container: + image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a + options: --platform linux/amd64 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install exact sentinel dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Run ordinary immutable Chromium sentinel + run: npm run test:sentinel -- --reporter=line + + - name: Run Chromium sentinel semantic negative controls + run: node scripts/test-consumer-reference-sentinel.mjs + + chromium-calibration: + name: Chromium 20-run calibration (nonblocking) + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + continue-on-error: true + container: + image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a + options: --platform linux/amd64 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install exact calibration dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Capture 20 unique raw calibration runs + shell: bash + env: + SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a + run: | + set +e + failed=0 + mkdir -p calibration-artifacts + artifact_name="chromium-sentinel-calibration-${GITHUB_RUN_ID}-${GITHUB_SHA}" + checkout_sha="$(git rev-parse HEAD)" + for run in $(seq 1 20); do + run_dir="calibration-artifacts/run-$(printf '%02d' "$run")" + mkdir -p "$run_dir" + SENTINEL_ARTIFACT_DIR="$run_dir" SENTINEL_CALIBRATION_RUN="$run" \ + npx playwright test tests/consumer-reference-sentinels.spec.mjs \ + --project=chromium --grep "canonical consumer reference" --reporter=json \ + > "$run_dir/playwright.json" 2> "$run_dir/playwright.stderr" + test_exit=$? + if [ "$test_exit" -ne 0 ]; then failed=1; fi + printf '{"schema_version":"1.0","run":%s,"exit_code":%s}\n' "$run" "$test_exit" > "$run_dir/exit.json" + done + validation_output="${RUNNER_TEMP}/chromium-calibration-validation-${GITHUB_RUN_ID}.json" + node scripts/summarize-sentinel-calibration.mjs \ + --input calibration-artifacts \ + --output calibration-artifacts/calibration.json \ + --repository "$GITHUB_REPOSITORY" \ + --workflow ".github/workflows/validate.yml" \ + --run-id "$GITHUB_RUN_ID" \ + --run-attempt "$GITHUB_RUN_ATTEMPT" \ + --sha "$GITHUB_SHA" \ + --checkout-sha "$checkout_sha" \ + --head-sha "${{ github.event.pull_request.head.sha || github.sha }}" \ + --artifact-name "$artifact_name" \ + --json > "$validation_output" + summary_exit=$? + mv "$validation_output" calibration-artifacts/calibration-validation.json + printf '{"run_id":"%s","sha":"%s","artifact_name":"%s","platform":"linux/amd64","architecture":"amd64","container_image":"%s"}\n' \ + "$GITHUB_RUN_ID" "$GITHUB_SHA" "$artifact_name" "$SENTINEL_CONTAINER_IMAGE" \ + > calibration-artifacts/workflow-metadata.json + if [ "$summary_exit" -ne 0 ]; then failed=1; fi + exit "$failed" + + - name: Upload raw Chromium calibration evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: chromium-sentinel-calibration-${{ github.run_id }}-${{ github.sha }} + path: calibration-artifacts/ + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index c2658d7..dc25d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ node_modules/ +playwright-report/ +test-results/ +blob-report/ diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 7dfb1fa..2c4f7fa 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -21,9 +21,10 @@ Use this file before editing repository documentation. It names which file is au | Planning guides | `GUIDE.md`, `guides/*.md` | Manual | None | `stable` | Workflow changes, route changes, source-lineage changes, or broken guide links. | `scripts/validate-okf.mjs`, `scripts/validate-links.mjs`, `scripts/validate-ia.mjs` | Planning-doc owner | | Layout recipes | `recipes/*.md` | Manual | None | `stable` | Pattern-stack changes, route changes, or broken recipe links. | `scripts/validate-okf.mjs`, `scripts/validate-links.mjs`, `scripts/validate-ia.mjs` | Recipe owner | | Quality gates and evidence | `quality/**/*.md` | Manual | None | `stable` | Claim-boundary changes, evidence-family changes, or broken quality links. | `scripts/validate-okf.mjs`, `scripts/validate-links.mjs`, `scripts/validate-ia.mjs` | Quality owner | -| Consumer reference contract | `consumer-reference/contract.md`, `consumer-reference/schema/*.json` | Manual | None | `stable` contract with related fixtures | Handoff shape, path boundary, lifecycle, ownership, or dependency-direction changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Repository governance owner with Validation owner | +| Consumer reference contract | `consumer-reference/contract.md`, `consumer-reference/schema/item.schema.json` | Manual | None | `stable` contract with related fixtures | Handoff shape, path boundary, lifecycle, ownership, or dependency-direction changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Repository governance owner with Validation owner | | Portable token source | `consumer-reference/fixtures/token-portability/valid-reference.json`, `consumer-reference/schema/portable-tokens.schema.json` | `scripts/build-reference-artifacts.mjs` with Style Dictionary `5.5.0` | `consumer-reference/generated/tokens.css`, `consumer-reference/generated/manifest.json` | `stable` restricted adapter contract, `generated` output | Allowed token shape, adapter/version pin, source token count, warning, declaration, or content hash changes. | `scripts/validate-reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | | Governed local reference profiles | `design-engineering/reference-profiles/governed-local/**` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Layout revision, identity values, UA/reset assumptions, explicit selection, or fixture relationship changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Design Engineering owner with Validation owner | +| Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/{baseline-manifest,calibration-record}.schema.json`, `consumer-reference/baselines/*.json` | Playwright `1.61.0` in the digest-pinned `linux/amd64` image | `tests/snapshots/consumer-reference-card-grid.png`, raw GitHub Actions calibration artifact | `experimental`, nonblocking while owner approval is pending | Renderer source, exact image/platform/tool pin, baseline bytes, computed layout contract, or calibration metadata changes. | `scripts/validate-baseline-manifest.mjs`, `scripts/test-validate-baseline-manifest.mjs`, `scripts/test-summarize-sentinel-calibration.mjs`, Playwright | Repository governance owner with Validation owner | | Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner | | Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner | | Motion domain guidance | `motion/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Motion domain owner | @@ -52,6 +53,7 @@ Current generated artifacts: - `patterns/**/*.md` - `consumer-reference/generated/tokens.css` - `consumer-reference/generated/manifest.json` +- `tests/snapshots/consumer-reference-card-grid.png` (proposed; update only by an explicit local baseline proposal, never in CI) Portable token artifacts are regenerated only from the restricted fixture through the pinned adapter. Run `npm run build`; never broaden the allowed token subset to accommodate an adapter false-success, and revert the adapter with both generated files if the pin regresses. @@ -92,6 +94,7 @@ Consumer-reference ownership records the current truth as `owner.enforcement: "p | `motion/**` | Motion domain owner | Motion terminology, review procedure, practice classification, and evidence boundaries. | | `design-engineering/**` | Design Engineering domain owner | Separation of product heuristics from shared quality gates. | | `design-engineering/reference-profiles/**` | Design Engineering owner with Validation owner | Profile-local identity and values, pinned Layout provenance, explicit non-default selection, and related-fixture truth. | +| `tests/**`, `playwright.config.mjs`, `consumer-reference/baselines/**`, `scripts/*baseline*.mjs`, `scripts/*sentinel*.mjs`, `scripts/*renderer-purity.mjs` | Repository governance owner with Validation owner | Pure rendering, computed semantics before screenshots, immutable browser pins, nonblocking status, calibration cardinality, and pending owner approval. | | `platform-guides/**` | Platform Guides domain owner | Platform/source/version limits, comparison boundaries, and stale review. | | `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs`, `.github/workflows/validate.yml` | Validation owner | Validator scope, negative fixtures, CI parity. | @@ -139,3 +142,19 @@ node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/gene node scripts/test-reference-adapters.mjs --json git diff --exit-code -- consumer-reference/generated ``` + +For the proposed Chromium sentinel, also run: + +```sh +npm run test:sentinel +node scripts/test-consumer-reference-sentinel.mjs +node scripts/validate-baseline-manifest.mjs --json +node scripts/test-validate-baseline-manifest.mjs --json +node scripts/test-summarize-sentinel-calibration.mjs +node scripts/validate-renderer-purity.mjs --json +node scripts/test-renderer-purity.mjs +``` + +Do not pass `--update-snapshots` in CI. Calibration runs exactly 20 times on the manifest's digest-pinned `linux/amd64` container and uploads raw Playwright JSON, strict exit records, PNG, DOM, AX, metadata, and post-assertion comparison evidence. Failed or incomplete calibration still uploads its truthful raw evidence without writing a completed aggregate. It remains nonblocking until the named owner explicitly approves the baseline. + +Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. diff --git a/consumer-reference/baselines/calibration.json b/consumer-reference/baselines/calibration.json new file mode 100644 index 0000000..72f18b0 --- /dev/null +++ b/consumer-reference/baselines/calibration.json @@ -0,0 +1,23 @@ +{ + "schema_version": "1.0", + "status": "awaiting_committed_ci", + "baseline_owner_approval": "pending", + "required_runs": 20, + "committed_ci": null, + "environment": { + "platform": "linux/amd64", + "architecture": "amd64", + "container_image": "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + "node": "22", + "os": "Ubuntu 24.04", + "playwright": "1.61.0", + "browser_revision": "1228", + "browser_version": "149.0.7827.55", + "viewport": { "width": 1024, "height": 768 } + }, + "reference": { + "source": { "path": "tests/helpers/render-consumer-reference.mjs", "sha256": "88802a948909d5e40470be6b5481766ce2de498e59c053ac68af370b46e72ca9" }, + "baseline": { "path": "tests/snapshots/consumer-reference-card-grid.png", "sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" } + }, + "runs": [] +} diff --git a/consumer-reference/baselines/manifest.json b/consumer-reference/baselines/manifest.json new file mode 100644 index 0000000..fc161ce --- /dev/null +++ b/consumer-reference/baselines/manifest.json @@ -0,0 +1,21 @@ +{ + "schema_version": "1.0", + "status": "proposed", + "baseline_owner_approval": "pending", + "platform": "linux/amd64", + "architecture": "amd64", + "container_image": "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + "node": "22", + "os": "Ubuntu 24.04", + "playwright": "1.61.0", + "browser_revision": "1228", + "browser_version": "149.0.7827.55", + "viewport": { "width": 1024, "height": 768 }, + "source": { "path": "tests/helpers/render-consumer-reference.mjs", "sha256": "88802a948909d5e40470be6b5481766ce2de498e59c053ac68af370b46e72ca9" }, + "baseline": { "path": "tests/snapshots/consumer-reference-card-grid.png", "sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" }, + "calibration": { + "record": "consumer-reference/baselines/calibration.json", + "required_runs": 20, + "workflow_artifact_name": "chromium-sentinel-calibration-${run_id}-${sha}" + } +} diff --git a/consumer-reference/contract.md b/consumer-reference/contract.md index f4fed22..99601c2 100644 --- a/consumer-reference/contract.md +++ b/consumer-reference/contract.md @@ -48,6 +48,10 @@ Consumer-reference infrastructure is owned by repository governance and validati Dependency flows from a consumer or profile to Layout. `layout/**`, `patterns/**`, `scripts/pattern-data.mjs`, and `CATALOG.md` must not import consumer-reference records, profile data, tokens, decorative values, or themes. +The Chromium reference sentinel is an evidence probe, not a Layout theme or product conformance claim. Its proposed baseline is compared in the immutable `linux/amd64` Playwright image, CI never updates snapshots, and the ordinary sentinel remains nonblocking while `baseline_owner_approval` is `pending`. Twenty identical committed-workflow repeats calibrate repeatability; they do not substitute for explicit `@changeroa` baseline approval. Each aggregate run is derived from a zero exit record, the exact passing Playwright test identity, hash-verified PNG/DOM/AX bytes, and comparator proof written only after the zero-diff assertion passes; producer metadata cannot claim completion or a diff. + +Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. + The no-dependency validator blocks direct spellings and literal fragments assembled through concatenation, template literals, or `path.join`. This bounded static check does not claim general JavaScript data-flow analysis; guarded sources must not hide reference paths behind variables or runtime computation. ## IA Navigation diff --git a/consumer-reference/index.md b/consumer-reference/index.md index 540718d..820baed 100644 --- a/consumer-reference/index.md +++ b/consumer-reference/index.md @@ -14,6 +14,9 @@ Out of scope: profiles, themes, components, browser runtime, decorative styling, - [Receiver contract](contract.md) - Required handoff shape, record-path boundary, lifecycle fields, ownership truth, and dependency direction. - [Consumer reference item schema](schema/item.schema.json) - Machine-readable shape for the current receiver item. +- [Chromium calibration schema](schema/calibration-record.schema.json) - Immutable environment, repetition, and evidence-cardinality contract for the proposed nonblocking sentinel. +- [Chromium baseline manifest schema](schema/baseline-manifest.schema.json) - Exact proposed source, baseline, platform, and calibration routing contract. +- `baselines/` - Proposed Chromium geometry baseline metadata and a pending committed-CI calibration record; these files do not constitute baseline-owner approval. - `fixtures/` - Related validator fixtures; they are not adopters or product references. ## IA Navigation diff --git a/consumer-reference/schema/baseline-manifest.schema.json b/consumer-reference/schema/baseline-manifest.schema.json new file mode 100644 index 0000000..2511ead --- /dev/null +++ b/consumer-reference/schema/baseline-manifest.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stylegallery.local/consumer-reference/schema/baseline-manifest.schema.json", + "title": "Proposed Chromium baseline manifest", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "status", "baseline_owner_approval", "platform", "architecture", "container_image", "node", "os", "playwright", "browser_revision", "browser_version", "viewport", "source", "baseline", "calibration"], + "properties": { + "schema_version": { "const": "1.0" }, + "status": { "const": "proposed" }, + "baseline_owner_approval": { "const": "pending" }, + "platform": { "const": "linux/amd64" }, + "architecture": { "const": "amd64" }, + "container_image": { "const": "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a" }, + "node": { "const": "22" }, + "os": { "const": "Ubuntu 24.04" }, + "playwright": { "const": "1.61.0" }, + "browser_revision": { "const": "1228" }, + "browser_version": { "const": "149.0.7827.55" }, + "viewport": { + "type": "object", + "additionalProperties": false, + "required": ["width", "height"], + "properties": { "width": { "const": 1024 }, "height": { "const": 768 } } + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": { "const": "tests/helpers/render-consumer-reference.mjs" }, + "sha256": { "const": "88802a948909d5e40470be6b5481766ce2de498e59c053ac68af370b46e72ca9" } + } + }, + "baseline": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": { "const": "tests/snapshots/consumer-reference-card-grid.png" }, + "sha256": { "const": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" } + } + }, + "calibration": { + "type": "object", + "additionalProperties": false, + "required": ["record", "required_runs", "workflow_artifact_name"], + "properties": { + "record": { "const": "consumer-reference/baselines/calibration.json" }, + "required_runs": { "const": 20 }, + "workflow_artifact_name": { "const": "chromium-sentinel-calibration-${run_id}-${sha}" } + } + } + } +} diff --git a/consumer-reference/schema/calibration-record.schema.json b/consumer-reference/schema/calibration-record.schema.json new file mode 100644 index 0000000..8197ec6 --- /dev/null +++ b/consumer-reference/schema/calibration-record.schema.json @@ -0,0 +1,139 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stylegallery.local/consumer-reference/schema/calibration-record.schema.json", + "title": "Chromium sentinel calibration record", + "$comment": "This schema owns recursive shape and exact scalar identity. scripts/baseline-contract.mjs additionally enforces committed_ci tuple correlation and cross-run DOM/AX/hash equality.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "status", "baseline_owner_approval", "required_runs", "committed_ci", "environment", "reference", "runs"], + "properties": { + "schema_version": { "const": "1.0" }, + "status": { "enum": ["awaiting_committed_ci", "completed"] }, + "baseline_owner_approval": { "const": "pending" }, + "required_runs": { "const": 20 }, + "committed_ci": { + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": ["artifact_name", "checkout_sha", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"], + "properties": { + "run_id": { "type": "string", "pattern": "^[0-9]+$" }, + "run_attempt": { "type": "string", "pattern": "^[0-9]+$" }, + "sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "checkout_sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "head_sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "raw_evidence_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "repository": { "const": "changeroa/StyleGallery" }, + "workflow": { "const": ".github/workflows/validate.yml" }, + "artifact_name": { "type": "string", "pattern": "^chromium-sentinel-calibration-[0-9]+-[0-9a-f]{40}$" } + } + } + ] + }, + "environment": { + "type": "object", + "additionalProperties": false, + "required": ["platform", "architecture", "container_image", "node", "os", "playwright", "browser_revision", "browser_version", "viewport"], + "properties": { + "platform": { "const": "linux/amd64" }, + "architecture": { "const": "amd64" }, + "container_image": { "const": "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a" }, + "node": { "const": "22" }, + "os": { "const": "Ubuntu 24.04" }, + "playwright": { "const": "1.61.0" }, + "browser_revision": { "const": "1228" }, + "browser_version": { "const": "149.0.7827.55" }, + "viewport": { + "type": "object", + "additionalProperties": false, + "required": ["width", "height"], + "properties": { "width": { "const": 1024 }, "height": { "const": 768 } } + } + } + }, + "reference": { + "type": "object", + "additionalProperties": false, + "required": ["source", "baseline"], + "properties": { + "source": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": { "const": "tests/helpers/render-consumer-reference.mjs" }, + "sha256": { "const": "88802a948909d5e40470be6b5481766ce2de498e59c053ac68af370b46e72ca9" } + } + }, + "baseline": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": { "const": "tests/snapshots/consumer-reference-card-grid.png" }, + "sha256": { "const": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" } + } + } + } + }, + "runs": { + "type": "array", + "maxItems": 20, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["run", "completed", "architecture", "png_sha256", "dom_sha256", "ax_sha256", "metadata_sha256", "screenshot_diff_pixels"], + "properties": { + "run": { "type": "integer", "minimum": 1, "maximum": 20 }, + "completed": { "const": true }, + "architecture": { "const": "amd64" }, + "png_sha256": { "const": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" }, + "dom_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "ax_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "metadata_sha256": { "const": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c" }, + "screenshot_diff_pixels": { "const": 0 } + } + } + } + }, + "allOf": [ + { + "if": { "properties": { "status": { "const": "awaiting_committed_ci" } } }, + "then": { "properties": { "committed_ci": { "type": "null" }, "runs": { "maxItems": 0 } } }, + "else": { + "properties": { + "committed_ci": { "type": "object" }, + "runs": { + "minItems": 20, + "maxItems": 20, + "allOf": [ + { "contains": { "required": ["run"], "properties": { "run": { "const": 1 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 2 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 3 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 4 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 5 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 6 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 7 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 8 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 9 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 10 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 11 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 12 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 13 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 14 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 15 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 16 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 17 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 18 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 19 } } }, "minContains": 1, "maxContains": 1 }, + { "contains": { "required": ["run"], "properties": { "run": { "const": 20 } } }, "minContains": 1, "maxContains": 1 } + ] + } + } + } + } + ] +} diff --git a/package-lock.json b/package-lock.json index 0eb15a1..2f7c7c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "dependencies": { "style-dictionary": "5.5.0" }, + "devDependencies": { + "@playwright/test": "1.61.0", + "ajv": "8.20.0" + }, "engines": { "node": ">=22" } @@ -461,6 +465,22 @@ "tslib": "2" } }, + "node_modules/@playwright/test": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@zip.js/zip.js": { "version": "2.8.26", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", @@ -472,6 +492,23 @@ "node": ">=18.0.0" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -753,6 +790,30 @@ "node": ">=0.8.x" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -768,6 +829,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1062,6 +1138,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -1249,6 +1332,38 @@ "inherits": "2.0.3" } }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -1304,6 +1419,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/package.json b/package.json index 0187ce3..fb3944d 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,13 @@ "scripts": { "build": "node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json", "test": "node scripts/test-reference-adapters.mjs --json", + "test:sentinel": "playwright test tests/consumer-reference-sentinels.spec.mjs --project=chromium", "validate": "node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json" }, + "devDependencies": { + "@playwright/test": "1.61.0", + "ajv": "8.20.0" + }, "dependencies": { "style-dictionary": "5.5.0" } diff --git a/playwright.config.mjs b/playwright.config.mjs new file mode 100644 index 0000000..12a8b4f --- /dev/null +++ b/playwright.config.mjs @@ -0,0 +1,22 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + expect: { + toHaveScreenshot: { + animations: "disabled", + caret: "hide", + threshold: 0, + }, + }, + fullyParallel: false, + projects: [{ name: "chromium", use: { browserName: "chromium" } }], + reporter: [["line"]], + snapshotPathTemplate: "{testDir}/snapshots/{arg}{ext}", + testDir: "./tests", + use: { + actionTimeout: 10_000, + trace: "retain-on-failure", + viewport: { height: 768, width: 1024 }, + }, + workers: 1, +}); diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index 36a87e8..f7a19d8 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -31,6 +31,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity, source ownership, or local quality. | | Consumer-reference handoffs, schema/runtime parity, repository-local record paths, lifecycle separation, ownership truth, and dependency direction remain enforced. | `scripts/validate-consumer-reference.mjs` and `scripts/test-validate-consumer-reference.mjs` | `node scripts/validate-consumer-reference.mjs --json`; `node scripts/test-validate-consumer-reference.mjs --json` | Declared and reasoned-not-applicable fixtures exit zero, every shipped schema rule has accepted/rejected parity coverage, all repository handoffs declare applicability, fixtures remain related, and ownership discloses placeholder single-account review. | Unknown top-level or nested properties, invalid schema fields, missing handoff or reason, stable with ended support, boolean independence, unresolved/absolute/scheme/network/parent/item-or-record-symlink/non-JSON paths, scaffold success, fifth-domain classification, literal or supported computed reverse imports, and repository handoff omissions must fail with named finding codes and non-zero child exit. | Proves receiver-contract consistency, bounded static dependency detection, and path containment, not general JavaScript data-flow safety, visual quality, independent adoption, verified owner permissions, or consumer implementation conformance. | | Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | +| The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, Linux repeatability, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | ## Validator Severity Contract diff --git a/scripts/baseline-contract.mjs b/scripts/baseline-contract.mjs new file mode 100644 index 0000000..5368947 --- /dev/null +++ b/scripts/baseline-contract.mjs @@ -0,0 +1,153 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { isDeepStrictEqual } from "node:util"; +import { parseStrictJson } from "./strict-json.mjs"; + +export const BASELINE_ENVIRONMENT = { + architecture: "amd64", + browser_revision: "1228", + browser_version: "149.0.7827.55", + container_image: "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + node: "22", + os: "Ubuntu 24.04", + platform: "linux/amd64", + playwright: "1.61.0", + viewport: { height: 768, width: 1024 }, +}; + +export const BASELINE_REFERENCE = { + baseline: { path: "tests/snapshots/consumer-reference-card-grid.png", sha256: "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" }, + source: { path: "tests/helpers/render-consumer-reference.mjs", sha256: "88802a948909d5e40470be6b5481766ce2de498e59c053ac68af370b46e72ca9" }, +}; + +export const BASELINE_METADATA_SHA256 = sha256(JSON.stringify({ environment: BASELINE_ENVIRONMENT, reference: BASELINE_REFERENCE })); + +export function finding(code, file, message) { + return { code, message, path: file }; +} + +export function sha256(value) { + return crypto.createHash("sha256").update(value).digest("hex"); +} + +export function readJson(file, failures) { + if (!fs.existsSync(file)) { + failures.push(finding("baseline_file_missing", file, "required baseline record is missing")); + return undefined; + } + try { + const stat = fs.lstatSync(file); + if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 1024 * 1024) throw new Error("JSON input must be a regular file no larger than 1 MiB"); + return parseStrictJson(fs.readFileSync(file, "utf8")); + } catch (error) { + failures.push(finding("baseline_json_invalid", file, error instanceof Error ? error.message : String(error))); + return undefined; + } +} + +function sameValue(left, right) { + return isDeepStrictEqual(left, right); +} + +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function rejectUnknown(value, allowed, code, file, failures) { + if (!isRecord(value)) return; + for (const key of Object.keys(value).filter((key) => !allowed.has(key))) { + failures.push(finding(code, file, `unsupported property ${key}`)); + } +} + +function isHash(value) { + return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); +} + +export function validateEnvironment(environment, file) { + return sameValue(environment, BASELINE_ENVIRONMENT) + ? [] + : [finding("calibration_environment_mismatch", file, "environment must match the immutable linux/amd64 calibration image")]; +} + +export function validateCalibration(record, file) { + const failures = []; + if (!isRecord(record)) return [finding("calibration_record_invalid", file, "calibration record must be an object")]; + const allowed = new Set(["baseline_owner_approval", "committed_ci", "environment", "reference", "required_runs", "runs", "schema_version", "status"]); + rejectUnknown(record, allowed, "calibration_property_unknown", file, failures); + if (record.schema_version !== "1.0") failures.push(finding("calibration_schema_version", file, "schema_version must be 1.0")); + if (record.baseline_owner_approval !== "pending") failures.push(finding("baseline_owner_approval_invalid", file, "baseline owner approval must remain pending")); + if (record.required_runs !== 20) failures.push(finding("calibration_required_runs", file, "required_runs must equal 20")); + failures.push(...validateEnvironment(record.environment, file)); + if (!sameValue(record.reference, BASELINE_REFERENCE)) failures.push(finding("calibration_reference_mismatch", file, "source and baseline hashes must match the proposed reference")); + if (!Array.isArray(record.runs)) return [...failures, finding("calibration_runs_invalid", file, "runs must be an array")]; + if (record.status === "awaiting_committed_ci") { + if (record.runs.length !== 0 || record.committed_ci !== null) failures.push(finding("calibration_pending_evidence_forbidden", file, "pending calibration cannot claim runs or committed CI")); + return failures; + } + if (record.status !== "completed") return [...failures, finding("calibration_status_invalid", file, "status must be awaiting_committed_ci or completed")]; + if (!isRecord(record.committed_ci)) failures.push(finding("calibration_committed_ci_missing", file, "completed calibration requires committed CI metadata")); + else { + rejectUnknown(record.committed_ci, new Set(["artifact_name", "checkout_sha", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"]), "calibration_committed_ci_property_unknown", file, failures); + const expectedArtifact = `chromium-sentinel-calibration-${record.committed_ci.run_id}-${record.committed_ci.sha}`; + if (!/^[0-9]+$/.test(record.committed_ci.run_id ?? "") + || !/^[0-9]+$/.test(record.committed_ci.run_attempt ?? "") + || !/^[0-9a-f]{40}$/.test(record.committed_ci.sha ?? "") + || !/^[0-9a-f]{40}$/.test(record.committed_ci.head_sha ?? "") + || record.committed_ci.checkout_sha !== record.committed_ci.sha + || !/^[0-9a-f]{64}$/.test(record.committed_ci.raw_evidence_sha256 ?? "") + || record.committed_ci.repository !== "changeroa/StyleGallery" + || record.committed_ci.workflow !== ".github/workflows/validate.yml" + || record.committed_ci.artifact_name !== expectedArtifact) { + failures.push(finding("calibration_committed_ci_invalid", file, "repository, workflow, run, attempt, SHAs, raw evidence, and artifact name must have the canonical identity")); + } + } + if (record.runs.length !== 20) failures.push(finding("calibration_run_count", file, "completed calibration requires exactly 20 runs")); + const runAllowed = new Set(["architecture", "ax_sha256", "completed", "dom_sha256", "metadata_sha256", "png_sha256", "run", "screenshot_diff_pixels"]); + for (const run of record.runs) { + if (!isRecord(run)) { + failures.push(finding("calibration_run_invalid", file, "every run must be an object")); + continue; + } + rejectUnknown(run, runAllowed, "calibration_run_property_unknown", file, failures); + if (!Number.isInteger(run.run) || run.run < 1 || run.run > 20) failures.push(finding("calibration_run_number_invalid", file, "run must be an integer from 1 through 20")); + if (run.completed !== true) failures.push(finding("calibration_run_incomplete", file, "every calibration run must complete")); + if (run.architecture !== "amd64") failures.push(finding("calibration_architecture_variance", file, "every run must record architecture amd64")); + if (![run.ax_sha256, run.dom_sha256, run.metadata_sha256, run.png_sha256].every(isHash)) failures.push(finding("calibration_hash_invalid", file, "every artifact hash must be 64 lowercase hexadecimal characters")); + if (run.png_sha256 !== BASELINE_REFERENCE.baseline.sha256) failures.push(finding("calibration_baseline_hash_mismatch", file, "every completed PNG must equal the proposed baseline hash")); + if (run.metadata_sha256 !== BASELINE_METADATA_SHA256) failures.push(finding("calibration_metadata_hash_mismatch", file, "every completed metadata hash must equal the canonical environment and reference hash")); + if (run.screenshot_diff_pixels !== 0) failures.push(finding("calibration_diff_nonzero", file, "every screenshot diff must equal zero")); + } + const runNumbers = record.runs.map((run) => run?.run); + if (new Set(runNumbers).size !== runNumbers.length) failures.push(finding("calibration_run_duplicate", file, "calibration run numbers must be unique")); + if (!sameValue([...runNumbers].sort((left, right) => left - right), Array.from({ length: 20 }, (_, index) => index + 1))) failures.push(finding("calibration_run_set", file, "calibration runs must be the complete range 1 through 20")); + const architectures = new Set(record.runs.map((run) => run?.architecture)); + if (architectures.size !== 1 || !architectures.has("amd64")) failures.push(finding("calibration_architecture_variance", file, "every run must record architecture amd64")); + if (["png_sha256", "dom_sha256", "ax_sha256"].some((field) => new Set(record.runs.map((run) => run?.[field])).size !== 1)) failures.push(finding("calibration_hash_variance", file, "PNG, DOM, and AX hashes must each be identical across runs")); + if (new Set(record.runs.map((run) => run?.metadata_sha256)).size !== 1) failures.push(finding("calibration_metadata_variance", file, "stable environment metadata must be identical across runs")); + return failures; +} + +export function validateManifest(manifest, file, root) { + const failures = []; + if (!isRecord(manifest)) return [finding("baseline_manifest_invalid", file, "baseline manifest must be an object")]; + const allowed = new Set(["architecture", "baseline", "baseline_owner_approval", "browser_revision", "browser_version", "calibration", "container_image", "node", "os", "platform", "playwright", "schema_version", "source", "status", "viewport"]); + rejectUnknown(manifest, allowed, "baseline_manifest_property_unknown", file, failures); + if (manifest.schema_version !== "1.0" || manifest.status !== "proposed") failures.push(finding("baseline_manifest_state", file, "manifest must be proposed schema 1.0")); + if (manifest.baseline_owner_approval !== "pending") failures.push(finding("baseline_owner_approval_invalid", file, "baseline owner approval must remain pending")); + for (const [key, value] of Object.entries(BASELINE_ENVIRONMENT)) if (!sameValue(manifest[key], value)) failures.push(finding("baseline_environment_mismatch", file, `${key} differs from the immutable environment`)); + rejectUnknown(manifest.calibration, new Set(["record", "required_runs", "workflow_artifact_name"]), "baseline_calibration_property_unknown", file, failures); + if (!isRecord(manifest.calibration) || manifest.calibration.required_runs !== 20 || manifest.calibration.record !== "consumer-reference/baselines/calibration.json") failures.push(finding("baseline_calibration_route", file, "manifest must route to the 20-run calibration record")); + if (manifest.calibration?.workflow_artifact_name !== "chromium-sentinel-calibration-${run_id}-${sha}") failures.push(finding("baseline_artifact_name", file, "workflow artifact name template is immutable")); + for (const [kind, entry] of [["source", manifest.source], ["baseline", manifest.baseline]]) { + rejectUnknown(entry, new Set(["path", "sha256"]), `baseline_${kind}_property_unknown`, file, failures); + if (!sameValue(entry, BASELINE_REFERENCE[kind])) failures.push(finding(`baseline_${kind}_reference_mismatch`, file, `${kind} identity must equal the proposed reference`)); + const target = typeof entry?.path === "string" ? path.resolve(root, entry.path) : ""; + if (!isRecord(entry) || !isHash(entry.sha256)) failures.push(finding(`baseline_${kind}_invalid`, file, `${kind} requires an exact path and 64-character hash`)); + if (!target || !target.startsWith(`${root}${path.sep}`) || !fs.existsSync(target)) failures.push(finding(`baseline_${kind}_missing`, file, `${kind} path must resolve inside the repository`)); + else if (fs.lstatSync(target).isSymbolicLink() || !fs.lstatSync(target).isFile() || !fs.realpathSync(target).startsWith(`${fs.realpathSync(root)}${path.sep}`)) failures.push(finding(`baseline_${kind}_file_invalid`, file, `${kind} must be a regular repository file, not a directory or symlink`)); + else if (entry.sha256 !== sha256(fs.readFileSync(target))) failures.push(finding(`baseline_${kind}_hash_mismatch`, file, `${kind} hash differs from repository bytes`)); + } + return failures; +} diff --git a/scripts/calibration-raw-contract.mjs b/scripts/calibration-raw-contract.mjs new file mode 100644 index 0000000..72753bb --- /dev/null +++ b/scripts/calibration-raw-contract.mjs @@ -0,0 +1,172 @@ +import fs from "node:fs"; +import path from "node:path"; +import { isDeepStrictEqual } from "node:util"; +import { BASELINE_ENVIRONMENT, BASELINE_METADATA_SHA256, BASELINE_REFERENCE, finding, sha256 } from "./baseline-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +const EXPECTED_TEST = "canonical consumer reference preserves computed layout before its locator screenshot"; +const HASH_PATTERN = /^[0-9a-f]{64}$/; +const REQUIRED_FILES = new Set(["actual.png", "ax.txt", "comparison.json", "dom.html", "exit.json", "metadata.json", "playwright.json"]); +const ALLOWED_FILES = new Set([...REQUIRED_FILES, "playwright.stderr"]); +const FILE_LIMITS = new Map([ + ["actual.png", 5 * 1024 * 1024], ["ax.txt", 1024 * 1024], ["comparison.json", 1024 * 1024], + ["dom.html", 1024 * 1024], ["exit.json", 1024 * 1024], ["metadata.json", 1024 * 1024], + ["playwright.json", 10 * 1024 * 1024], ["playwright.stderr", 1024 * 1024], +]); + +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function readJson(file, code, failures) { + if (!fs.existsSync(file)) { + failures.push(finding(code, file, "required raw calibration JSON is missing")); + return undefined; + } + try { + const stat = fs.lstatSync(file); + const limit = FILE_LIMITS.get(path.basename(file)) ?? 1024 * 1024; + if (stat.isSymbolicLink() || !stat.isFile() || stat.size > limit) throw new Error(`JSON evidence must be a regular file no larger than ${limit} bytes`); + return parseStrictJson(fs.readFileSync(file, "utf8")); + } catch (error) { + failures.push(finding(`${code}_invalid`, file, error instanceof Error ? error.message : String(error))); + return undefined; + } +} + +function rejectUnknown(value, allowed, code, file, failures) { + if (!isRecord(value)) return; + for (const key of Object.keys(value).filter((key) => !allowed.has(key))) { + failures.push(finding(code, file, `unsupported property ${key}`)); + } +} + +function validateExit(exit, expectedRun, file, failures) { + if (!isRecord(exit)) { + failures.push(finding("calibration_exit_invalid", file, "exit record must be an object")); + return false; + } + rejectUnknown(exit, new Set(["exit_code", "run", "schema_version"]), "calibration_exit_property_unknown", file, failures); + if (exit.schema_version !== "1.0" || exit.run !== expectedRun || exit.exit_code !== 0) { + failures.push(finding("calibration_exit_unsuccessful", file, "exit record must identify this run with exit code zero")); + return false; + } + return true; +} + +function validatePlaywright(report, file, failures) { + if (!isRecord(report)) { + failures.push(finding("calibration_playwright_result_invalid", file, "Playwright result must be an object")); + return false; + } + const project = report.config?.projects?.[0]; + const suite = report.suites?.[0]; + const spec = suite?.specs?.[0]; + const test = spec?.tests?.[0]; + const result = test?.results?.[0]; + const identityMatches = report.config?.version === "1.61.0" + && report.config?.workers === 1 + && report.config?.projects?.length === 1 + && project?.id === "chromium" + && project?.name === "chromium" + && report.suites?.length === 1 + && suite?.file === "consumer-reference-sentinels.spec.mjs" + && suite?.specs?.length === 1 + && spec?.title === EXPECTED_TEST + && spec?.tests?.length === 1 + && test?.projectId === "chromium" + && test?.projectName === "chromium"; + if (!identityMatches) failures.push(finding("calibration_playwright_identity", file, "result must contain exactly the canonical Chromium sentinel test")); + const completionMatches = spec?.ok === true + && test?.expectedStatus === "passed" + && test?.status === "expected" + && test?.results?.length === 1 + && result?.status === "passed" + && Array.isArray(result?.errors) + && result.errors.length === 0 + && Array.isArray(report.errors) + && report.errors.length === 0 + && report.stats?.expected === 1 + && report.stats?.skipped === 0 + && report.stats?.unexpected === 0 + && report.stats?.flaky === 0; + if (!completionMatches) failures.push(finding("calibration_playwright_incomplete", file, "canonical test must pass once with no skipped, flaky, interrupted, or unexpected result")); + return identityMatches && completionMatches; +} + +function validateMetadata(metadata, expectedRun, file, failures) { + if (!isRecord(metadata)) { + failures.push(finding("calibration_metadata_invalid", file, "metadata must be an object")); + return false; + } + const allowed = new Set(["architecture", "ax_sha256", "dom_sha256", "environment", "metadata_sha256", "png_sha256", "reference", "run", "schema_version"]); + rejectUnknown(metadata, allowed, "calibration_metadata_property_unknown", file, failures); + const hashesValid = [metadata.ax_sha256, metadata.dom_sha256, metadata.metadata_sha256, metadata.png_sha256].every((value) => typeof value === "string" && HASH_PATTERN.test(value)); + const valid = metadata.schema_version === "1.0" + && metadata.run === expectedRun + && metadata.architecture === "amd64" + && hashesValid + && metadata.metadata_sha256 === BASELINE_METADATA_SHA256 + && isDeepStrictEqual(metadata.environment, BASELINE_ENVIRONMENT) + && isDeepStrictEqual(metadata.reference, BASELINE_REFERENCE); + if (!valid) failures.push(finding("calibration_metadata_mismatch", file, "metadata shape, identity, hashes, environment, and reference must be canonical")); + return valid; +} + +function validateComparison(comparison, expectedRun, actualHash, file, failures) { + if (!isRecord(comparison)) { + failures.push(finding("calibration_comparison_invalid", file, "comparison proof must be an object")); + return false; + } + const allowed = new Set(["actual_sha256", "assertion", "diff_pixels", "expected_sha256", "max_diff_pixels", "run", "schema_version", "status", "threshold"]); + rejectUnknown(comparison, allowed, "calibration_comparison_property_unknown", file, failures); + const valid = comparison.schema_version === "1.0" + && comparison.run === expectedRun + && comparison.assertion === "visual_geometry_matches_proposed_baseline" + && comparison.status === "passed" + && comparison.threshold === 0 + && comparison.max_diff_pixels === 0 + && comparison.diff_pixels === 0 + && comparison.actual_sha256 === actualHash + && comparison.expected_sha256 === BASELINE_REFERENCE.baseline.sha256 + && comparison.actual_sha256 === comparison.expected_sha256; + if (!valid) failures.push(finding("calibration_comparison_mismatch", file, "comparison must prove the exact zero-diff baseline assertion")); + return valid; +} + +export function parseCalibrationRun(root, expectedRun, failures) { + const entries = fs.readdirSync(root, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile() || !ALLOWED_FILES.has(entry.name)) failures.push(finding("calibration_raw_file_unknown", path.join(root, entry.name), "run directory contains unsupported or duplicate evidence")); + else if (fs.lstatSync(path.join(root, entry.name)).size > FILE_LIMITS.get(entry.name)) failures.push(finding("calibration_raw_file_oversized", path.join(root, entry.name), `${entry.name} exceeds its calibration evidence byte limit`)); + } + for (const name of REQUIRED_FILES) if (!entries.some((entry) => entry.isFile() && entry.name === name)) failures.push(finding("calibration_raw_file_missing", path.join(root, name), `${name} is required exactly once`)); + const metadata = readJson(path.join(root, "metadata.json"), "calibration_metadata_missing", failures); + const exit = readJson(path.join(root, "exit.json"), "calibration_exit_missing", failures); + const report = readJson(path.join(root, "playwright.json"), "calibration_playwright_missing", failures); + const comparison = readJson(path.join(root, "comparison.json"), "calibration_comparison_missing", failures); + const metadataValid = validateMetadata(metadata, expectedRun, path.join(root, "metadata.json"), failures); + const exitValid = validateExit(exit, expectedRun, path.join(root, "exit.json"), failures); + const reportValid = validatePlaywright(report, path.join(root, "playwright.json"), failures); + const artifactFields = [["actual.png", "png_sha256"], ["dom.html", "dom_sha256"], ["ax.txt", "ax_sha256"]]; + let artifactsValid = metadataValid; + for (const [name, field] of artifactFields) { + const file = path.join(root, name); + if (!fs.existsSync(file) || fs.lstatSync(file).isSymbolicLink() || !fs.lstatSync(file).isFile() || fs.lstatSync(file).size > FILE_LIMITS.get(name) || !metadata || sha256(fs.readFileSync(file)) !== metadata[field]) { + failures.push(finding("calibration_artifact_hash_mismatch", file, `${field} differs from raw artifact bytes`)); + artifactsValid = false; + } + } + const comparisonValid = validateComparison(comparison, expectedRun, metadata?.png_sha256, path.join(root, "comparison.json"), failures); + if (!(metadataValid && exitValid && reportValid && artifactsValid && comparisonValid)) return undefined; + return { + architecture: metadata.architecture, + ax_sha256: metadata.ax_sha256, + completed: true, + dom_sha256: metadata.dom_sha256, + metadata_sha256: metadata.metadata_sha256, + png_sha256: metadata.png_sha256, + run: expectedRun, + screenshot_diff_pixels: comparison.diff_pixels, + }; +} diff --git a/scripts/strict-json.mjs b/scripts/strict-json.mjs new file mode 100644 index 0000000..92b6f77 --- /dev/null +++ b/scripts/strict-json.mjs @@ -0,0 +1,72 @@ +export function parseStrictJson(source) { + const value = JSON.parse(source); + let index = 0; + + function skipWhitespace() { + while (/\s/.test(source[index] ?? "")) index += 1; + } + + function scanString() { + const start = index; + index += 1; + while (source[index] !== '"') { + if (source[index] === "\\") index += 1; + index += 1; + } + index += 1; + return JSON.parse(source.slice(start, index)); + } + + function scanValue() { + skipWhitespace(); + if (source[index] === "{") scanObject(); + else if (source[index] === "[") scanArray(); + else if (source[index] === '"') scanString(); + else while (index < source.length && !/[\s,}\]]/.test(source[index])) index += 1; + skipWhitespace(); + } + + function scanArray() { + index += 1; + skipWhitespace(); + if (source[index] === "]") { + index += 1; + return; + } + while (index < source.length) { + scanValue(); + if (source[index] === "]") { + index += 1; + return; + } + index += 1; + } + } + + function scanObject() { + const keys = new Set(); + index += 1; + skipWhitespace(); + if (source[index] === "}") { + index += 1; + return; + } + while (index < source.length) { + const key = scanString(); + if (keys.has(key)) throw new SyntaxError(`duplicate JSON property ${key}`); + keys.add(key); + skipWhitespace(); + index += 1; + scanValue(); + if (source[index] === "}") { + index += 1; + return; + } + index += 1; + skipWhitespace(); + } + } + + scanValue(); + return value; +} diff --git a/scripts/summarize-sentinel-calibration.mjs b/scripts/summarize-sentinel-calibration.mjs new file mode 100644 index 0000000..35e8416 --- /dev/null +++ b/scripts/summarize-sentinel-calibration.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { BASELINE_ENVIRONMENT, BASELINE_REFERENCE, finding, sha256, validateCalibration } from "./baseline-contract.mjs"; +import { parseCalibrationRun } from "./calibration-raw-contract.mjs"; + +const options = { artifactName: "", checkoutSha: "", headSha: "", input: "", json: false, output: "", repository: "", runAttempt: "", runId: "", sha: "", workflow: "" }; +const valueOptions = new Map([ + ["--artifact-name", "artifactName"], ["--checkout-sha", "checkoutSha"], ["--head-sha", "headSha"], + ["--input", "input"], ["--output", "output"], ["--repository", "repository"], + ["--run-attempt", "runAttempt"], ["--run-id", "runId"], ["--sha", "sha"], ["--workflow", "workflow"], +]); +const failures = []; +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (valueOptions.has(argument)) { + const value = process.argv[index + 1]; + if (!value) failures.push(finding("argument_value_required", "", `${argument} requires a value`)); + else { + options[valueOptions.get(argument)] = value; + index += 1; + } + } else failures.push(finding("argument_unknown", "", `unsupported argument ${argument}`)); +} + +if (!options.input || !options.output) failures.push(finding("calibration_path_required", "", "--input and --output are required")); +const inputIsDirectory = Boolean(options.input && fs.existsSync(options.input) && !fs.lstatSync(options.input).isSymbolicLink() && fs.lstatSync(options.input).isDirectory()); +if (options.input && fs.existsSync(options.input) && !inputIsDirectory) failures.push(finding("calibration_input_invalid", options.input, "input must be a regular raw calibration directory, not a file or symlink")); +const expectedOutput = options.input ? path.join(path.resolve(options.input), "calibration.json") : ""; +if (options.output && path.resolve(options.output) !== expectedOutput) failures.push(finding("calibration_output_route", options.output, "output must be calibration.json directly inside the raw input directory")); +if (inputIsDirectory && options.output && path.resolve(options.output) === expectedOutput && fs.existsSync(options.output)) { + if (fs.lstatSync(options.output).isDirectory()) failures.push(finding("calibration_output_invalid", options.output, "output path must not be a directory")); + else fs.rmSync(options.output); +} +const runs = []; +if (options.input && fs.existsSync(options.input)) { + if (inputIsDirectory) { + const directories = fs.readdirSync(options.input, { withFileTypes: true }); + const expectedDirectories = Array.from({ length: 20 }, (_, index) => `run-${String(index + 1).padStart(2, "0")}`); + for (const directory of directories.filter((entry) => !entry.isDirectory() || !expectedDirectories.includes(entry.name))) { + failures.push(finding("calibration_run_directory_unknown", path.join(options.input, directory.name), "raw input may contain only run-01 through run-20 directories")); + } + for (let run = 1; run <= 20; run += 1) { + const name = expectedDirectories[run - 1]; + const root = path.join(options.input, name); + if (!fs.existsSync(root)) failures.push(finding("calibration_run_directory_missing", root, `${name} is required`)); + else if (fs.lstatSync(root).isSymbolicLink() || !fs.lstatSync(root).isDirectory() || !fs.realpathSync(root).startsWith(`${fs.realpathSync(options.input)}${path.sep}`)) failures.push(finding("calibration_run_directory_invalid", root, `${name} must be a contained regular directory, not a file or symlink`)); + else { + const parsed = parseCalibrationRun(root, run, failures); + if (parsed) runs.push(parsed); + } + } + } +} else if (options.input) failures.push(finding("calibration_input_missing", options.input, "input artifact directory does not exist")); + +const rawEvidence = failures.length === 0 ? sha256(Array.from({ length: 20 }, (_, index) => path.join(options.input, `run-${String(index + 1).padStart(2, "0")}`)) + .flatMap((root) => fs.readdirSync(root).sort().map((name) => { + const file = path.join(root, name); + return `${path.relative(options.input, file)}:${sha256(fs.readFileSync(file))}`; + })).join("\n")) : ""; + +const record = { + baseline_owner_approval: "pending", + committed_ci: { + artifact_name: options.artifactName, + checkout_sha: options.checkoutSha, + head_sha: options.headSha, + raw_evidence_sha256: rawEvidence, + repository: options.repository, + run_attempt: options.runAttempt, + run_id: options.runId, + sha: options.sha, + workflow: options.workflow, + }, + environment: BASELINE_ENVIRONMENT, + required_runs: 20, + reference: BASELINE_REFERENCE, + runs, + schema_version: "1.0", + status: "completed", +}; +failures.push(...validateCalibration(record, options.output || "")); +if (failures.length === 0) { + const temporaryOutput = `${options.output}.tmp`; + fs.writeFileSync(temporaryOutput, `${JSON.stringify(record, null, 2)}\n`); + fs.renameSync(temporaryOutput, options.output); +} +const diffs = runs.map((run) => run.screenshot_diff_pixels).sort((left, right) => left - right); +const stats = diffs.length === 20 ? { max: diffs[19], mean: diffs.reduce((sum, value) => sum + value, 0) / 20, min: diffs[0], p95: diffs[18] } : null; +const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()]; +const result = { failures: uniqueFailures, ok: uniqueFailures.length === 0, runs: runs.length, stats, status: uniqueFailures.length === 0 ? "completed" : "incomplete", warnings: [] }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (!result.ok) process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/test-consumer-reference-sentinel.mjs b/scripts/test-consumer-reference-sentinel.mjs new file mode 100644 index 0000000..483ea34 --- /dev/null +++ b/scripts/test-consumer-reference-sentinel.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const cases = [ + ["hidden_layout", "hidden-layout", "computed_layout_visible"], + ["long_content_reflow", "long-content-reflow", "computed_long_content_reflow"], +]; + +const results = cases.map(([name, mutation, expected]) => { + const child = spawnSync( + path.join(repositoryRoot, "node_modules", ".bin", "playwright"), + ["test", "tests/consumer-reference-sentinels.spec.mjs", "--project=chromium", "--reporter=line"], + { cwd: repositoryRoot, encoding: "utf8", env: { ...process.env, SENTINEL_MUTATION: mutation } }, + ); + const output = `${child.stdout}\n${child.stderr}`; + return { + actual: { named: output.includes(expected), status: child.status }, + expected, + name, + ok: child.status !== 0 && output.includes(expected), + }; +}); + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +if (!report.ok) process.exitCode = 1; diff --git a/scripts/test-renderer-purity.mjs b/scripts/test-renderer-purity.mjs new file mode 100644 index 0000000..dd740cf --- /dev/null +++ b/scripts/test-renderer-purity.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const validator = path.join(repositoryRoot, "scripts", "validate-renderer-purity.mjs"); +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-renderer-")); +const cases = []; + +function run(renderer) { + const child = spawnSync(process.execPath, [validator, "--renderer", renderer, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + return { child, output: JSON.parse(child.stdout) }; +} + +try { + const valid = run("tests/helpers/render-consumer-reference.mjs"); + cases.push({ actual: { codes: valid.output.failures.map((failure) => failure.code), status: valid.child.status }, expected: "pure import and exit:0", name: "canonical_renderer_import", ok: valid.child.status === 0 && valid.output.ok === true }); + const mutation = path.join(tempRoot, "render-consumer-reference.mjs"); + fs.writeFileSync(mutation, 'import fs from "node:fs";\nimport { patterns } from "../../scripts/pattern-data.mjs";\nfs.appendFileSync("CATALOG.md", "mutation");\nexport { patterns };\n'); + const invalid = run(mutation); + const codes = invalid.output.failures.map((failure) => failure.code); + cases.push({ actual: { codes, status: invalid.child.status }, expected: "renderer_side_effect_capability", name: "renderer_side_effect", ok: invalid.child.status !== 0 && codes.includes("renderer_side_effect_capability") }); +} finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); +} + +const failures = cases.filter((testCase) => !testCase.ok).map((testCase) => `missing_semantic:${testCase.name}:${testCase.expected}`); +const result = { failures, ok: failures.length === 0, results: cases }; +process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/test-summarize-sentinel-calibration.mjs b/scripts/test-summarize-sentinel-calibration.mjs new file mode 100644 index 0000000..de94f3b --- /dev/null +++ b/scripts/test-summarize-sentinel-calibration.mjs @@ -0,0 +1,178 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { BASELINE_ENVIRONMENT, BASELINE_REFERENCE, sha256 } from "./baseline-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const summarizer = path.join(repositoryRoot, "scripts", "summarize-sentinel-calibration.mjs"); +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-summary-")); +const png = fs.readFileSync(path.join(repositoryRoot, BASELINE_REFERENCE.baseline.path)); +const dom = Buffer.from("
\n"); +const ax = Buffer.from("- region \"Project cards\"\n"); +const pngHash = sha256(png); +const metadataHash = sha256(JSON.stringify({ environment: BASELINE_ENVIRONMENT, reference: BASELINE_REFERENCE })); + +function playwrightReport() { + return { + config: { projects: [{ id: "chromium", name: "chromium" }], version: "1.61.0", workers: 1 }, + errors: [], + stats: { expected: 1, flaky: 0, skipped: 0, unexpected: 0 }, + suites: [{ + file: "consumer-reference-sentinels.spec.mjs", + specs: [{ + ok: true, + tests: [{ + expectedStatus: "passed", + projectId: "chromium", + projectName: "chromium", + results: [{ errors: [], status: "passed" }], + status: "expected", + }], + title: "canonical consumer reference preserves computed layout before its locator screenshot", + }], + }], + }; +} + +function writeJson(file, value) { + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function writeRun(root, run) { + const runRoot = path.join(root, `run-${String(run).padStart(2, "0")}`); + fs.mkdirSync(runRoot, { recursive: true }); + fs.writeFileSync(path.join(runRoot, "actual.png"), png); + fs.writeFileSync(path.join(runRoot, "dom.html"), dom); + fs.writeFileSync(path.join(runRoot, "ax.txt"), ax); + writeJson(path.join(runRoot, "playwright.json"), playwrightReport()); + writeJson(path.join(runRoot, "exit.json"), { exit_code: 0, run, schema_version: "1.0" }); + writeJson(path.join(runRoot, "comparison.json"), { + actual_sha256: pngHash, + assertion: "visual_geometry_matches_proposed_baseline", + diff_pixels: 0, + expected_sha256: BASELINE_REFERENCE.baseline.sha256, + max_diff_pixels: 0, + run, + schema_version: "1.0", + status: "passed", + threshold: 0, + }); + writeJson(path.join(runRoot, "metadata.json"), { + architecture: "amd64", + ax_sha256: sha256(ax), + dom_sha256: sha256(dom), + environment: BASELINE_ENVIRONMENT, + metadata_sha256: metadataHash, + png_sha256: pngHash, + reference: BASELINE_REFERENCE, + run, + schema_version: "1.0", + }); + return runRoot; +} + +function invoke(name, mutate) { + const input = path.join(tempRoot, name, "raw"); + const outputFile = path.join(input, "calibration.json"); + for (let run = 1; run <= 20; run += 1) writeRun(input, run); + mutate?.(path.join(input, "run-01")); + const sha = "0".repeat(40); + const child = spawnSync(process.execPath, [ + summarizer, + "--input", input, + "--output", outputFile, + "--repository", "changeroa/StyleGallery", + "--workflow", ".github/workflows/validate.yml", + "--run-id", "123", + "--run-attempt", "1", + "--sha", sha, + "--checkout-sha", sha, + "--head-sha", "1".repeat(40), + "--artifact-name", `chromium-sentinel-calibration-123-${sha}`, + "--json", + ], { cwd: repositoryRoot, encoding: "utf8" }); + return { + output: fs.existsSync(outputFile) && fs.lstatSync(outputFile).isFile() ? JSON.parse(fs.readFileSync(outputFile, "utf8")) : null, + report: JSON.parse(child.stdout), + status: child.status, + }; +} + +const results = []; +try { + const valid = invoke("valid"); + results.push({ + actual: { outputRuns: valid.output?.runs.length, report: valid.report, status: valid.status }, + expected: "20 truth-derived stable runs and zero diff statistics", + name: "valid_raw_calibration", + ok: valid.status === 0 && valid.report.ok === true && valid.report.status === "completed" + && valid.output?.runs.length === 20 && new Set(valid.output.runs.map((run) => run.png_sha256)).size === 1, + }); + const invalidCases = [ + ["missing_exit", (root) => fs.rmSync(path.join(root, "exit.json")), "calibration_raw_file_missing"], + ["nonzero_exit", (root) => writeJson(path.join(root, "exit.json"), { exit_code: 1, run: 1, schema_version: "1.0" }), "calibration_exit_unsuccessful"], + ["fake_playwright", (root) => writeJson(path.join(root, "playwright.json"), {}), "calibration_playwright_identity"], + ["duplicate_metadata", (root) => fs.copyFileSync(path.join(root, "metadata.json"), path.join(root, "metadata-copy.json")), "calibration_raw_file_unknown"], + ["unknown_metadata", (root) => { + const file = path.join(root, "metadata.json"); + const metadata = JSON.parse(fs.readFileSync(file, "utf8")); + writeJson(file, { ...metadata, completed: true }); + }, "calibration_metadata_property_unknown"], + ["duplicate_metadata_property", (root) => { + const file = path.join(root, "metadata.json"); + const metadata = fs.readFileSync(file, "utf8").replace('"schema_version": "1.0"', '"schema_version": "1.0",\n "schema_version": "1.0"'); + fs.writeFileSync(file, metadata); + }, "calibration_metadata_missing_invalid"], + ["missing_comparison", (root) => fs.rmSync(path.join(root, "comparison.json")), "calibration_raw_file_missing"], + ["relaxed_comparison_threshold", (root) => { + const file = path.join(root, "comparison.json"); + const comparison = JSON.parse(fs.readFileSync(file, "utf8")); + writeJson(file, { ...comparison, threshold: 0.1 }); + }, "calibration_comparison_mismatch"], + ["invalid_run_directory", (root) => { + fs.rmSync(root, { recursive: true }); + fs.writeFileSync(root, "not a directory\n"); + }, "calibration_run_directory_invalid"], + ["invalid_input_file", (root) => { + const input = path.dirname(root); + fs.rmSync(input, { recursive: true }); + fs.writeFileSync(input, "not a directory\n"); + }, "calibration_input_invalid"], + ["symlinked_input_directory", (root) => { + const input = path.dirname(root); + const external = path.join(tempRoot, "external-input"); + fs.cpSync(input, external, { recursive: true }); + fs.rmSync(input, { recursive: true }); + fs.symlinkSync(external, input); + }, "calibration_input_invalid"], + ["invalid_output_directory", (root) => fs.mkdirSync(path.join(path.dirname(root), "calibration.json")), "calibration_output_invalid"], + ["unknown_root_file", (root) => fs.writeFileSync(path.join(path.dirname(root), "forged.json"), "{}\n"), "calibration_run_directory_unknown"], + ["symlinked_run_directory", (root) => { + const external = path.join(tempRoot, "external-run"); + fs.cpSync(path.join(path.dirname(root), "run-02"), external, { recursive: true }); + fs.rmSync(root, { recursive: true }); + fs.symlinkSync(external, root); + }, "calibration_run_directory_invalid"], + ["artifact_directory", (root) => { + fs.rmSync(path.join(root, "actual.png")); + fs.mkdirSync(path.join(root, "actual.png")); + }, "calibration_artifact_hash_mismatch"], + ["oversized_metadata", (root) => fs.appendFileSync(path.join(root, "metadata.json"), " ".repeat(1024 * 1024)), "calibration_raw_file_oversized"], + ["oversized_png", (root) => fs.appendFileSync(path.join(root, "actual.png"), Buffer.alloc(5 * 1024 * 1024)), "calibration_raw_file_oversized"], + ]; + for (const [name, mutate, expected] of invalidCases) { + const actual = invoke(name, mutate); + const codes = actual.report.failures.map((failure) => failure.code); + results.push({ actual: { codes, outputExists: actual.output !== null, status: actual.status }, expected, name, ok: actual.status !== 0 && actual.output === null && actual.report.status === "incomplete" && codes.includes(expected) }); + } +} finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); +} + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +if (!report.ok) process.exitCode = 1; diff --git a/scripts/test-validate-baseline-manifest.mjs b/scripts/test-validate-baseline-manifest.mjs new file mode 100644 index 0000000..74f2e54 --- /dev/null +++ b/scripts/test-validate-baseline-manifest.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { BASELINE_METADATA_SHA256, BASELINE_REFERENCE, sha256 } from "./baseline-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const validator = path.join(repositoryRoot, "scripts", "validate-baseline-manifest.mjs"); +const canonical = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "baselines", "calibration.json"), "utf8")); +const canonicalManifest = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "baselines", "manifest.json"), "utf8")); +const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "calibration-record.schema.json"), "utf8")); +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-calibration-")); + +function completeRecord() { + const runs = Array.from({ length: 20 }, (_, index) => ({ + architecture: "amd64", + ax_sha256: "a".repeat(64), + completed: true, + dom_sha256: "b".repeat(64), + metadata_sha256: BASELINE_METADATA_SHA256, + png_sha256: BASELINE_REFERENCE.baseline.sha256, + run: index + 1, + screenshot_diff_pixels: 0, + })); + return { + ...canonical, + committed_ci: { + artifact_name: `chromium-sentinel-calibration-123-${"0".repeat(40)}`, + checkout_sha: "0".repeat(40), + head_sha: "1".repeat(40), + raw_evidence_sha256: "2".repeat(64), + repository: "changeroa/StyleGallery", + run_attempt: "1", + run_id: "123", + sha: "0".repeat(40), + workflow: ".github/workflows/validate.yml", + }, + status: "completed", + runs, + }; +} + +const cases = [ + ["nineteen_of_twenty", (record) => ({ ...record, runs: record.runs.slice(0, 19) }), "calibration_run_count"], + ["duplicate_run", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, run: 19 } : run) }), "calibration_run_duplicate"], + ["missing_run", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, run: 21 } : run) }), "calibration_run_set"], + ["architecture_variance", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, architecture: "arm64" } : run) }), "calibration_architecture_variance"], + ["environment_mismatch", (record) => ({ ...record, environment: { ...record.environment, architecture: "arm64" } }), "calibration_environment_mismatch"], + ["hash_variance", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, png_sha256: "e".repeat(64) } : run) }), "calibration_hash_variance"], + ["metadata_variance", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, metadata_sha256: "f".repeat(64) } : run) }), "calibration_metadata_variance"], + ["incomplete_run", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, completed: false } : run) }), "calibration_run_incomplete"], + ["nonzero_diff", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, screenshot_diff_pixels: 1 } : run) }), "calibration_diff_nonzero"], + ["malformed_hash", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, png_sha256: "x" } : run) }), "calibration_hash_invalid"], + ["wrong_stable_baseline_hash", (record) => ({ ...record, runs: record.runs.map((run) => ({ ...run, png_sha256: "d".repeat(64) })) }), "calibration_baseline_hash_mismatch"], + ["wrong_stable_metadata_hash", (record) => ({ ...record, runs: record.runs.map((run) => ({ ...run, metadata_sha256: "c".repeat(64) })) }), "calibration_metadata_hash_mismatch"], + ["unknown_run_property", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, forged: true } : run) }), "calibration_run_property_unknown"], + ["invalid_committed_ci", (record) => ({ ...record, committed_ci: { artifact_name: "forged", extra: true, run_id: "abc", sha: "x" } }), "calibration_committed_ci_property_unknown"], + ["owner_approval_preclaim", (record) => ({ ...record, baseline_owner_approval: "approved" }), "baseline_owner_approval_invalid"], + ["pending_evidence_preclaim", (record) => ({ ...record, status: "awaiting_committed_ci" }), "calibration_pending_evidence_forbidden"], +]; + +const results = []; +try { + const committedSchema = schema.properties.committed_ci.oneOf.find((entry) => entry.type === "object"); + const runSchema = schema.properties.runs.items; + const runContains = schema.allOf[0].else.properties.runs.allOf; + const schemaParity = schema.additionalProperties === false + && committedSchema.additionalProperties === false + && schema.properties.environment.additionalProperties === false + && Object.entries(canonical.environment).every(([key, value]) => key === "viewport" + ? JSON.stringify(schema.properties.environment.properties.viewport.properties) === JSON.stringify({ width: { const: value.width }, height: { const: value.height } }) + : schema.properties.environment.properties[key].const === value) + && schema.properties.reference.additionalProperties === false + && schema.properties.reference.properties.source.additionalProperties === false + && schema.properties.reference.properties.source.properties.sha256.const === BASELINE_REFERENCE.source.sha256 + && schema.properties.reference.properties.baseline.properties.sha256.const === BASELINE_REFERENCE.baseline.sha256 + && runSchema.additionalProperties === false + && runSchema.properties.png_sha256.const === BASELINE_REFERENCE.baseline.sha256 + && runSchema.properties.metadata_sha256.const === BASELINE_METADATA_SHA256 + && JSON.stringify(runContains.map((entry) => entry.contains.properties.run.const).sort((left, right) => left - right)) === JSON.stringify(Array.from({ length: 20 }, (_, index) => index + 1)) + && runContains.every((entry) => entry.minContains === 1 && entry.maxContains === 1) + && JSON.stringify([...committedSchema.required].sort()) === JSON.stringify(["artifact_name", "checkout_sha", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"].sort()); + results.push({ actual: { schemaParity }, expected: "recursive schema/runtime identity parity", name: "schema_runtime_parity", ok: schemaParity }); + const validFixture = path.join(tempRoot, "valid-completed.json"); + fs.writeFileSync(validFixture, `${JSON.stringify(completeRecord(), null, 2)}\n`); + const validChild = spawnSync(process.execPath, [validator, "--calibration", validFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const validOutput = JSON.parse(validChild.stdout); + results.push({ actual: { codes: validOutput.failures.map((failure) => failure.code), status: validChild.status }, expected: "completed calibration and exit:0", name: "valid_completed_calibration", ok: validChild.status === 0 && validOutput.ok === true }); + for (const [name, mutate, expected] of cases) { + const fixture = path.join(tempRoot, `${name}.json`); + fs.writeFileSync(fixture, `${JSON.stringify(mutate(completeRecord()), null, 2)}\n`); + const child = spawnSync(process.execPath, [validator, "--calibration", fixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const output = JSON.parse(child.stdout); + const codes = Array.isArray(output.failures) ? output.failures.map((failure) => failure.code) : []; + results.push({ actual: { codes, status: child.status }, expected, name, ok: child.status !== 0 && codes.includes(expected) }); + } + const manifestFixture = path.join(tempRoot, "unknown-manifest-nested.json"); + fs.writeFileSync(manifestFixture, `${JSON.stringify({ + ...canonicalManifest, + calibration: { ...canonicalManifest.calibration, forged: true }, + source: { ...canonicalManifest.source, forged: true }, + }, null, 2)}\n`); + const manifestChild = spawnSync(process.execPath, [validator, "--manifest", manifestFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const manifestOutput = JSON.parse(manifestChild.stdout); + const manifestCodes = manifestOutput.failures.map((failure) => failure.code); + results.push({ + actual: { codes: manifestCodes, status: manifestChild.status }, + expected: "baseline_calibration_property_unknown and baseline_source_property_unknown", + name: "unknown_manifest_nested_properties", + ok: manifestChild.status !== 0 && manifestCodes.includes("baseline_calibration_property_unknown") && manifestCodes.includes("baseline_source_property_unknown"), + }); + const alternateFile = path.join(repositoryRoot, "package.json"); + const alternateManifestFixture = path.join(tempRoot, "alternate-baseline.json"); + fs.writeFileSync(alternateManifestFixture, `${JSON.stringify({ + ...canonicalManifest, + baseline: { path: "package.json", sha256: sha256(fs.readFileSync(alternateFile)) }, + }, null, 2)}\n`); + const alternateChild = spawnSync(process.execPath, [validator, "--manifest", alternateManifestFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const alternateOutput = JSON.parse(alternateChild.stdout); + const alternateCodes = alternateOutput.failures.map((failure) => failure.code); + results.push({ + actual: { codes: alternateCodes, status: alternateChild.status }, + expected: "baseline_baseline_reference_mismatch", + name: "alternate_in_repo_baseline", + ok: alternateChild.status !== 0 && alternateCodes.includes("baseline_baseline_reference_mismatch"), + }); + const directoryManifestFixture = path.join(tempRoot, "directory-baseline.json"); + fs.writeFileSync(directoryManifestFixture, `${JSON.stringify({ ...canonicalManifest, baseline: { path: "tests", sha256: "a".repeat(64) } }, null, 2)}\n`); + const directoryChild = spawnSync(process.execPath, [validator, "--manifest", directoryManifestFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const directoryOutput = JSON.parse(directoryChild.stdout); + results.push({ actual: { codes: directoryOutput.failures.map((failure) => failure.code), status: directoryChild.status }, expected: "baseline_baseline_file_invalid", name: "directory_manifest_target", ok: directoryChild.status !== 0 && directoryOutput.failures.some((failure) => failure.code === "baseline_baseline_file_invalid") }); + for (const [name, content, expected] of [ + ["null_calibration", "null\n", "calibration_record_invalid"], + ["duplicate_calibration_property", '{"schema_version":"1.0","schema_version":"1.0"}\n', "baseline_json_invalid"], + ]) { + const fixture = path.join(tempRoot, `${name}.json`); + fs.writeFileSync(fixture, content); + const child = spawnSync(process.execPath, [validator, "--calibration", fixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const output = JSON.parse(child.stdout); + results.push({ actual: { codes: output.failures.map((failure) => failure.code), status: child.status }, expected, name, ok: child.status !== 0 && output.failures.some((failure) => failure.code === expected) }); + } +} finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); +} + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +if (!report.ok) process.exitCode = 1; diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index ea33d1b..5c3657d 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -8,12 +8,20 @@ import { spawnSync } from "node:child_process"; const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const validator = path.join(root, "scripts", "validate-governance.mjs"); const generatedWarning = ""; +const sentinelProvenanceClauses = [ + "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", + "Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending.", + "Linux/amd64 repeatability remains unclaimed until that external verification succeeds.", + "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", + "Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance.", +]; const files = { "AGENTS.md": "# Agent Instructions\n\nSee [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md) and [StyleGallery Domains](DOMAINS.md).\n", "README.md": "# StyleGallery\n\n- [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md)\n- [StyleGallery Domains](DOMAINS.md)\n", "index.md": "# StyleGallery\n\n- [Governance, lifecycle, and docs-as-code](GOVERNANCE.md)\n- [StyleGallery Domains](DOMAINS.md)\n", "DOMAINS.md": "# StyleGallery Domains\n", + "consumer-reference/contract.md": `${sentinelProvenanceClauses.join("\n")}\n`, "CATALOG.md": `# Layout Pattern Catalog\n\n${generatedWarning}\n`, "patterns/index.md": `# Pattern Categories\n\n${generatedWarning}\n`, "patterns/stacking/index.md": `# Stacking\n\n${generatedWarning}\n`, @@ -33,6 +41,9 @@ const files = { "| Governance, lifecycle, generated-file, ownership, and stale-content policy remain discoverable and CI-enforced. | `scripts/validate-governance.mjs` and `scripts/test-validate-governance.mjs` | `node scripts/validate-governance.mjs --json`; `node scripts/test-validate-governance.mjs --json` | `GOVERNANCE.md`, `.github/CODEOWNERS`, generated warnings, generated metadata, root links, lifecycle states, stale-audit decision, and CI wiring are present. | Missing governance file, generated warning, generated metadata, CODEOWNERS coverage, or stale policy fixtures must fail. | Proves governance policy is present and linked, not that CODEOWNERS users have verified repository write access. |", "| Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity or local quality. |", "Consumer-reference handoffs, schema/runtime parity, and containment remain enforced; repository handoff omissions must fail.", + "The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", + "Linux/amd64 20-run calibration and `baseline_owner_approval` remain pending.", + ...sentinelProvenanceClauses, "", ].join("\n"), ".github/CODEOWNERS": [ @@ -51,6 +62,9 @@ const files = { "/design-engineering/ @changeroa", "/platform-guides/ @changeroa", "/consumer-reference/ @changeroa", + "/consumer-reference/baselines/ @changeroa", + "/tests/ @changeroa", + "/playwright.config.mjs @changeroa", "/scripts/pattern-data.mjs @changeroa", "/scripts/generate-patterns.mjs @changeroa", "/patterns/ @changeroa", @@ -77,6 +91,11 @@ const files = { "node -c scripts/test-validate-consumer-reference.mjs", "node scripts/validate-consumer-reference.mjs --json", "node scripts/test-validate-consumer-reference.mjs --json", + "node scripts/test-consumer-reference-sentinel.mjs", + "node scripts/validate-baseline-manifest.mjs --json", + "node scripts/test-validate-baseline-manifest.mjs --json", + "node scripts/test-summarize-sentinel-calibration.mjs", + "node scripts/validate-renderer-purity.mjs --json", "permissions:", "contents: read", "", @@ -96,6 +115,7 @@ const files = { "| Layout recipes | `recipes/*.md` | Manual | None | `scripts/validate-okf.mjs` | Recipe owner |", "| Quality gates and evidence | `quality/**/*.md` | Manual | None | `scripts/validate-okf.mjs` | Quality owner |", "| Consumer reference contract | `consumer-reference/contract.md` | Manual | None | `stable` | Receiver changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", + "| Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/calibration-record.schema.json`, `consumer-reference/baselines/*.json` | Playwright | Snapshot and raw calibration evidence | `experimental` | Renderer or baseline changes. | `scripts/validate-baseline-manifest.mjs` | Validation owner |", "| Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner |", "| Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner |", "| Motion domain guidance | `motion/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Motion domain owner |", @@ -123,6 +143,8 @@ const files = { "Decision: no scheduled stale-content workflow yet.", "Audit trigger:", "node scripts/validate-links.mjs --json", + "node scripts/test-consumer-reference-sentinel.mjs", + ...sentinelProvenanceClauses, "", ].join("\n"), }; @@ -187,12 +209,24 @@ const cases = [ { name: "paraphrased_governance_link_label", mutate: { "README.md": files["README.md"].replace("[Governance, Lifecycle, And Docs-As-Code]", "[Governance reference]") }, expectWarning: "README.md: recommended link label missing [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md)" }, { name: "missing_motion_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/motion/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /motion/ @changeroa" }, { name: "missing_consumer_reference_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/consumer-reference/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /consumer-reference/ @changeroa" }, + { name: "missing_baseline_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/consumer-reference/baselines/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /consumer-reference/baselines/ @changeroa" }, { name: "missing_domain_family", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("| Motion domain guidance |", "| Motion reference notes |") }, expect: "GOVERNANCE.md: missing Motion domain guidance" }, { name: "missing_domain_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/validate-domains.mjs --json\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/validate-domains.mjs --json" }, { name: "missing_domain_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail.", "Domain fixtures must fail.") }, expect: "quality/evidence/executable-evidence.md: missing Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail." }, + { name: "missing_sentinel_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/test-consumer-reference-sentinel.mjs\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-consumer-reference-sentinel.mjs" }, + { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, { name: "success_path", expect: null }, ]; +for (const [pathIndex, relative] of ["consumer-reference/contract.md", "quality/evidence/executable-evidence.md", "GOVERNANCE.md"].entries()) { + for (const [clauseIndex, clause] of sentinelProvenanceClauses.entries()) { + cases.splice(cases.length - 1, 0, + { name: `provenance_clause_deleted_${pathIndex}_${clauseIndex}`, mutate: { [relative]: files[relative].replace(clause, "") }, expect: `${relative}: missing ${clause}` }, + { name: `provenance_clause_misworded_${pathIndex}_${clauseIndex}`, mutate: { [relative]: files[relative].replace(clause, `Misworded ${clause.slice(1)}`) }, expect: `${relative}: missing ${clause}` }, + ); + } +} + function writeFixture(testCase) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), `layout-gallery-governance-${testCase.name}-`)); const omitted = new Set(testCase.omit ?? []); diff --git a/scripts/validate-baseline-manifest.mjs b/scripts/validate-baseline-manifest.mjs new file mode 100644 index 0000000..4f5c9bd --- /dev/null +++ b/scripts/validate-baseline-manifest.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +import path from "node:path"; +import Ajv2020 from "ajv/dist/2020.js"; +import { readJson, validateCalibration, validateManifest } from "./baseline-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const options = { calibration: "consumer-reference/baselines/calibration.json", json: false, manifest: "consumer-reference/baselines/manifest.json" }; +const failures = []; +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (argument === "--manifest" || argument === "--calibration") { + const value = process.argv[index + 1]; + if (!value) failures.push({ code: "argument_value_required", message: `${argument} requires a value`, path: "" }); + else { + options[argument.slice(2)] = value; + index += 1; + } + } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); +} + +const manifestPath = path.resolve(repositoryRoot, options.manifest); +const calibrationPath = path.resolve(repositoryRoot, options.calibration); +const manifestSchemaPath = path.join(repositoryRoot, "consumer-reference/schema/baseline-manifest.schema.json"); +const calibrationSchemaPath = path.join(repositoryRoot, "consumer-reference/schema/calibration-record.schema.json"); +const manifest = readJson(manifestPath, failures); +const calibration = readJson(calibrationPath, failures); +const manifestSchema = readJson(manifestSchemaPath, failures); +const calibrationSchema = readJson(calibrationSchemaPath, failures); +const ajv = new Ajv2020({ allErrors: true, strict: false }); +for (const [kind, value, schema, file] of [["manifest", manifest, manifestSchema, manifestPath], ["calibration", calibration, calibrationSchema, calibrationPath]]) { + if (value === undefined || schema === undefined) continue; + const validate = ajv.compile(schema); + if (!validate(value)) for (const error of validate.errors ?? []) failures.push({ code: `baseline_${kind}_schema_invalid`, message: `${error.instancePath || "/"} ${error.message}`, path: file }); +} +if (manifest !== undefined) failures.push(...validateManifest(manifest, manifestPath, repositoryRoot)); +if (calibration !== undefined) failures.push(...validateCalibration(calibration, calibrationPath)); +const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()]; +const result = { failures: uniqueFailures, ok: uniqueFailures.length === 0, warnings: [] }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (!result.ok) process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 869d42e..a767fb3 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -8,6 +8,13 @@ const json = args.has("--json"); const root = process.cwd(); const failures = []; const warnings = []; +const sentinelProvenanceClauses = [ + "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", + "Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending.", + "Linux/amd64 repeatability remains unclaimed until that external verification succeeds.", + "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", + "Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance.", +]; const requiredCodeowners = [ "* @changeroa", "/GOVERNANCE.md @changeroa", @@ -20,6 +27,9 @@ const requiredCodeowners = [ "/design-engineering/ @changeroa", "/platform-guides/ @changeroa", "/consumer-reference/ @changeroa", + "/consumer-reference/baselines/ @changeroa", + "/tests/ @changeroa", + "/playwright.config.mjs @changeroa", "/GUIDE.md @changeroa", "/guides/ @changeroa", "/recipes/ @changeroa", @@ -74,6 +84,7 @@ function requireGovernanceMatrix() { "Layout recipes", "Quality gates and evidence", "Consumer reference contract", + "Proposed Chromium sentinel", "Domain manifest and scope decision", "Layout domain hub", "Motion domain guidance", @@ -129,6 +140,11 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "node -c scripts/test-validate-consumer-reference.mjs"); requireIncludes(".github/workflows/validate.yml", "node scripts/validate-consumer-reference.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-consumer-reference.mjs --json"); + requireIncludes(".github/workflows/validate.yml", "node scripts/test-consumer-reference-sentinel.mjs"); + requireIncludes(".github/workflows/validate.yml", "node scripts/validate-baseline-manifest.mjs --json"); + requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-baseline-manifest.mjs --json"); + requireIncludes(".github/workflows/validate.yml", "node scripts/test-summarize-sentinel-calibration.mjs"); + requireIncludes(".github/workflows/validate.yml", "node scripts/validate-renderer-purity.mjs --json"); requireIncludes(".github/workflows/validate.yml", "permissions:"); requireIncludes(".github/workflows/validate.yml", "contents: read"); } @@ -153,10 +169,18 @@ function requireEvidenceMap() { requireIncludes("quality/evidence/executable-evidence.md", "Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail."); requireIncludes("quality/evidence/executable-evidence.md", "Consumer-reference handoffs, schema/runtime parity"); requireIncludes("quality/evidence/executable-evidence.md", "repository handoff omissions must fail"); + requireIncludes("quality/evidence/executable-evidence.md", "The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence."); + requireIncludes("GOVERNANCE.md", "node scripts/test-consumer-reference-sentinel.mjs"); requireIncludes("GOVERNANCE.md", "owner.enforcement: \"placeholder\""); requireIncludes("GOVERNANCE.md", "review_independence: \"single_account\""); } +function requireSentinelProvenanceBoundary() { + for (const relative of ["consumer-reference/contract.md", "quality/evidence/executable-evidence.md", "GOVERNANCE.md"]) { + for (const clause of sentinelProvenanceClauses) requireIncludes(relative, clause); + } +} + requireGovernanceMatrix(); requireLifecycleStates(); requireOwnership(); @@ -164,6 +188,7 @@ requireStalenessDecision(); requireCiwiring(); requireRootLinks(); requireEvidenceMap(); +requireSentinelProvenanceBoundary(); requireGeneratedWarning("CATALOG.md"); requireGeneratedWarning("patterns/index.md"); requireGeneratedWarning("patterns/stacking/index.md"); diff --git a/scripts/validate-renderer-purity.mjs b/scripts/validate-renderer-purity.mjs new file mode 100644 index 0000000..22dd3a2 --- /dev/null +++ b/scripts/validate-renderer-purity.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +let renderer = "tests/helpers/render-consumer-reference.mjs"; +let json = false; +const failures = []; +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") json = true; + else if (argument === "--renderer" && process.argv[index + 1]) { + renderer = process.argv[index + 1]; + index += 1; + } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); +} + +function hashProtectedSources() { + const files = [path.join(repositoryRoot, "CATALOG.md")]; + const queue = [path.join(repositoryRoot, "patterns")]; + while (queue.length > 0) { + const current = queue.pop(); + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const target = path.join(current, entry.name); + if (entry.isDirectory()) queue.push(target); + else if (entry.isFile()) files.push(target); + } + } + const hash = crypto.createHash("sha256"); + for (const file of files.sort()) hash.update(path.relative(repositoryRoot, file)).update(fs.readFileSync(file)); + return hash.digest("hex"); +} + +const rendererPath = path.resolve(repositoryRoot, renderer); +if (!fs.existsSync(rendererPath)) failures.push({ code: "renderer_missing", message: "renderer helper does not exist", path: renderer }); +else { + const source = fs.readFileSync(rendererPath, "utf8"); + const imports = [...source.matchAll(/^import\s+[^;]+?\s+from\s+["']([^"']+)["'];?$/gm)].map((match) => match[1]); + if (imports.length !== 1 || imports[0] !== "../../scripts/pattern-data.mjs") failures.push({ code: "renderer_import_boundary", message: "renderer may import only scripts/pattern-data.mjs", path: renderer }); + if (/generate-patterns|build-reference-artifacts/.test(source)) failures.push({ code: "renderer_generator_dependency", message: "renderer must not import or call generators", path: renderer }); + if (/node:(?:fs|child_process)|\b(?:writeFile|appendFile|rmSync|renameSync|spawnSync|execSync)\b/.test(source)) failures.push({ code: "renderer_side_effect_capability", message: "renderer must not hold filesystem or process mutation capability", path: renderer }); + if (failures.length === 0) { + const before = hashProtectedSources(); + await import(`${pathToFileURL(rendererPath).href}?purity=${Date.now()}`); + const after = hashProtectedSources(); + if (after !== before) failures.push({ code: "renderer_protected_source_mutation", message: "renderer import changed patterns or CATALOG.md", path: renderer }); + } +} + +const result = { failures, ok: failures.length === 0, warnings: [] }; +if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (!result.ok) process.stderr.write(`${failures.map((failure) => `${failure.code}: ${failure.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/tests/consumer-reference-sentinels.spec.mjs b/tests/consumer-reference-sentinels.spec.mjs new file mode 100644 index 0000000..d666e70 --- /dev/null +++ b/tests/consumer-reference-sentinels.spec.mjs @@ -0,0 +1,100 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; +import { BASELINE_ENVIRONMENT, BASELINE_REFERENCE } from "../scripts/baseline-contract.mjs"; +import { renderConsumerReference } from "./helpers/render-consumer-reference.mjs"; + +const mutation = process.env.SENTINEL_MUTATION ?? "none"; +const artifactRoot = process.env.SENTINEL_ARTIFACT_DIR; + +test("canonical consumer reference preserves computed layout before its locator screenshot", async ({ page }) => { + await renderConsumerReference(page, mutation); + const root = page.locator(".card_grid"); + await expect(root).toHaveCount(1); + await expect(root, "computed_layout_visible").toBeVisible(); + await expect(root.locator(":scope > article"), "computed_cardinality").toHaveCount(3); + const computed = await root.evaluate((element) => { + const style = getComputedStyle(element); + const box = element.getBoundingClientRect(); + return { + clientWidth: document.documentElement.clientWidth, + display: style.display, + gap: style.gap, + gridTemplateColumns: style.gridTemplateColumns, + height: box.height, + scrollWidth: document.documentElement.scrollWidth, + width: box.width, + }; + }); + expect(computed.display, "computed_layout_display_grid").toBe("grid"); + expect(computed.gap, "computed_layout_gap").toBe("16px"); + expect(computed.gridTemplateColumns, "computed_layout_columns").not.toBe("none"); + expect(computed.width > 0 && computed.height > 0, "computed_layout_nonzero_box").toBe(true); + expect(computed.scrollWidth <= computed.clientWidth, "computed_long_content_reflow").toBe(true); + const mask = [root.locator(":scope > article")]; + const png = await root.screenshot({ animations: "disabled", caret: "hide", mask }); + const actualHash = crypto.createHash("sha256").update(png).digest("hex"); + const calibrationRun = Number(process.env.SENTINEL_CALIBRATION_RUN ?? 1); + if (artifactRoot) { + fs.mkdirSync(artifactRoot, { recursive: true }); + const dom = await root.evaluate((element) => element.outerHTML); + const ax = await page.locator("body").ariaSnapshot(); + const domBytes = Buffer.from(`${dom}\n`); + const axBytes = Buffer.from(`${ax}\n`); + fs.writeFileSync(path.join(artifactRoot, "actual.png"), png); + fs.writeFileSync(path.join(artifactRoot, "dom.html"), domBytes); + fs.writeFileSync(path.join(artifactRoot, "ax.txt"), axBytes); + const localEnvironment = { + architecture: process.arch === "x64" ? "amd64" : process.arch, + browser_revision: "1228", + browser_version: page.context().browser()?.version() ?? "unknown", + container_image: process.env.SENTINEL_CONTAINER_IMAGE ?? "local-unpinned", + node: process.versions.node, + os: process.platform, + platform: process.platform === "linux" ? "linux/amd64" : `${process.platform}/${process.arch}`, + playwright: "1.61.0", + viewport: page.viewportSize(), + }; + if (process.env.SENTINEL_CALIBRATION_RUN) { + expect(process.platform, "calibration_runtime_platform").toBe("linux"); + expect(localEnvironment.architecture, "calibration_runtime_architecture").toBe("amd64"); + expect(process.versions.node.split(".")[0], "calibration_runtime_node").toBe("22"); + expect(process.env.SENTINEL_CONTAINER_IMAGE, "calibration_runtime_container").toBe(BASELINE_ENVIRONMENT.container_image); + expect(localEnvironment.browser_version, "calibration_runtime_browser_version").toBe(BASELINE_ENVIRONMENT.browser_version); + expect(localEnvironment.viewport, "calibration_runtime_viewport").toEqual(BASELINE_ENVIRONMENT.viewport); + } + const environment = process.env.SENTINEL_CALIBRATION_RUN ? BASELINE_ENVIRONMENT : localEnvironment; + fs.writeFileSync(path.join(artifactRoot, "metadata.json"), `${JSON.stringify({ + architecture: environment.architecture, + ax_sha256: crypto.createHash("sha256").update(axBytes).digest("hex"), + dom_sha256: crypto.createHash("sha256").update(domBytes).digest("hex"), + environment, + metadata_sha256: crypto.createHash("sha256").update(JSON.stringify({ environment, reference: BASELINE_REFERENCE })).digest("hex"), + png_sha256: actualHash, + reference: BASELINE_REFERENCE, + run: calibrationRun, + schema_version: "1.0", + }, null, 2)}\n`); + } + await expect(root, "visual_geometry_matches_proposed_baseline").toHaveScreenshot("consumer-reference-card-grid.png", { + animations: "disabled", + caret: "hide", + mask, + maxDiffPixels: 0, + threshold: 0, + }); + if (artifactRoot) { + fs.writeFileSync(path.join(artifactRoot, "comparison.json"), `${JSON.stringify({ + actual_sha256: actualHash, + assertion: "visual_geometry_matches_proposed_baseline", + diff_pixels: 0, + expected_sha256: BASELINE_REFERENCE.baseline.sha256, + max_diff_pixels: 0, + run: calibrationRun, + schema_version: "1.0", + status: "passed", + threshold: 0, + }, null, 2)}\n`); + } +}); diff --git a/tests/helpers/render-consumer-reference.mjs b/tests/helpers/render-consumer-reference.mjs new file mode 100644 index 0000000..499c4a1 --- /dev/null +++ b/tests/helpers/render-consumer-reference.mjs @@ -0,0 +1,36 @@ +import { patterns, samples } from "../../scripts/pattern-data.mjs"; + +const CANONICAL_PATTERN = "card-grid"; + +function declarationsToCss(declarations) { + return declarations + .map(([selector, properties]) => `${selector}{${Object.entries(properties).map(([name, value]) => `${name}:${value}`).join(";")}}`) + .join("\n"); +} + +export function canonicalConsumerReference() { + const pattern = patterns.find(([name]) => name === CANONICAL_PATTERN); + if (!pattern) throw new Error(`canonical pattern ${CANONICAL_PATTERN} is unavailable`); + return { + css: declarationsToCss(pattern[6]), + html: samples[CANONICAL_PATTERN], + pattern: CANONICAL_PATTERN, + }; +} + +export async function renderConsumerReference(page, mutation = "none") { + const fixture = canonicalConsumerReference(); + const mutationCss = mutation === "hidden-layout" ? ".card_grid{display:none;gap:99999rem}" : ""; + const mutationHtml = mutation === "long-content-reflow" + ? fixture.html.replace("Design audit", `Design audit ${"unbroken".repeat(256)}`) + : fixture.html; + await page.setContent(`${mutationHtml}`); + return fixture; +} diff --git a/tests/snapshots/consumer-reference-card-grid.png b/tests/snapshots/consumer-reference-card-grid.png new file mode 100644 index 0000000000000000000000000000000000000000..c251fb2734852afe88de09bd81c8bb4cd6a89984 GIT binary patch literal 713 zcmeAS@N?(olHy`uVBq!ia0y~yV7>rkS8y-^$*I}B3mF)g>O5T>Ln`LHxn{_Fz(C-@ zf&9>aQ-XOUZB%wWPB?jgmgxb_vg%qki{@PqxNgn29U{D+-M#F#_>0qHe1H=FS k_m6$i0VXXrP)cNG@Qq>lD3W7+1t`Mc>FVdQ&MBb@0KRmW6aWAK literal 0 HcmV?d00001 From 8b8eaed41094286138973157b581a1d9ab9957a8 Mon Sep 17 00:00:00 2001 From: victor Date: Mon, 13 Jul 2026 23:38:10 +0900 Subject: [PATCH 04/44] Fix calibration checkout SHA capture --- .github/workflows/validate.yml | 2 +- scripts/test-validate-governance.mjs | 21 +++++++++++++++++++++ scripts/validate-governance.mjs | 4 ++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1c6137f..49ba952 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -195,7 +195,7 @@ jobs: failed=0 mkdir -p calibration-artifacts artifact_name="chromium-sentinel-calibration-${GITHUB_RUN_ID}-${GITHUB_SHA}" - checkout_sha="$(git rev-parse HEAD)" + checkout_sha="$(git -c safe.directory="$GITHUB_WORKSPACE" rev-parse HEAD)" for run in $(seq 1 20); do run_dir="calibration-artifacts/run-$(printf '%02d' "$run")" mkdir -p "$run_dir" diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index 5c3657d..329f9bc 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -96,6 +96,7 @@ const files = { "node scripts/test-validate-baseline-manifest.mjs --json", "node scripts/test-summarize-sentinel-calibration.mjs", "node scripts/validate-renderer-purity.mjs --json", + "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\"", "permissions:", "contents: read", "", @@ -215,6 +216,26 @@ const cases = [ { name: "missing_domain_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail.", "Domain fixtures must fail.") }, expect: "quality/evidence/executable-evidence.md: missing Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail." }, { name: "missing_sentinel_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/test-consumer-reference-sentinel.mjs\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-consumer-reference-sentinel.mjs" }, { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, + { + name: "missing_scoped_checkout_sha_trust", + mutate: { + ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace( + "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\"", + "checkout_sha=\"$(git rev-parse HEAD)\"", + ), + }, + expect: ".github/workflows/validate.yml: missing checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\"", + }, + { + name: "reject_broad_checkout_sha_trust", + mutate: { + ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace( + "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\"", + "checkout_sha=\"$(git -c safe.directory='*' rev-parse HEAD)\"", + ), + }, + expect: ".github/workflows/validate.yml: broad Git safe.directory wildcard is forbidden", + }, { name: "success_path", expect: null }, ]; diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index a767fb3..0117b05 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -145,6 +145,10 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-baseline-manifest.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-summarize-sentinel-calibration.mjs"); requireIncludes(".github/workflows/validate.yml", "node scripts/validate-renderer-purity.mjs --json"); + requireIncludes(".github/workflows/validate.yml", "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\""); + if (/safe\.directory(?:=|\s+)["']?\*["']?/.test(read(".github/workflows/validate.yml"))) { + failures.push(".github/workflows/validate.yml: broad Git safe.directory wildcard is forbidden"); + } requireIncludes(".github/workflows/validate.yml", "permissions:"); requireIncludes(".github/workflows/validate.yml", "contents: read"); } From 3bfdd25ec9a2df4ca84e19541365d48842a73f59 Mon Sep 17 00:00:00 2001 From: victor Date: Mon, 13 Jul 2026 23:59:54 +0900 Subject: [PATCH 05/44] Fix calibration repository identity --- .github/workflows/validate.yml | 7 ++- GOVERNANCE.md | 2 +- consumer-reference/contract.md | 2 +- .../schema/calibration-record.schema.json | 11 +++- quality/evidence/executable-evidence.md | 2 +- scripts/baseline-contract.mjs | 9 ++- scripts/summarize-sentinel-calibration.mjs | 5 +- .../test-summarize-sentinel-calibration.mjs | 56 ++++++++++++++++--- scripts/test-validate-baseline-manifest.mjs | 37 +++++++++++- scripts/test-validate-governance.mjs | 20 +++++++ scripts/validate-governance.mjs | 5 ++ 11 files changed, 134 insertions(+), 22 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 49ba952..61c3493 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -211,7 +211,8 @@ jobs: node scripts/summarize-sentinel-calibration.mjs \ --input calibration-artifacts \ --output calibration-artifacts/calibration.json \ - --repository "$GITHUB_REPOSITORY" \ + --repository "changeroa/StyleGallery" \ + --execution-repository "$GITHUB_REPOSITORY" \ --workflow ".github/workflows/validate.yml" \ --run-id "$GITHUB_RUN_ID" \ --run-attempt "$GITHUB_RUN_ATTEMPT" \ @@ -222,8 +223,8 @@ jobs: --json > "$validation_output" summary_exit=$? mv "$validation_output" calibration-artifacts/calibration-validation.json - printf '{"run_id":"%s","sha":"%s","artifact_name":"%s","platform":"linux/amd64","architecture":"amd64","container_image":"%s"}\n' \ - "$GITHUB_RUN_ID" "$GITHUB_SHA" "$artifact_name" "$SENTINEL_CONTAINER_IMAGE" \ + printf '{"repository":"%s","execution_repository":"%s","run_id":"%s","sha":"%s","artifact_name":"%s","platform":"linux/amd64","architecture":"amd64","container_image":"%s"}\n' \ + "changeroa/StyleGallery" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID" "$GITHUB_SHA" "$artifact_name" "$SENTINEL_CONTAINER_IMAGE" \ > calibration-artifacts/workflow-metadata.json if [ "$summary_exit" -ne 0 ]; then failed=1; fi exit "$failed" diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 2c4f7fa..28f654f 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -157,4 +157,4 @@ node scripts/test-renderer-purity.mjs Do not pass `--update-snapshots` in CI. Calibration runs exactly 20 times on the manifest's digest-pinned `linux/amd64` container and uploads raw Playwright JSON, strict exit records, PNG, DOM, AX, metadata, and post-assertion comparison evidence. Failed or incomplete calibration still uploads its truthful raw evidence without writing a completed aggregate. It remains nonblocking until the named owner explicitly approves the baseline. -Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. +Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. diff --git a/consumer-reference/contract.md b/consumer-reference/contract.md index 99601c2..31378b1 100644 --- a/consumer-reference/contract.md +++ b/consumer-reference/contract.md @@ -50,7 +50,7 @@ Dependency flows from a consumer or profile to Layout. `layout/**`, `patterns/** The Chromium reference sentinel is an evidence probe, not a Layout theme or product conformance claim. Its proposed baseline is compared in the immutable `linux/amd64` Playwright image, CI never updates snapshots, and the ordinary sentinel remains nonblocking while `baseline_owner_approval` is `pending`. Twenty identical committed-workflow repeats calibrate repeatability; they do not substitute for explicit `@changeroa` baseline approval. Each aggregate run is derived from a zero exit record, the exact passing Playwright test identity, hash-verified PNG/DOM/AX bytes, and comparator proof written only after the zero-diff assertion passes; producer metadata cannot claim completion or a diff. -Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. +Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. The no-dependency validator blocks direct spellings and literal fragments assembled through concatenation, template literals, or `path.join`. This bounded static check does not claim general JavaScript data-flow analysis; guarded sources must not hide reference paths behind variables or runtime computation. diff --git a/consumer-reference/schema/calibration-record.schema.json b/consumer-reference/schema/calibration-record.schema.json index 8197ec6..e723bdc 100644 --- a/consumer-reference/schema/calibration-record.schema.json +++ b/consumer-reference/schema/calibration-record.schema.json @@ -17,15 +17,22 @@ { "type": "object", "additionalProperties": false, - "required": ["artifact_name", "checkout_sha", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"], + "required": ["artifact_name", "checkout_sha", "execution_repository", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"], "properties": { "run_id": { "type": "string", "pattern": "^[0-9]+$" }, "run_attempt": { "type": "string", "pattern": "^[0-9]+$" }, "sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, "checkout_sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "execution_repository": { + "description": "Actual GitHub Actions repository, limited to the canonical upstream or the explicit calibration fork.", + "enum": ["ark-jo/StyleGallery", "changeroa/StyleGallery"] + }, "head_sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, "raw_evidence_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "repository": { "const": "changeroa/StyleGallery" }, + "repository": { + "description": "Canonical upstream repository, independent of the GitHub Actions execution repository.", + "const": "changeroa/StyleGallery" + }, "workflow": { "const": ".github/workflows/validate.yml" }, "artifact_name": { "type": "string", "pattern": "^chromium-sentinel-calibration-[0-9]+-[0-9a-f]{40}$" } } diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index f7a19d8..a21179b 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -31,7 +31,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity, source ownership, or local quality. | | Consumer-reference handoffs, schema/runtime parity, repository-local record paths, lifecycle separation, ownership truth, and dependency direction remain enforced. | `scripts/validate-consumer-reference.mjs` and `scripts/test-validate-consumer-reference.mjs` | `node scripts/validate-consumer-reference.mjs --json`; `node scripts/test-validate-consumer-reference.mjs --json` | Declared and reasoned-not-applicable fixtures exit zero, every shipped schema rule has accepted/rejected parity coverage, all repository handoffs declare applicability, fixtures remain related, and ownership discloses placeholder single-account review. | Unknown top-level or nested properties, invalid schema fields, missing handoff or reason, stable with ended support, boolean independence, unresolved/absolute/scheme/network/parent/item-or-record-symlink/non-JSON paths, scaffold success, fifth-domain classification, literal or supported computed reverse imports, and repository handoff omissions must fail with named finding codes and non-zero child exit. | Proves receiver-contract consistency, bounded static dependency detection, and path containment, not general JavaScript data-flow safety, visual quality, independent adoption, verified owner permissions, or consumer implementation conformance. | | Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | -| The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, Linux repeatability, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | +| The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, Linux repeatability, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | ## Validator Severity Contract diff --git a/scripts/baseline-contract.mjs b/scripts/baseline-contract.mjs index 5368947..cfdc7ea 100644 --- a/scripts/baseline-contract.mjs +++ b/scripts/baseline-contract.mjs @@ -22,6 +22,8 @@ export const BASELINE_REFERENCE = { }; export const BASELINE_METADATA_SHA256 = sha256(JSON.stringify({ environment: BASELINE_ENVIRONMENT, reference: BASELINE_REFERENCE })); +export const CALIBRATION_CANONICAL_REPOSITORY = "changeroa/StyleGallery"; +export const CALIBRATION_EXECUTION_REPOSITORIES = Object.freeze(["ark-jo/StyleGallery", CALIBRATION_CANONICAL_REPOSITORY]); export function finding(code, file, message) { return { code, message, path: file }; @@ -89,18 +91,19 @@ export function validateCalibration(record, file) { if (record.status !== "completed") return [...failures, finding("calibration_status_invalid", file, "status must be awaiting_committed_ci or completed")]; if (!isRecord(record.committed_ci)) failures.push(finding("calibration_committed_ci_missing", file, "completed calibration requires committed CI metadata")); else { - rejectUnknown(record.committed_ci, new Set(["artifact_name", "checkout_sha", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"]), "calibration_committed_ci_property_unknown", file, failures); + rejectUnknown(record.committed_ci, new Set(["artifact_name", "checkout_sha", "execution_repository", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"]), "calibration_committed_ci_property_unknown", file, failures); const expectedArtifact = `chromium-sentinel-calibration-${record.committed_ci.run_id}-${record.committed_ci.sha}`; if (!/^[0-9]+$/.test(record.committed_ci.run_id ?? "") || !/^[0-9]+$/.test(record.committed_ci.run_attempt ?? "") || !/^[0-9a-f]{40}$/.test(record.committed_ci.sha ?? "") || !/^[0-9a-f]{40}$/.test(record.committed_ci.head_sha ?? "") || record.committed_ci.checkout_sha !== record.committed_ci.sha + || !CALIBRATION_EXECUTION_REPOSITORIES.includes(record.committed_ci.execution_repository) || !/^[0-9a-f]{64}$/.test(record.committed_ci.raw_evidence_sha256 ?? "") - || record.committed_ci.repository !== "changeroa/StyleGallery" + || record.committed_ci.repository !== CALIBRATION_CANONICAL_REPOSITORY || record.committed_ci.workflow !== ".github/workflows/validate.yml" || record.committed_ci.artifact_name !== expectedArtifact) { - failures.push(finding("calibration_committed_ci_invalid", file, "repository, workflow, run, attempt, SHAs, raw evidence, and artifact name must have the canonical identity")); + failures.push(finding("calibration_committed_ci_invalid", file, "canonical and execution repositories, workflow, run, attempt, SHAs, raw evidence, and artifact name must have the canonical identity")); } } if (record.runs.length !== 20) failures.push(finding("calibration_run_count", file, "completed calibration requires exactly 20 runs")); diff --git a/scripts/summarize-sentinel-calibration.mjs b/scripts/summarize-sentinel-calibration.mjs index 35e8416..1280e3a 100644 --- a/scripts/summarize-sentinel-calibration.mjs +++ b/scripts/summarize-sentinel-calibration.mjs @@ -5,9 +5,9 @@ import path from "node:path"; import { BASELINE_ENVIRONMENT, BASELINE_REFERENCE, finding, sha256, validateCalibration } from "./baseline-contract.mjs"; import { parseCalibrationRun } from "./calibration-raw-contract.mjs"; -const options = { artifactName: "", checkoutSha: "", headSha: "", input: "", json: false, output: "", repository: "", runAttempt: "", runId: "", sha: "", workflow: "" }; +const options = { artifactName: "", checkoutSha: "", executionRepository: "", headSha: "", input: "", json: false, output: "", repository: "", runAttempt: "", runId: "", sha: "", workflow: "" }; const valueOptions = new Map([ - ["--artifact-name", "artifactName"], ["--checkout-sha", "checkoutSha"], ["--head-sha", "headSha"], + ["--artifact-name", "artifactName"], ["--checkout-sha", "checkoutSha"], ["--execution-repository", "executionRepository"], ["--head-sha", "headSha"], ["--input", "input"], ["--output", "output"], ["--repository", "repository"], ["--run-attempt", "runAttempt"], ["--run-id", "runId"], ["--sha", "sha"], ["--workflow", "workflow"], ]); @@ -66,6 +66,7 @@ const record = { committed_ci: { artifact_name: options.artifactName, checkout_sha: options.checkoutSha, + execution_repository: options.executionRepository, head_sha: options.headSha, raw_evidence_sha256: rawEvidence, repository: options.repository, diff --git a/scripts/test-summarize-sentinel-calibration.mjs b/scripts/test-summarize-sentinel-calibration.mjs index de94f3b..8d682c2 100644 --- a/scripts/test-summarize-sentinel-calibration.mjs +++ b/scripts/test-summarize-sentinel-calibration.mjs @@ -74,24 +74,31 @@ function writeRun(root, run) { return runRoot; } -function invoke(name, mutate) { +function invoke(name, mutate, identity = {}) { const input = path.join(tempRoot, name, "raw"); const outputFile = path.join(input, "calibration.json"); for (let run = 1; run <= 20; run += 1) writeRun(input, run); mutate?.(path.join(input, "run-01")); - const sha = "0".repeat(40); + const sha = identity.sha ?? "0".repeat(40); + const checkoutSha = identity.checkoutSha ?? sha; + const executionRepository = identity.executionRepository ?? "changeroa/StyleGallery"; + const headSha = identity.headSha ?? "1".repeat(40); + const repository = identity.repository ?? "changeroa/StyleGallery"; + const runId = identity.runId ?? "123"; + const artifactName = identity.artifactName ?? `chromium-sentinel-calibration-${runId}-${sha}`; const child = spawnSync(process.execPath, [ summarizer, "--input", input, "--output", outputFile, - "--repository", "changeroa/StyleGallery", + "--repository", repository, + "--execution-repository", executionRepository, "--workflow", ".github/workflows/validate.yml", - "--run-id", "123", + "--run-id", runId, "--run-attempt", "1", "--sha", sha, - "--checkout-sha", sha, - "--head-sha", "1".repeat(40), - "--artifact-name", `chromium-sentinel-calibration-123-${sha}`, + "--checkout-sha", checkoutSha, + "--head-sha", headSha, + "--artifact-name", artifactName, "--json", ], { cwd: repositoryRoot, encoding: "utf8" }); return { @@ -111,6 +118,25 @@ try { ok: valid.status === 0 && valid.report.ok === true && valid.report.status === "completed" && valid.output?.runs.length === 20 && new Set(valid.output.runs.map((run) => run.png_sha256)).size === 1, }); + const pullRequestMergeSha = "11f4668fe5988720c27e88ec7203ecd1685a40df"; + const pullRequestHeadSha = "8b8eaed41094286138973157b581a1d9ab9957a8"; + const validFork = invoke("valid_explicit_fork", undefined, { + executionRepository: "ark-jo/StyleGallery", + headSha: pullRequestHeadSha, + runId: "29258810962", + sha: pullRequestMergeSha, + }); + results.push({ + actual: { committedCi: validFork.output?.committed_ci, report: validFork.report, status: validFork.status }, + expected: "canonical upstream plus explicit execution fork with distinct merge/head SHAs", + name: "valid_explicit_fork_pull_request_identity", + ok: validFork.status === 0 && validFork.report.ok === true + && validFork.output?.committed_ci.repository === "changeroa/StyleGallery" + && validFork.output?.committed_ci.execution_repository === "ark-jo/StyleGallery" + && validFork.output?.committed_ci.sha === pullRequestMergeSha + && validFork.output?.committed_ci.checkout_sha === pullRequestMergeSha + && validFork.output?.committed_ci.head_sha === pullRequestHeadSha, + }); const invalidCases = [ ["missing_exit", (root) => fs.rmSync(path.join(root, "exit.json")), "calibration_raw_file_missing"], ["nonzero_exit", (root) => writeJson(path.join(root, "exit.json"), { exit_code: 1, run: 1, schema_version: "1.0" }), "calibration_exit_unsuccessful"], @@ -168,6 +194,22 @@ try { const codes = actual.report.failures.map((failure) => failure.code); results.push({ actual: { codes, outputExists: actual.output !== null, status: actual.status }, expected, name, ok: actual.status !== 0 && actual.output === null && actual.report.status === "incomplete" && codes.includes(expected) }); } + const identityCases = [ + ["checkout_sha_not_tested_sha", { checkoutSha: "1".repeat(40) }], + ["artifact_bound_to_head_sha", { artifactName: `chromium-sentinel-calibration-123-${"1".repeat(40)}` }], + ["unrecognized_execution_repository", { executionRepository: "untrusted/StyleGallery" }], + ["swapped_canonical_and_execution_repository", { executionRepository: "changeroa/StyleGallery", repository: "ark-jo/StyleGallery" }], + ]; + for (const [name, identity] of identityCases) { + const actual = invoke(name, undefined, identity); + const codes = actual.report.failures.map((failure) => failure.code); + results.push({ + actual: { codes, outputExists: actual.output !== null, status: actual.status }, + expected: "calibration_committed_ci_invalid", + name, + ok: actual.status !== 0 && actual.output === null && codes.includes("calibration_committed_ci_invalid"), + }); + } } finally { fs.rmSync(tempRoot, { force: true, recursive: true }); } diff --git a/scripts/test-validate-baseline-manifest.mjs b/scripts/test-validate-baseline-manifest.mjs index 74f2e54..8e2b200 100644 --- a/scripts/test-validate-baseline-manifest.mjs +++ b/scripts/test-validate-baseline-manifest.mjs @@ -12,8 +12,9 @@ const canonical = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer const canonicalManifest = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "baselines", "manifest.json"), "utf8")); const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "calibration-record.schema.json"), "utf8")); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-calibration-")); +const executionRepositories = ["ark-jo/StyleGallery", "changeroa/StyleGallery"]; -function completeRecord() { +function completeRecord(committedCi = {}) { const runs = Array.from({ length: 20 }, (_, index) => ({ architecture: "amd64", ax_sha256: "a".repeat(64), @@ -29,6 +30,7 @@ function completeRecord() { committed_ci: { artifact_name: `chromium-sentinel-calibration-123-${"0".repeat(40)}`, checkout_sha: "0".repeat(40), + execution_repository: "changeroa/StyleGallery", head_sha: "1".repeat(40), raw_evidence_sha256: "2".repeat(64), repository: "changeroa/StyleGallery", @@ -36,6 +38,7 @@ function completeRecord() { run_id: "123", sha: "0".repeat(40), workflow: ".github/workflows/validate.yml", + ...committedCi, }, status: "completed", runs, @@ -56,6 +59,14 @@ const cases = [ ["wrong_stable_baseline_hash", (record) => ({ ...record, runs: record.runs.map((run) => ({ ...run, png_sha256: "d".repeat(64) })) }), "calibration_baseline_hash_mismatch"], ["wrong_stable_metadata_hash", (record) => ({ ...record, runs: record.runs.map((run) => ({ ...run, metadata_sha256: "c".repeat(64) })) }), "calibration_metadata_hash_mismatch"], ["unknown_run_property", (record) => ({ ...record, runs: record.runs.map((run, index) => index === 19 ? { ...run, forged: true } : run) }), "calibration_run_property_unknown"], + ["checkout_sha_not_tested_sha", (record) => ({ ...record, committed_ci: { ...record.committed_ci, checkout_sha: record.committed_ci.head_sha } }), "calibration_committed_ci_invalid"], + ["artifact_bound_to_head_sha", (record) => ({ ...record, committed_ci: { ...record.committed_ci, artifact_name: `chromium-sentinel-calibration-${record.committed_ci.run_id}-${record.committed_ci.head_sha}` } }), "calibration_committed_ci_invalid"], + ["unrecognized_execution_repository", (record) => ({ ...record, committed_ci: { ...record.committed_ci, execution_repository: "untrusted/StyleGallery" } }), "calibration_committed_ci_invalid"], + ["swapped_canonical_and_execution_repository", (record) => ({ ...record, committed_ci: { ...record.committed_ci, execution_repository: "changeroa/StyleGallery", repository: "ark-jo/StyleGallery" } }), "calibration_committed_ci_invalid"], + ["missing_execution_repository", (record) => { + const { execution_repository: omitted, ...committedCi } = record.committed_ci; + return { ...record, committed_ci: committedCi }; + }, "calibration_committed_ci_invalid"], ["invalid_committed_ci", (record) => ({ ...record, committed_ci: { artifact_name: "forged", extra: true, run_id: "abc", sha: "x" } }), "calibration_committed_ci_property_unknown"], ["owner_approval_preclaim", (record) => ({ ...record, baseline_owner_approval: "approved" }), "baseline_owner_approval_invalid"], ["pending_evidence_preclaim", (record) => ({ ...record, status: "awaiting_committed_ci" }), "calibration_pending_evidence_forbidden"], @@ -79,15 +90,37 @@ try { && runSchema.additionalProperties === false && runSchema.properties.png_sha256.const === BASELINE_REFERENCE.baseline.sha256 && runSchema.properties.metadata_sha256.const === BASELINE_METADATA_SHA256 + && committedSchema.properties.repository.const === "changeroa/StyleGallery" + && JSON.stringify(committedSchema.properties.execution_repository?.enum ?? []) === JSON.stringify(executionRepositories) && JSON.stringify(runContains.map((entry) => entry.contains.properties.run.const).sort((left, right) => left - right)) === JSON.stringify(Array.from({ length: 20 }, (_, index) => index + 1)) && runContains.every((entry) => entry.minContains === 1 && entry.maxContains === 1) - && JSON.stringify([...committedSchema.required].sort()) === JSON.stringify(["artifact_name", "checkout_sha", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"].sort()); + && JSON.stringify([...committedSchema.required].sort()) === JSON.stringify(["artifact_name", "checkout_sha", "execution_repository", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"].sort()); results.push({ actual: { schemaParity }, expected: "recursive schema/runtime identity parity", name: "schema_runtime_parity", ok: schemaParity }); const validFixture = path.join(tempRoot, "valid-completed.json"); fs.writeFileSync(validFixture, `${JSON.stringify(completeRecord(), null, 2)}\n`); const validChild = spawnSync(process.execPath, [validator, "--calibration", validFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); const validOutput = JSON.parse(validChild.stdout); results.push({ actual: { codes: validOutput.failures.map((failure) => failure.code), status: validChild.status }, expected: "completed calibration and exit:0", name: "valid_completed_calibration", ok: validChild.status === 0 && validOutput.ok === true }); + const pullRequestFixture = path.join(tempRoot, "valid-pull-request-merge-head.json"); + const mergeSha = "11f4668fe5988720c27e88ec7203ecd1685a40df"; + const headSha = "8b8eaed41094286138973157b581a1d9ab9957a8"; + fs.writeFileSync(pullRequestFixture, `${JSON.stringify(completeRecord({ + artifact_name: `chromium-sentinel-calibration-29258810962-${mergeSha}`, + checkout_sha: mergeSha, + execution_repository: "ark-jo/StyleGallery", + head_sha: headSha, + repository: "changeroa/StyleGallery", + run_id: "29258810962", + sha: mergeSha, + }), null, 2)}\n`); + const pullRequestChild = spawnSync(process.execPath, [validator, "--calibration", pullRequestFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const pullRequestOutput = JSON.parse(pullRequestChild.stdout); + results.push({ + actual: { codes: pullRequestOutput.failures.map((failure) => failure.code), status: pullRequestChild.status }, + expected: "distinct canonical pull-request merge checkout and head SHAs", + name: "valid_pull_request_merge_and_head_identity", + ok: pullRequestChild.status === 0 && pullRequestOutput.ok === true, + }); for (const [name, mutate, expected] of cases) { const fixture = path.join(tempRoot, `${name}.json`); fs.writeFileSync(fixture, `${JSON.stringify(mutate(completeRecord()), null, 2)}\n`); diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index 329f9bc..c7ee4fd 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -10,6 +10,7 @@ const validator = path.join(root, "scripts", "validate-governance.mjs"); const generatedWarning = ""; const sentinelProvenanceClauses = [ "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", + "The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery.", "Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending.", "Linux/amd64 repeatability remains unclaimed until that external verification succeeds.", "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", @@ -97,6 +98,10 @@ const files = { "node scripts/test-summarize-sentinel-calibration.mjs", "node scripts/validate-renderer-purity.mjs --json", "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\"", + "--repository \"changeroa/StyleGallery\" \\", + "--execution-repository \"$GITHUB_REPOSITORY\" \\", + "\"repository\":\"%s\",\"execution_repository\":\"%s\"", + "\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", "permissions:", "contents: read", "", @@ -236,6 +241,21 @@ const cases = [ }, expect: ".github/workflows/validate.yml: broad Git safe.directory wildcard is forbidden", }, + { + name: "missing_canonical_repository_wiring", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("--repository \"changeroa/StyleGallery\" \\", "--repository \"$GITHUB_REPOSITORY\" \\") }, + expect: ".github/workflows/validate.yml: missing --repository \"changeroa/StyleGallery\" \\", + }, + { + name: "missing_execution_repository_wiring", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("--execution-repository \"$GITHUB_REPOSITORY\" \\\n", "") }, + expect: ".github/workflows/validate.yml: missing --execution-repository \"$GITHUB_REPOSITORY\" \\", + }, + { + name: "workflow_metadata_repository_mismatch", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", "\"$GITHUB_REPOSITORY\" \"changeroa/StyleGallery\" \"$GITHUB_RUN_ID\"") }, + expect: ".github/workflows/validate.yml: missing \"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", + }, { name: "success_path", expect: null }, ]; diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 0117b05..d894fe1 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -10,6 +10,7 @@ const failures = []; const warnings = []; const sentinelProvenanceClauses = [ "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", + "The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery.", "Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending.", "Linux/amd64 repeatability remains unclaimed until that external verification succeeds.", "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", @@ -146,6 +147,10 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "node scripts/test-summarize-sentinel-calibration.mjs"); requireIncludes(".github/workflows/validate.yml", "node scripts/validate-renderer-purity.mjs --json"); requireIncludes(".github/workflows/validate.yml", "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\""); + requireIncludes(".github/workflows/validate.yml", "--repository \"changeroa/StyleGallery\" \\"); + requireIncludes(".github/workflows/validate.yml", "--execution-repository \"$GITHUB_REPOSITORY\" \\"); + requireIncludes(".github/workflows/validate.yml", "\"repository\":\"%s\",\"execution_repository\":\"%s\""); + requireIncludes(".github/workflows/validate.yml", "\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\""); if (/safe\.directory(?:=|\s+)["']?\*["']?/.test(read(".github/workflows/validate.yml"))) { failures.push(".github/workflows/validate.yml: broad Git safe.directory wildcard is forbidden"); } From bb772d303c9dafea6e99d2c5713b4bd66e41a3bb Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 00:23:05 +0900 Subject: [PATCH 06/44] Record Linux Chromium calibration --- GOVERNANCE.md | 2 +- consumer-reference/baselines/calibration.json | 259 +++++++++++++++++- consumer-reference/contract.md | 2 +- .../schema/calibration-record.schema.json | 48 +++- quality/evidence/executable-evidence.md | 2 +- scripts/baseline-contract.mjs | 41 ++- scripts/summarize-sentinel-calibration.mjs | 5 +- .../test-summarize-sentinel-calibration.mjs | 9 +- scripts/test-validate-baseline-manifest.mjs | 116 ++++++-- scripts/test-validate-governance.mjs | 5 +- scripts/validate-governance.mjs | 5 +- 11 files changed, 449 insertions(+), 45 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 28f654f..7011f16 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -157,4 +157,4 @@ node scripts/test-renderer-purity.mjs Do not pass `--update-snapshots` in CI. Calibration runs exactly 20 times on the manifest's digest-pinned `linux/amd64` container and uploads raw Playwright JSON, strict exit records, PNG, DOM, AX, metadata, and post-assertion comparison evidence. Failed or incomplete calibration still uploads its truthful raw evidence without writing a completed aggregate. It remains nonblocking until the named owner explicitly approves the baseline. -Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. +Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. diff --git a/consumer-reference/baselines/calibration.json b/consumer-reference/baselines/calibration.json index 72f18b0..d10b807 100644 --- a/consumer-reference/baselines/calibration.json +++ b/consumer-reference/baselines/calibration.json @@ -1,23 +1,260 @@ { - "schema_version": "1.0", - "status": "awaiting_committed_ci", "baseline_owner_approval": "pending", - "required_runs": 20, - "committed_ci": null, + "committed_ci": { + "artifact_name": "chromium-sentinel-calibration-29260372260-18229be570766d3b42f5600955120bfcba690b76", + "checkout_sha": "18229be570766d3b42f5600955120bfcba690b76", + "execution_repository": "ark-jo/StyleGallery", + "head_sha": "3bfdd25ec9a2df4ca84e19541365d48842a73f59", + "raw_evidence_sha256": "1f125d5b321063b364e19283897c78c126d73eb1b3368d14686c494b1296dfab", + "repository": "changeroa/StyleGallery", + "run_attempt": "1", + "run_id": "29260372260", + "sha": "18229be570766d3b42f5600955120bfcba690b76", + "workflow": ".github/workflows/validate.yml", + "external_verification": { + "artifact": { + "api_digest": "sha256:3f11a517b447e1b5a1da17d9ee66ba2dd947fc8a0a482c447556ef00652e6074", + "expires_at": "2026-07-27T15:01:36Z", + "id": "8283099324", + "name": "chromium-sentinel-calibration-29260372260-18229be570766d3b42f5600955120bfcba690b76", + "size_in_bytes": 70310 + }, + "repository_relationship": { + "execution_is_fork": true, + "parent": "changeroa/StyleGallery", + "source": "changeroa/StyleGallery" + }, + "source": "https://github.com/ark-jo/StyleGallery/actions/runs/29260372260", + "verified_at": "2026-07-13T15:04:41Z" + } + }, "environment": { - "platform": "linux/amd64", "architecture": "amd64", + "browser_revision": "1228", + "browser_version": "149.0.7827.55", "container_image": "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", "node": "22", "os": "Ubuntu 24.04", + "platform": "linux/amd64", "playwright": "1.61.0", - "browser_revision": "1228", - "browser_version": "149.0.7827.55", - "viewport": { "width": 1024, "height": 768 } + "viewport": { + "height": 768, + "width": 1024 + } }, + "required_runs": 20, "reference": { - "source": { "path": "tests/helpers/render-consumer-reference.mjs", "sha256": "88802a948909d5e40470be6b5481766ce2de498e59c053ac68af370b46e72ca9" }, - "baseline": { "path": "tests/snapshots/consumer-reference-card-grid.png", "sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" } + "baseline": { + "path": "tests/snapshots/consumer-reference-card-grid.png", + "sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837" + }, + "source": { + "path": "tests/helpers/render-consumer-reference.mjs", + "sha256": "88802a948909d5e40470be6b5481766ce2de498e59c053ac68af370b46e72ca9" + } }, - "runs": [] + "runs": [ + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 1, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 2, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 3, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 4, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 5, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 6, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 7, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 8, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 9, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 10, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 11, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 12, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 13, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 14, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 15, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 16, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 17, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 18, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 19, + "screenshot_diff_pixels": 0 + }, + { + "architecture": "amd64", + "ax_sha256": "def6b05ea534a76b5362a436642f9790e464b4c6489a23118ff60b5eaee3aa1b", + "completed": true, + "dom_sha256": "ea28521d6b2e6b6d38b80a3a0411bddfd55da5f015b46271737c21c7f4098bb1", + "metadata_sha256": "43a98d03b473e29c78ba2d359cf5f39346288d55564e5bfe65bc4a4fd78ebc2c", + "png_sha256": "5528358e957a6115793155e501f62716f7db31dc1c86856d9e1234868d672837", + "run": 20, + "screenshot_diff_pixels": 0 + } + ], + "schema_version": "1.0", + "status": "completed" } diff --git a/consumer-reference/contract.md b/consumer-reference/contract.md index 31378b1..9b6eb75 100644 --- a/consumer-reference/contract.md +++ b/consumer-reference/contract.md @@ -50,7 +50,7 @@ Dependency flows from a consumer or profile to Layout. `layout/**`, `patterns/** The Chromium reference sentinel is an evidence probe, not a Layout theme or product conformance claim. Its proposed baseline is compared in the immutable `linux/amd64` Playwright image, CI never updates snapshots, and the ordinary sentinel remains nonblocking while `baseline_owner_approval` is `pending`. Twenty identical committed-workflow repeats calibrate repeatability; they do not substitute for explicit `@changeroa` baseline approval. Each aggregate run is derived from a zero exit record, the exact passing Playwright test identity, hash-verified PNG/DOM/AX bytes, and comparator proof written only after the zero-diff assertion passes; producer metadata cannot claim completion or a diff. -Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. +Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. The no-dependency validator blocks direct spellings and literal fragments assembled through concatenation, template literals, or `path.join`. This bounded static check does not claim general JavaScript data-flow analysis; guarded sources must not hide reference paths behind variables or runtime computation. diff --git a/consumer-reference/schema/calibration-record.schema.json b/consumer-reference/schema/calibration-record.schema.json index e723bdc..382c375 100644 --- a/consumer-reference/schema/calibration-record.schema.json +++ b/consumer-reference/schema/calibration-record.schema.json @@ -8,7 +8,7 @@ "required": ["schema_version", "status", "baseline_owner_approval", "required_runs", "committed_ci", "environment", "reference", "runs"], "properties": { "schema_version": { "const": "1.0" }, - "status": { "enum": ["awaiting_committed_ci", "completed"] }, + "status": { "enum": ["awaiting_committed_ci", "awaiting_external_verification", "completed"] }, "baseline_owner_approval": { "const": "pending" }, "required_runs": { "const": 20 }, "committed_ci": { @@ -17,7 +17,7 @@ { "type": "object", "additionalProperties": false, - "required": ["artifact_name", "checkout_sha", "execution_repository", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"], + "required": ["artifact_name", "checkout_sha", "execution_repository", "external_verification", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"], "properties": { "run_id": { "type": "string", "pattern": "^[0-9]+$" }, "run_attempt": { "type": "string", "pattern": "^[0-9]+$" }, @@ -27,6 +27,42 @@ "description": "Actual GitHub Actions repository, limited to the canonical upstream or the explicit calibration fork.", "enum": ["ark-jo/StyleGallery", "changeroa/StyleGallery"] }, + "external_verification": { + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": ["artifact", "repository_relationship", "source", "verified_at"], + "properties": { + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["api_digest", "expires_at", "id", "name", "size_in_bytes"], + "properties": { + "api_digest": { "const": "sha256:3f11a517b447e1b5a1da17d9ee66ba2dd947fc8a0a482c447556ef00652e6074" }, + "expires_at": { "const": "2026-07-27T15:01:36Z" }, + "id": { "const": "8283099324" }, + "name": { "const": "chromium-sentinel-calibration-29260372260-18229be570766d3b42f5600955120bfcba690b76" }, + "size_in_bytes": { "const": 70310 } + } + }, + "repository_relationship": { + "type": "object", + "additionalProperties": false, + "required": ["execution_is_fork", "parent", "source"], + "properties": { + "execution_is_fork": { "const": true }, + "parent": { "const": "changeroa/StyleGallery" }, + "source": { "const": "changeroa/StyleGallery" } + } + }, + "source": { "const": "https://github.com/ark-jo/StyleGallery/actions/runs/29260372260" }, + "verified_at": { "const": "2026-07-13T15:04:41Z" } + } + } + ] + }, "head_sha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, "raw_evidence_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "repository": { @@ -141,6 +177,14 @@ } } } + }, + { + "if": { "properties": { "status": { "const": "completed" } } }, + "then": { "properties": { "committed_ci": { "properties": { "external_verification": { "type": "object" } } } } }, + "else": { + "if": { "properties": { "status": { "const": "awaiting_external_verification" } } }, + "then": { "properties": { "committed_ci": { "properties": { "external_verification": { "type": "null" } } } } } + } } ] } diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index a21179b..b617539 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -31,7 +31,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity, source ownership, or local quality. | | Consumer-reference handoffs, schema/runtime parity, repository-local record paths, lifecycle separation, ownership truth, and dependency direction remain enforced. | `scripts/validate-consumer-reference.mjs` and `scripts/test-validate-consumer-reference.mjs` | `node scripts/validate-consumer-reference.mjs --json`; `node scripts/test-validate-consumer-reference.mjs --json` | Declared and reasoned-not-applicable fixtures exit zero, every shipped schema rule has accepted/rejected parity coverage, all repository handoffs declare applicability, fixtures remain related, and ownership discloses placeholder single-account review. | Unknown top-level or nested properties, invalid schema fields, missing handoff or reason, stable with ended support, boolean independence, unresolved/absolute/scheme/network/parent/item-or-record-symlink/non-JSON paths, scaffold success, fifth-domain classification, literal or supported computed reverse imports, and repository handoff omissions must fail with named finding codes and non-zero child exit. | Proves receiver-contract consistency, bounded static dependency detection, and path containment, not general JavaScript data-flow safety, visual quality, independent adoption, verified owner permissions, or consumer implementation conformance. | | Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | -| The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, Linux repeatability, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. Verification against the uploaded GitHub Actions run and artifact ID and digest remains pending. Linux/amd64 repeatability remains unclaimed until that external verification succeeds. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | +| The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | ## Validator Severity Contract diff --git a/scripts/baseline-contract.mjs b/scripts/baseline-contract.mjs index cfdc7ea..1baab46 100644 --- a/scripts/baseline-contract.mjs +++ b/scripts/baseline-contract.mjs @@ -24,6 +24,22 @@ export const BASELINE_REFERENCE = { export const BASELINE_METADATA_SHA256 = sha256(JSON.stringify({ environment: BASELINE_ENVIRONMENT, reference: BASELINE_REFERENCE })); export const CALIBRATION_CANONICAL_REPOSITORY = "changeroa/StyleGallery"; export const CALIBRATION_EXECUTION_REPOSITORIES = Object.freeze(["ark-jo/StyleGallery", CALIBRATION_CANONICAL_REPOSITORY]); +export const CALIBRATION_EXTERNAL_VERIFICATION = { + artifact: { + api_digest: "sha256:3f11a517b447e1b5a1da17d9ee66ba2dd947fc8a0a482c447556ef00652e6074", + expires_at: "2026-07-27T15:01:36Z", + id: "8283099324", + name: "chromium-sentinel-calibration-29260372260-18229be570766d3b42f5600955120bfcba690b76", + size_in_bytes: 70310, + }, + repository_relationship: { + execution_is_fork: true, + parent: CALIBRATION_CANONICAL_REPOSITORY, + source: CALIBRATION_CANONICAL_REPOSITORY, + }, + source: "https://github.com/ark-jo/StyleGallery/actions/runs/29260372260", + verified_at: "2026-07-13T15:04:41Z", +}; export function finding(code, file, message) { return { code, message, path: file }; @@ -88,10 +104,10 @@ export function validateCalibration(record, file) { if (record.runs.length !== 0 || record.committed_ci !== null) failures.push(finding("calibration_pending_evidence_forbidden", file, "pending calibration cannot claim runs or committed CI")); return failures; } - if (record.status !== "completed") return [...failures, finding("calibration_status_invalid", file, "status must be awaiting_committed_ci or completed")]; - if (!isRecord(record.committed_ci)) failures.push(finding("calibration_committed_ci_missing", file, "completed calibration requires committed CI metadata")); + if (!["awaiting_external_verification", "completed"].includes(record.status)) return [...failures, finding("calibration_status_invalid", file, "status must be awaiting_committed_ci, awaiting_external_verification, or completed")]; + if (!isRecord(record.committed_ci)) failures.push(finding("calibration_committed_ci_missing", file, "calibrated record requires committed CI metadata")); else { - rejectUnknown(record.committed_ci, new Set(["artifact_name", "checkout_sha", "execution_repository", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"]), "calibration_committed_ci_property_unknown", file, failures); + rejectUnknown(record.committed_ci, new Set(["artifact_name", "checkout_sha", "execution_repository", "external_verification", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"]), "calibration_committed_ci_property_unknown", file, failures); const expectedArtifact = `chromium-sentinel-calibration-${record.committed_ci.run_id}-${record.committed_ci.sha}`; if (!/^[0-9]+$/.test(record.committed_ci.run_id ?? "") || !/^[0-9]+$/.test(record.committed_ci.run_attempt ?? "") @@ -105,8 +121,25 @@ export function validateCalibration(record, file) { || record.committed_ci.artifact_name !== expectedArtifact) { failures.push(finding("calibration_committed_ci_invalid", file, "canonical and execution repositories, workflow, run, attempt, SHAs, raw evidence, and artifact name must have the canonical identity")); } + if (record.status === "awaiting_external_verification" && record.committed_ci.external_verification !== null) { + failures.push(finding("calibration_external_verification_preclaim", file, "awaiting calibration must not claim uploaded artifact verification")); + } + if (record.status === "completed") { + if (!("external_verification" in record.committed_ci) || record.committed_ci.external_verification === null) { + failures.push(finding("calibration_external_verification_missing", file, "completed calibration requires independently checked run and artifact metadata")); + } else { + const external = record.committed_ci.external_verification; + const expectedSource = `https://github.com/${record.committed_ci.execution_repository}/actions/runs/${record.committed_ci.run_id}`; + if (!sameValue(external, CALIBRATION_EXTERNAL_VERIFICATION) + || external?.source !== expectedSource + || external?.artifact?.name !== record.committed_ci.artifact_name + || external?.artifact?.api_digest === `sha256:${record.committed_ci.raw_evidence_sha256}`) { + failures.push(finding("calibration_external_verification_invalid", file, "external verification must equal the independently checked GitHub Actions run, fork relationship, and artifact API identity")); + } + } + } } - if (record.runs.length !== 20) failures.push(finding("calibration_run_count", file, "completed calibration requires exactly 20 runs")); + if (record.runs.length !== 20) failures.push(finding("calibration_run_count", file, "calibrated record requires exactly 20 runs")); const runAllowed = new Set(["architecture", "ax_sha256", "completed", "dom_sha256", "metadata_sha256", "png_sha256", "run", "screenshot_diff_pixels"]); for (const run of record.runs) { if (!isRecord(run)) { diff --git a/scripts/summarize-sentinel-calibration.mjs b/scripts/summarize-sentinel-calibration.mjs index 1280e3a..b144f6b 100644 --- a/scripts/summarize-sentinel-calibration.mjs +++ b/scripts/summarize-sentinel-calibration.mjs @@ -67,6 +67,7 @@ const record = { artifact_name: options.artifactName, checkout_sha: options.checkoutSha, execution_repository: options.executionRepository, + external_verification: null, head_sha: options.headSha, raw_evidence_sha256: rawEvidence, repository: options.repository, @@ -80,7 +81,7 @@ const record = { reference: BASELINE_REFERENCE, runs, schema_version: "1.0", - status: "completed", + status: "awaiting_external_verification", }; failures.push(...validateCalibration(record, options.output || "")); if (failures.length === 0) { @@ -91,7 +92,7 @@ if (failures.length === 0) { const diffs = runs.map((run) => run.screenshot_diff_pixels).sort((left, right) => left - right); const stats = diffs.length === 20 ? { max: diffs[19], mean: diffs.reduce((sum, value) => sum + value, 0) / 20, min: diffs[0], p95: diffs[18] } : null; const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()]; -const result = { failures: uniqueFailures, ok: uniqueFailures.length === 0, runs: runs.length, stats, status: uniqueFailures.length === 0 ? "completed" : "incomplete", warnings: [] }; +const result = { failures: uniqueFailures, ok: uniqueFailures.length === 0, runs: runs.length, stats, status: uniqueFailures.length === 0 ? "awaiting_external_verification" : "incomplete", warnings: [] }; if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); else if (!result.ok) process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.message}`).join("\n")}\n`); if (!result.ok) process.exitCode = 1; diff --git a/scripts/test-summarize-sentinel-calibration.mjs b/scripts/test-summarize-sentinel-calibration.mjs index 8d682c2..46a98d1 100644 --- a/scripts/test-summarize-sentinel-calibration.mjs +++ b/scripts/test-summarize-sentinel-calibration.mjs @@ -113,9 +113,11 @@ try { const valid = invoke("valid"); results.push({ actual: { outputRuns: valid.output?.runs.length, report: valid.report, status: valid.status }, - expected: "20 truth-derived stable runs and zero diff statistics", + expected: "20 truth-derived stable runs awaiting external verification and zero diff statistics", name: "valid_raw_calibration", - ok: valid.status === 0 && valid.report.ok === true && valid.report.status === "completed" + ok: valid.status === 0 && valid.report.ok === true && valid.report.status === "awaiting_external_verification" + && valid.output?.status === "awaiting_external_verification" + && valid.output?.committed_ci.external_verification === null && valid.output?.runs.length === 20 && new Set(valid.output.runs.map((run) => run.png_sha256)).size === 1, }); const pullRequestMergeSha = "11f4668fe5988720c27e88ec7203ecd1685a40df"; @@ -131,6 +133,9 @@ try { expected: "canonical upstream plus explicit execution fork with distinct merge/head SHAs", name: "valid_explicit_fork_pull_request_identity", ok: validFork.status === 0 && validFork.report.ok === true + && validFork.report.status === "awaiting_external_verification" + && validFork.output?.status === "awaiting_external_verification" + && validFork.output?.committed_ci.external_verification === null && validFork.output?.committed_ci.repository === "changeroa/StyleGallery" && validFork.output?.committed_ci.execution_repository === "ark-jo/StyleGallery" && validFork.output?.committed_ci.sha === pullRequestMergeSha diff --git a/scripts/test-validate-baseline-manifest.mjs b/scripts/test-validate-baseline-manifest.mjs index 8e2b200..8cde0a3 100644 --- a/scripts/test-validate-baseline-manifest.mjs +++ b/scripts/test-validate-baseline-manifest.mjs @@ -13,6 +13,38 @@ const canonicalManifest = JSON.parse(fs.readFileSync(path.join(repositoryRoot, " const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "calibration-record.schema.json"), "utf8")); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-calibration-")); const executionRepositories = ["ark-jo/StyleGallery", "changeroa/StyleGallery"]; +const completedEvidence = { + artifactId: "8283099324", + artifactName: "chromium-sentinel-calibration-29260372260-18229be570766d3b42f5600955120bfcba690b76", + artifactApiDigest: "sha256:3f11a517b447e1b5a1da17d9ee66ba2dd947fc8a0a482c447556ef00652e6074", + artifactExpiresAt: "2026-07-27T15:01:36Z", + artifactSize: 70310, + checkoutSha: "18229be570766d3b42f5600955120bfcba690b76", + headSha: "3bfdd25ec9a2df4ca84e19541365d48842a73f59", + rawEvidenceSha256: "1f125d5b321063b364e19283897c78c126d73eb1b3368d14686c494b1296dfab", + runId: "29260372260", + source: "https://github.com/ark-jo/StyleGallery/actions/runs/29260372260", + verifiedAt: "2026-07-13T15:04:41Z", +}; + +function externalVerification() { + return { + artifact: { + api_digest: completedEvidence.artifactApiDigest, + expires_at: completedEvidence.artifactExpiresAt, + id: completedEvidence.artifactId, + name: completedEvidence.artifactName, + size_in_bytes: completedEvidence.artifactSize, + }, + repository_relationship: { + execution_is_fork: true, + parent: "changeroa/StyleGallery", + source: "changeroa/StyleGallery", + }, + source: completedEvidence.source, + verified_at: completedEvidence.verifiedAt, + }; +} function completeRecord(committedCi = {}) { const runs = Array.from({ length: 20 }, (_, index) => ({ @@ -28,15 +60,16 @@ function completeRecord(committedCi = {}) { return { ...canonical, committed_ci: { - artifact_name: `chromium-sentinel-calibration-123-${"0".repeat(40)}`, - checkout_sha: "0".repeat(40), - execution_repository: "changeroa/StyleGallery", - head_sha: "1".repeat(40), - raw_evidence_sha256: "2".repeat(64), + artifact_name: completedEvidence.artifactName, + checkout_sha: completedEvidence.checkoutSha, + execution_repository: "ark-jo/StyleGallery", + external_verification: externalVerification(), + head_sha: completedEvidence.headSha, + raw_evidence_sha256: completedEvidence.rawEvidenceSha256, repository: "changeroa/StyleGallery", run_attempt: "1", - run_id: "123", - sha: "0".repeat(40), + run_id: completedEvidence.runId, + sha: completedEvidence.checkoutSha, workflow: ".github/workflows/validate.yml", ...committedCi, }, @@ -67,6 +100,15 @@ const cases = [ const { execution_repository: omitted, ...committedCi } = record.committed_ci; return { ...record, committed_ci: committedCi }; }, "calibration_committed_ci_invalid"], + ["completed_missing_external_verification", (record) => { + const { external_verification: omitted, ...committedCi } = record.committed_ci; + return { ...record, committed_ci: committedCi }; + }, "calibration_external_verification_missing"], + ["forged_external_artifact_id", (record) => ({ ...record, committed_ci: { ...record.committed_ci, external_verification: { ...record.committed_ci.external_verification, artifact: { ...record.committed_ci.external_verification.artifact, id: "8283099325" } } } }), "calibration_external_verification_invalid"], + ["forged_external_artifact_digest", (record) => ({ ...record, committed_ci: { ...record.committed_ci, external_verification: { ...record.committed_ci.external_verification, artifact: { ...record.committed_ci.external_verification.artifact, api_digest: `sha256:${"f".repeat(64)}` } } } }), "calibration_external_verification_invalid"], + ["raw_digest_substituted_for_artifact_digest", (record) => ({ ...record, committed_ci: { ...record.committed_ci, external_verification: { ...record.committed_ci.external_verification, artifact: { ...record.committed_ci.external_verification.artifact, api_digest: `sha256:${record.committed_ci.raw_evidence_sha256}` } } } }), "calibration_external_verification_invalid"], + ["forged_external_source", (record) => ({ ...record, committed_ci: { ...record.committed_ci, external_verification: { ...record.committed_ci.external_verification, source: "https://github.com/ark-jo/StyleGallery/actions/runs/1" } } }), "calibration_external_verification_invalid"], + ["forged_fork_parent_relationship", (record) => ({ ...record, committed_ci: { ...record.committed_ci, external_verification: { ...record.committed_ci.external_verification, repository_relationship: { ...record.committed_ci.external_verification.repository_relationship, parent: "untrusted/StyleGallery" } } } }), "calibration_external_verification_invalid"], ["invalid_committed_ci", (record) => ({ ...record, committed_ci: { artifact_name: "forged", extra: true, run_id: "abc", sha: "x" } }), "calibration_committed_ci_property_unknown"], ["owner_approval_preclaim", (record) => ({ ...record, baseline_owner_approval: "approved" }), "baseline_owner_approval_invalid"], ["pending_evidence_preclaim", (record) => ({ ...record, status: "awaiting_committed_ci" }), "calibration_pending_evidence_forbidden"], @@ -75,6 +117,9 @@ const cases = [ const results = []; try { const committedSchema = schema.properties.committed_ci.oneOf.find((entry) => entry.type === "object"); + const externalSchema = committedSchema.properties.external_verification?.oneOf?.find((entry) => entry.type === "object") ?? {}; + const artifactSchema = externalSchema.properties?.artifact ?? {}; + const relationshipSchema = externalSchema.properties?.repository_relationship ?? {}; const runSchema = schema.properties.runs.items; const runContains = schema.allOf[0].else.properties.runs.allOf; const schemaParity = schema.additionalProperties === false @@ -92,27 +137,64 @@ try { && runSchema.properties.metadata_sha256.const === BASELINE_METADATA_SHA256 && committedSchema.properties.repository.const === "changeroa/StyleGallery" && JSON.stringify(committedSchema.properties.execution_repository?.enum ?? []) === JSON.stringify(executionRepositories) + && externalSchema.additionalProperties === false + && artifactSchema.additionalProperties === false + && relationshipSchema.additionalProperties === false + && artifactSchema.properties?.api_digest?.const === completedEvidence.artifactApiDigest + && artifactSchema.properties?.id?.const === completedEvidence.artifactId + && artifactSchema.properties?.name?.const === completedEvidence.artifactName + && artifactSchema.properties?.size_in_bytes?.const === completedEvidence.artifactSize + && artifactSchema.properties?.expires_at?.const === completedEvidence.artifactExpiresAt + && externalSchema.properties?.source?.const === completedEvidence.source + && externalSchema.properties?.verified_at?.const === completedEvidence.verifiedAt && JSON.stringify(runContains.map((entry) => entry.contains.properties.run.const).sort((left, right) => left - right)) === JSON.stringify(Array.from({ length: 20 }, (_, index) => index + 1)) && runContains.every((entry) => entry.minContains === 1 && entry.maxContains === 1) - && JSON.stringify([...committedSchema.required].sort()) === JSON.stringify(["artifact_name", "checkout_sha", "execution_repository", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"].sort()); + && JSON.stringify([...committedSchema.required].sort()) === JSON.stringify(["artifact_name", "checkout_sha", "execution_repository", "external_verification", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"].sort()); results.push({ actual: { schemaParity }, expected: "recursive schema/runtime identity parity", name: "schema_runtime_parity", ok: schemaParity }); const validFixture = path.join(tempRoot, "valid-completed.json"); fs.writeFileSync(validFixture, `${JSON.stringify(completeRecord(), null, 2)}\n`); const validChild = spawnSync(process.execPath, [validator, "--calibration", validFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); const validOutput = JSON.parse(validChild.stdout); results.push({ actual: { codes: validOutput.failures.map((failure) => failure.code), status: validChild.status }, expected: "completed calibration and exit:0", name: "valid_completed_calibration", ok: validChild.status === 0 && validOutput.ok === true }); + const awaitingExternalFixture = path.join(tempRoot, "valid-awaiting-external-verification.json"); + fs.writeFileSync(awaitingExternalFixture, `${JSON.stringify({ + ...completeRecord({ external_verification: null }), + status: "awaiting_external_verification", + }, null, 2)}\n`); + const awaitingExternalChild = spawnSync(process.execPath, [validator, "--calibration", awaitingExternalFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const awaitingExternalOutput = JSON.parse(awaitingExternalChild.stdout); + results.push({ + actual: { codes: awaitingExternalOutput.failures.map((failure) => failure.code), status: awaitingExternalChild.status }, + expected: "pre-upload calibration awaits external verification and exits 0", + name: "valid_awaiting_external_verification", + ok: awaitingExternalChild.status === 0 && awaitingExternalOutput.ok === true, + }); + const pendingFixture = path.join(tempRoot, "valid-awaiting-committed-ci.json"); + fs.writeFileSync(pendingFixture, `${JSON.stringify({ ...canonical, committed_ci: null, runs: [], status: "awaiting_committed_ci" }, null, 2)}\n`); + const pendingChild = spawnSync(process.execPath, [validator, "--calibration", pendingFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const pendingOutput = JSON.parse(pendingChild.stdout); + results.push({ + actual: { codes: pendingOutput.failures.map((failure) => failure.code), status: pendingChild.status }, + expected: "initial pending record and exit:0", + name: "valid_awaiting_committed_ci", + ok: pendingChild.status === 0 && pendingOutput.ok === true, + }); const pullRequestFixture = path.join(tempRoot, "valid-pull-request-merge-head.json"); const mergeSha = "11f4668fe5988720c27e88ec7203ecd1685a40df"; const headSha = "8b8eaed41094286138973157b581a1d9ab9957a8"; - fs.writeFileSync(pullRequestFixture, `${JSON.stringify(completeRecord({ - artifact_name: `chromium-sentinel-calibration-29258810962-${mergeSha}`, - checkout_sha: mergeSha, - execution_repository: "ark-jo/StyleGallery", - head_sha: headSha, - repository: "changeroa/StyleGallery", - run_id: "29258810962", - sha: mergeSha, - }), null, 2)}\n`); + fs.writeFileSync(pullRequestFixture, `${JSON.stringify({ + ...completeRecord({ + artifact_name: `chromium-sentinel-calibration-29258810962-${mergeSha}`, + checkout_sha: mergeSha, + execution_repository: "ark-jo/StyleGallery", + external_verification: null, + head_sha: headSha, + repository: "changeroa/StyleGallery", + run_id: "29258810962", + sha: mergeSha, + }), + status: "awaiting_external_verification", + }, null, 2)}\n`); const pullRequestChild = spawnSync(process.execPath, [validator, "--calibration", pullRequestFixture, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); const pullRequestOutput = JSON.parse(pullRequestChild.stdout); results.push({ diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index c7ee4fd..c1e3503 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -11,8 +11,9 @@ const generatedWarning = " +# editorial button evidence coverage + +Claim boundary: Scenario artifacts are evidence, not visual regression baselines or accessibility certification. + +| Scenario | Required channels | Recorded channels | Pass records | Artifacts | +| --- | --- | --- | --- | --- | +| action-disabled-busy | visual, dom, ax | ax, dom, visual | 3 | runtime/editorial-action-disabled-busy.ax.json
runtime/editorial-action-disabled-busy.dom.json
runtime/editorial-action-disabled-busy.png | +| action-focused | visual, dom, ax | ax, dom, visual | 3 | runtime/editorial-action-focused.ax.json
runtime/editorial-action-focused.dom.json
runtime/editorial-action-focused.png | +| action-loading-busy | visual, dom, ax | ax, dom, visual | 3 | runtime/editorial-action-loading-busy.ax.json
runtime/editorial-action-loading-busy.dom.json
runtime/editorial-action-loading-busy.png | +| disclosure-expanded-loading | visual, dom, ax | ax, dom, visual | 3 | runtime/editorial-disclosure-expanded-loading.ax.json
runtime/editorial-disclosure-expanded-loading.dom.json
runtime/editorial-disclosure-expanded-loading.png | +| toggle-focused-pressed | visual, dom, ax | ax, dom, visual | 3 | runtime/editorial-toggle-focused-pressed.ax.json
runtime/editorial-toggle-focused-pressed.dom.json
runtime/editorial-toggle-focused-pressed.png | + +## IA Navigation + +Parent: [Keyboard Matrix](keyboard-matrix.md). +Next: [Reference Profiles](../../../index.md). diff --git a/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md b/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md new file mode 100644 index 0000000..f24ea2f --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md @@ -0,0 +1,19 @@ +--- +type: Generated Evidence +title: "editorial button keyboard matrix" +description: "Keyboard behavior and live fixture coverage derived from canonical JSON." +--- + + +# editorial button keyboard matrix + +| Mode | Element | Role | Keys | Behavior | Runtime scenario | +| --- | --- | --- | --- | --- | --- | +| action | button | button | Enter, Space | invoke | action-loading-busy | +| disclosure | button | button | Enter, Space | control | disclosure-expanded-loading | +| toggle | button | button | Enter, Space | toggle | toggle-focused-pressed | + +## IA Navigation + +Parent: [State Matrix](state-matrix.md). +Next: [Evidence Coverage](evidence-coverage.md). diff --git a/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md b/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md new file mode 100644 index 0000000..61f5ebb --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md @@ -0,0 +1,21 @@ +--- +type: Generated Evidence +title: "editorial button state matrix" +description: "Compound state sets and channel expectations derived from canonical JSON." +--- + + +# editorial button state matrix + +| Scenario | Mode | State set | ARIA | Visual | Activation | +| --- | --- | --- | --- | --- | --- | +| action-disabled-busy | action | busy, disabled | aria-busy=true
aria-disabled=true | busy, disabled | suppressed | +| action-focused | action | focus | none | focus, ready | allowed | +| action-loading-busy | action | busy, focus, loading | aria-busy=true | busy, focus, loading | suppressed | +| disclosure-expanded-loading | disclosure | busy, expanded, focus, loading | aria-busy=true
aria-expanded=true | busy, expanded, focus, loading | allowed | +| toggle-focused-pressed | toggle | focus, pressed | aria-pressed=true | focus, pressed | allowed | + +## IA Navigation + +Parent: [Governed Local Profiles](../../index.md). +Next: [Keyboard Matrix](keyboard-matrix.md). diff --git a/design-engineering/reference-profiles/governed-local/editorial/profile.json b/design-engineering/reference-profiles/governed-local/editorial/profile.json index b9b4365..0a5a592 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/profile.json +++ b/design-engineering/reference-profiles/governed-local/editorial/profile.json @@ -1,5 +1,6 @@ { "artifact_mode": "governed_local", + "component_records": ["components/button.component.json"], "default": false, "environment_assumptions": { "reset": { @@ -10,7 +11,10 @@ "user_agent_styles": "Preserve semantic browser defaults except for the declared reset." }, "example_only": true, + "evidence_records": ["evidence/button.evidence.json"], "fixture_independence": "related", + "fixture_records": ["fixtures/button.fixture.json"], + "generated_records": ["generated/evidence-coverage.md", "generated/keyboard-matrix.md", "generated/state-matrix.md"], "handoff": { "record": "design-engineering/reference-profiles/governed-local/editorial/profile.json", "status": "declared" @@ -34,5 +38,6 @@ "support": { "status": "active" }, + "state_records": ["states/button.states.json"], "tokens": "tokens.dtcg.json" } diff --git a/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json b/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json new file mode 100644 index 0000000..6d0ee1e --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json @@ -0,0 +1,155 @@ +{ + "schema_version": "1.0", + "record_kind": "state_set", + "component_id": "button", + "profile_id": "editorial-reference-profile", + "scenarios": [ + { + "id": "action-disabled-busy", + "semantic_mode": "action", + "states": [ + "busy", + "disabled" + ], + "aria": { + "busy": "true", + "disabled": "true" + }, + "expected": { + "visual": [ + "busy", + "disabled" + ], + "dom": { + "active": "false", + "aria-busy": "true", + "disabled": "true", + "role": "button" + }, + "ax": { + "busy": true, + "disabled": true, + "focused": false, + "role": "button" + }, + "activation": "suppressed" + } + }, + { + "id": "action-focused", + "semantic_mode": "action", + "states": [ + "focus" + ], + "aria": {}, + "expected": { + "visual": [ + "focus", + "ready" + ], + "dom": { + "active": "true", + "role": "button" + }, + "ax": { + "focused": true, + "role": "button" + }, + "activation": "allowed" + } + }, + { + "id": "action-loading-busy", + "semantic_mode": "action", + "states": [ + "busy", + "focus", + "loading" + ], + "aria": { + "busy": "true" + }, + "expected": { + "visual": [ + "busy", + "focus", + "loading" + ], + "dom": { + "active": "true", + "aria-busy": "true", + "role": "button" + }, + "ax": { + "busy": true, + "focused": true, + "role": "button" + }, + "activation": "suppressed" + } + }, + { + "id": "disclosure-expanded-loading", + "semantic_mode": "disclosure", + "states": [ + "busy", + "expanded", + "focus", + "loading" + ], + "aria": { + "busy": "true", + "expanded": "true" + }, + "expected": { + "visual": [ + "busy", + "expanded", + "focus", + "loading" + ], + "dom": { + "active": "true", + "aria-busy": "true", + "aria-expanded": "true", + "role": "button" + }, + "ax": { + "busy": true, + "expanded": true, + "focused": true, + "role": "button" + }, + "activation": "allowed" + } + }, + { + "id": "toggle-focused-pressed", + "semantic_mode": "toggle", + "states": [ + "focus", + "pressed" + ], + "aria": { + "pressed": "true" + }, + "expected": { + "visual": [ + "focus", + "pressed" + ], + "dom": { + "active": "true", + "aria-pressed": "true", + "role": "button" + }, + "ax": { + "focused": true, + "pressed": true, + "role": "button" + }, + "activation": "allowed" + } + } + ] +} diff --git a/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json b/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json new file mode 100644 index 0000000..0a62aa4 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json @@ -0,0 +1,125 @@ +{ + "schema_version": "1.0", + "record_kind": "component", + "component_id": "button", + "profile_id": "terminal-reference-profile", + "anatomy": [ + "control", + "label", + "status" + ], + "semantic_modes": [ + { + "id": "action", + "element": "button", + "role": "button", + "keys": [ + "Enter", + "Space" + ], + "aria_states": [ + { + "name": "pressed", + "status": "prohibited", + "reason": "Action buttons do not expose toggle state." + }, + { + "name": "expanded", + "status": "not_applicable", + "reason": "Action buttons do not control disclosed content." + }, + { + "name": "busy", + "status": "conditional", + "condition": "An invoked action is still processing.", + "resolution": "required" + }, + { + "name": "disabled", + "status": "conditional", + "condition": "The action is unavailable to the current user.", + "resolution": "required" + } + ], + "activation": "invoke" + }, + { + "id": "toggle", + "element": "button", + "role": "button", + "keys": [ + "Enter", + "Space" + ], + "aria_states": [ + { + "name": "pressed", + "status": "required", + "values": [ + "false", + "true" + ] + }, + { + "name": "expanded", + "status": "prohibited", + "reason": "Toggle buttons expose pressed rather than disclosure state." + }, + { + "name": "busy", + "status": "not_applicable", + "reason": "The governed toggle fixture has no asynchronous operation." + }, + { + "name": "disabled", + "status": "not_applicable", + "reason": "The governed toggle fixture remains available for activation." + } + ], + "activation": "toggle" + }, + { + "id": "disclosure", + "element": "button", + "role": "button", + "keys": [ + "Enter", + "Space" + ], + "aria_states": [ + { + "name": "expanded", + "status": "required", + "values": [ + "false", + "true" + ] + }, + { + "name": "pressed", + "status": "prohibited", + "reason": "Disclosure buttons expose expanded rather than toggle state." + }, + { + "name": "busy", + "status": "conditional", + "condition": "Expanded controlled content is still loading.", + "resolution": "required" + }, + { + "name": "disabled", + "status": "not_applicable", + "reason": "The governed disclosure fixture remains available for activation." + } + ], + "activation": "control" + } + ], + "versions": { + "design_revision": "2026.07.14", + "package_version": "0.1.0-preview.1", + "api_version": "1.0", + "delivery_channel": "preview", + "lifecycle": "experimental" + } +} diff --git a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json new file mode 100644 index 0000000..763b7f2 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json @@ -0,0 +1,954 @@ +{ + "claim_boundary": "Scenario artifacts are evidence, not visual regression baselines or accessibility certification.", + "component_id": "button", + "passes": [ + { + "artifact": { + "path": "runtime/terminal-action-disabled-busy.png", + "byte_length": 3599, + "media_type": "image/png", + "sha256": "sha256:d346be9e96750f636f29078d17d9871a04c2f4442b6849b029fa754c2795674e", + "height": 56, + "width": 640 + }, + "channel": "visual", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-disabled-busy-visual", + "recorded_at": "2026-07-13T17:57:28.259Z", + "result": { + "observed": "Recorded visual evidence for action-disabled-busy; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-disabled-busy", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "busy", + "disabled" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-disabled-busy.dom.json", + "byte_length": 1285, + "media_type": "application/json", + "sha256": "sha256:b81b10289290e069b73a81481b6b2bc0705a625f0dbd6244c5608e684f58819f" + }, + "channel": "dom", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-disabled-busy-dom", + "recorded_at": "2026-07-13T17:57:28.259Z", + "result": { + "observed": "Recorded dom evidence for action-disabled-busy; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-disabled-busy", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "busy", + "disabled" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-disabled-busy.ax.json", + "byte_length": 1204, + "media_type": "application/json", + "sha256": "sha256:282e01575951673bb419bf2064984fe7bb8468d93c296f6fc8f55165347594f1" + }, + "channel": "ax", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-disabled-busy-ax", + "recorded_at": "2026-07-13T17:57:28.259Z", + "result": { + "observed": "Recorded ax evidence for action-disabled-busy; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-disabled-busy", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "busy", + "disabled" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-focused.png", + "byte_length": 2470, + "media_type": "image/png", + "sha256": "sha256:39b96bc2459e2f0f442ce7b709f22e46b44f49b17c52d61940dff0246108ec48", + "height": 56, + "width": 640 + }, + "channel": "visual", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-focused-visual", + "recorded_at": "2026-07-13T17:57:28.366Z", + "result": { + "observed": "Recorded visual evidence for action-focused; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-focused", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "focus" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-focused.dom.json", + "byte_length": 1225, + "media_type": "application/json", + "sha256": "sha256:fcbed06dfa9cfbc4e764a9c4bd3bb0dbc90920d72f8cf9698095eb23d1317e38" + }, + "channel": "dom", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-focused-dom", + "recorded_at": "2026-07-13T17:57:28.366Z", + "result": { + "observed": "Recorded dom evidence for action-focused; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-focused", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "focus" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-focused.ax.json", + "byte_length": 1149, + "media_type": "application/json", + "sha256": "sha256:ec9240455b30b17b3d09d7a5a9d3d5488ff8ea10caff2a03524e861210609631" + }, + "channel": "ax", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-focused-ax", + "recorded_at": "2026-07-13T17:57:28.366Z", + "result": { + "observed": "Recorded ax evidence for action-focused; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-focused", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "focus" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-loading-busy.png", + "byte_length": 3660, + "media_type": "image/png", + "sha256": "sha256:05cbe173a8274cfa7e6aab05713d1cb925820a7412bffceadb94d83498c17747", + "height": 56, + "width": 640 + }, + "channel": "visual", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-loading-busy-visual", + "recorded_at": "2026-07-13T17:57:28.482Z", + "result": { + "observed": "Recorded visual evidence for action-loading-busy; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-loading-busy", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "busy", + "focus", + "loading" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-loading-busy.dom.json", + "byte_length": 1271, + "media_type": "application/json", + "sha256": "sha256:fd96628699bc5b1035160e66fff9a35430c1aa38657c9d3b410825151566ba98" + }, + "channel": "dom", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-loading-busy-dom", + "recorded_at": "2026-07-13T17:57:28.482Z", + "result": { + "observed": "Recorded dom evidence for action-loading-busy; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-loading-busy", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "busy", + "focus", + "loading" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-action-loading-busy.ax.json", + "byte_length": 1176, + "media_type": "application/json", + "sha256": "sha256:051996c60a57e0934046e63cc32a0bcad8f7bd0e298c571ab57b0cc2ca0e0375" + }, + "channel": "ax", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "action-loading-busy-ax", + "recorded_at": "2026-07-13T17:57:28.482Z", + "result": { + "observed": "Recorded ax evidence for action-loading-busy; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "action-loading-busy", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "action", + "state_set": [ + "busy", + "focus", + "loading" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-disclosure-expanded-loading.png", + "byte_length": 8556, + "media_type": "image/png", + "sha256": "sha256:d8c7ba056dd2a61ce6c85d8ec6bf56733cdc34da07678dd9b4175c7de9ab926a", + "height": 88, + "width": 640 + }, + "channel": "visual", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "disclosure-expanded-loading-visual", + "recorded_at": "2026-07-13T17:57:28.602Z", + "result": { + "observed": "Recorded visual evidence for disclosure-expanded-loading; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "disclosure-expanded-loading", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "disclosure", + "state_set": [ + "busy", + "expanded", + "focus", + "loading" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-disclosure-expanded-loading.dom.json", + "byte_length": 1328, + "media_type": "application/json", + "sha256": "sha256:d6c84c4b2edeb5306cc3606a3f79589e385c8d46c5fad2f54e4fb5eedd258f8f" + }, + "channel": "dom", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "disclosure-expanded-loading-dom", + "recorded_at": "2026-07-13T17:57:28.602Z", + "result": { + "observed": "Recorded dom evidence for disclosure-expanded-loading; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "disclosure-expanded-loading", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "disclosure", + "state_set": [ + "busy", + "expanded", + "focus", + "loading" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-disclosure-expanded-loading.ax.json", + "byte_length": 1211, + "media_type": "application/json", + "sha256": "sha256:4db17e7bbe59a29cf409a798d28f2f91b39dde203f6704df5e3e3038d9e96fd4" + }, + "channel": "ax", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "disclosure-expanded-loading-ax", + "recorded_at": "2026-07-13T17:57:28.602Z", + "result": { + "observed": "Recorded ax evidence for disclosure-expanded-loading; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "disclosure-expanded-loading", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "disclosure", + "state_set": [ + "busy", + "expanded", + "focus", + "loading" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-toggle-focused-pressed.png", + "byte_length": 2070, + "media_type": "image/png", + "sha256": "sha256:838229be984b8bda1ebfe6095564f1093df18fdd2ca1e6c3d5b5c086162d4692", + "height": 56, + "width": 640 + }, + "channel": "visual", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "toggle-focused-pressed-visual", + "recorded_at": "2026-07-13T17:57:28.707Z", + "result": { + "observed": "Recorded visual evidence for toggle-focused-pressed; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "toggle-focused-pressed", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "toggle", + "state_set": [ + "focus", + "pressed" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-toggle-focused-pressed.dom.json", + "byte_length": 1265, + "media_type": "application/json", + "sha256": "sha256:643b8f352ff6895eff1c44f95d6655b363da6ea269b5f0d560150afa931156bd" + }, + "channel": "dom", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "toggle-focused-pressed-dom", + "recorded_at": "2026-07-13T17:57:28.707Z", + "result": { + "observed": "Recorded dom evidence for toggle-focused-pressed; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "toggle-focused-pressed", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "toggle", + "state_set": [ + "focus", + "pressed" + ] + } + }, + { + "artifact": { + "path": "runtime/terminal-toggle-focused-pressed.ax.json", + "byte_length": 1177, + "media_type": "application/json", + "sha256": "sha256:d5f2271c832538821cfee9683b873243cc5f51aa1f0fa7912d8fc1878a7fbeec" + }, + "channel": "ax", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "id": "toggle-focused-pressed-ax", + "recorded_at": "2026-07-13T17:57:28.707Z", + "result": { + "observed": "Recorded ax evidence for toggle-focused-pressed; this is not certification.", + "status": "passed" + }, + "run": { + "attempt": 1, + "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "repository": "changeroa/StyleGallery", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "source": "local" + }, + "scenario_id": "toggle-focused-pressed", + "session": { + "attempt": 1, + "branch": "codex/roadmap-05-state-evidence", + "environment": { + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "kind": "browser", + "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "node": "v22.23.1", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", + "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", + "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "started_at": "2026-07-13T17:57:26.704Z" + }, + "scope": { + "component": "button", + "semantic_mode": "toggle", + "state_set": [ + "focus", + "pressed" + ] + } + } + ], + "profile_id": "terminal-reference-profile", + "schema_version": "1.0" +} diff --git a/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json b/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json new file mode 100644 index 0000000..2df938c --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json @@ -0,0 +1,65 @@ +{ + "schema_version": "1.0", + "fixture_id": "terminal-button-runtime", + "component_id": "button", + "profile_id": "terminal-reference-profile", + "component_record": "../components/button.component.json", + "state_record": "../states/button.states.json", + "scenarios": [ + { + "id": "action-disabled-busy", + "semantic_mode": "action", + "label": "Command unavailable", + "activation_key": "Enter", + "required_channels": [ + "visual", + "dom", + "ax" + ] + }, + { + "id": "action-focused", + "semantic_mode": "action", + "label": "Run command", + "activation_key": "Enter", + "required_channels": [ + "visual", + "dom", + "ax" + ] + }, + { + "id": "action-loading-busy", + "semantic_mode": "action", + "label": "Command running", + "activation_key": "Enter", + "required_channels": [ + "visual", + "dom", + "ax" + ] + }, + { + "id": "disclosure-expanded-loading", + "semantic_mode": "disclosure", + "label": "Show process log", + "activation_key": "Enter", + "required_channels": [ + "visual", + "dom", + "ax" + ] + }, + { + "id": "toggle-focused-pressed", + "semantic_mode": "toggle", + "label": "Pin output", + "activation_key": "Space", + "required_channels": [ + "visual", + "dom", + "ax" + ] + } + ] +} diff --git a/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md b/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md new file mode 100644 index 0000000..04fc7ba --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md @@ -0,0 +1,23 @@ +--- +type: Generated Evidence +title: "terminal button evidence coverage" +description: "Claim-scoped evidence channels and artifacts derived from canonical JSON." +--- + + +# terminal button evidence coverage + +Claim boundary: Scenario artifacts are evidence, not visual regression baselines or accessibility certification. + +| Scenario | Required channels | Recorded channels | Pass records | Artifacts | +| --- | --- | --- | --- | --- | +| action-disabled-busy | visual, dom, ax | ax, dom, visual | 3 | runtime/terminal-action-disabled-busy.ax.json
runtime/terminal-action-disabled-busy.dom.json
runtime/terminal-action-disabled-busy.png | +| action-focused | visual, dom, ax | ax, dom, visual | 3 | runtime/terminal-action-focused.ax.json
runtime/terminal-action-focused.dom.json
runtime/terminal-action-focused.png | +| action-loading-busy | visual, dom, ax | ax, dom, visual | 3 | runtime/terminal-action-loading-busy.ax.json
runtime/terminal-action-loading-busy.dom.json
runtime/terminal-action-loading-busy.png | +| disclosure-expanded-loading | visual, dom, ax | ax, dom, visual | 3 | runtime/terminal-disclosure-expanded-loading.ax.json
runtime/terminal-disclosure-expanded-loading.dom.json
runtime/terminal-disclosure-expanded-loading.png | +| toggle-focused-pressed | visual, dom, ax | ax, dom, visual | 3 | runtime/terminal-toggle-focused-pressed.ax.json
runtime/terminal-toggle-focused-pressed.dom.json
runtime/terminal-toggle-focused-pressed.png | + +## IA Navigation + +Parent: [Keyboard Matrix](keyboard-matrix.md). +Next: [Reference Profiles](../../../index.md). diff --git a/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md b/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md new file mode 100644 index 0000000..fc8c83f --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md @@ -0,0 +1,19 @@ +--- +type: Generated Evidence +title: "terminal button keyboard matrix" +description: "Keyboard behavior and live fixture coverage derived from canonical JSON." +--- + + +# terminal button keyboard matrix + +| Mode | Element | Role | Keys | Behavior | Runtime scenario | +| --- | --- | --- | --- | --- | --- | +| action | button | button | Enter, Space | invoke | action-loading-busy | +| disclosure | button | button | Enter, Space | control | disclosure-expanded-loading | +| toggle | button | button | Enter, Space | toggle | toggle-focused-pressed | + +## IA Navigation + +Parent: [State Matrix](state-matrix.md). +Next: [Evidence Coverage](evidence-coverage.md). diff --git a/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md b/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md new file mode 100644 index 0000000..7169d87 --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md @@ -0,0 +1,21 @@ +--- +type: Generated Evidence +title: "terminal button state matrix" +description: "Compound state sets and channel expectations derived from canonical JSON." +--- + + +# terminal button state matrix + +| Scenario | Mode | State set | ARIA | Visual | Activation | +| --- | --- | --- | --- | --- | --- | +| action-disabled-busy | action | busy, disabled | aria-busy=true
aria-disabled=true | busy, disabled | suppressed | +| action-focused | action | focus | none | focus, ready | allowed | +| action-loading-busy | action | busy, focus, loading | aria-busy=true | busy, focus, loading | suppressed | +| disclosure-expanded-loading | disclosure | busy, expanded, focus, loading | aria-busy=true
aria-expanded=true | busy, expanded, focus, loading | allowed | +| toggle-focused-pressed | toggle | focus, pressed | aria-pressed=true | focus, pressed | allowed | + +## IA Navigation + +Parent: [Governed Local Profiles](../../index.md). +Next: [Keyboard Matrix](keyboard-matrix.md). diff --git a/design-engineering/reference-profiles/governed-local/terminal/profile.json b/design-engineering/reference-profiles/governed-local/terminal/profile.json index 6dc353c..cdeb352 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/profile.json +++ b/design-engineering/reference-profiles/governed-local/terminal/profile.json @@ -1,5 +1,6 @@ { "artifact_mode": "governed_local", + "component_records": ["components/button.component.json"], "default": false, "environment_assumptions": { "reset": { @@ -10,7 +11,10 @@ "user_agent_styles": "Preserve semantic browser defaults except for the declared reset." }, "example_only": true, + "evidence_records": ["evidence/button.evidence.json"], "fixture_independence": "related", + "fixture_records": ["fixtures/button.fixture.json"], + "generated_records": ["generated/evidence-coverage.md", "generated/keyboard-matrix.md", "generated/state-matrix.md"], "handoff": { "record": "design-engineering/reference-profiles/governed-local/terminal/profile.json", "status": "declared" @@ -34,5 +38,6 @@ "support": { "status": "active" }, + "state_records": ["states/button.states.json"], "tokens": "tokens.dtcg.json" } diff --git a/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json b/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json new file mode 100644 index 0000000..1209e8d --- /dev/null +++ b/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json @@ -0,0 +1,155 @@ +{ + "schema_version": "1.0", + "record_kind": "state_set", + "component_id": "button", + "profile_id": "terminal-reference-profile", + "scenarios": [ + { + "id": "action-disabled-busy", + "semantic_mode": "action", + "states": [ + "busy", + "disabled" + ], + "aria": { + "busy": "true", + "disabled": "true" + }, + "expected": { + "visual": [ + "busy", + "disabled" + ], + "dom": { + "active": "false", + "aria-busy": "true", + "disabled": "true", + "role": "button" + }, + "ax": { + "busy": true, + "disabled": true, + "focused": false, + "role": "button" + }, + "activation": "suppressed" + } + }, + { + "id": "action-focused", + "semantic_mode": "action", + "states": [ + "focus" + ], + "aria": {}, + "expected": { + "visual": [ + "focus", + "ready" + ], + "dom": { + "active": "true", + "role": "button" + }, + "ax": { + "focused": true, + "role": "button" + }, + "activation": "allowed" + } + }, + { + "id": "action-loading-busy", + "semantic_mode": "action", + "states": [ + "busy", + "focus", + "loading" + ], + "aria": { + "busy": "true" + }, + "expected": { + "visual": [ + "busy", + "focus", + "loading" + ], + "dom": { + "active": "true", + "aria-busy": "true", + "role": "button" + }, + "ax": { + "busy": true, + "focused": true, + "role": "button" + }, + "activation": "suppressed" + } + }, + { + "id": "disclosure-expanded-loading", + "semantic_mode": "disclosure", + "states": [ + "busy", + "expanded", + "focus", + "loading" + ], + "aria": { + "busy": "true", + "expanded": "true" + }, + "expected": { + "visual": [ + "busy", + "expanded", + "focus", + "loading" + ], + "dom": { + "active": "true", + "aria-busy": "true", + "aria-expanded": "true", + "role": "button" + }, + "ax": { + "busy": true, + "expanded": true, + "focused": true, + "role": "button" + }, + "activation": "allowed" + } + }, + { + "id": "toggle-focused-pressed", + "semantic_mode": "toggle", + "states": [ + "focus", + "pressed" + ], + "aria": { + "pressed": "true" + }, + "expected": { + "visual": [ + "focus", + "pressed" + ], + "dom": { + "active": "true", + "aria-pressed": "true", + "role": "button" + }, + "ax": { + "focused": true, + "pressed": true, + "role": "button" + }, + "activation": "allowed" + } + } + ] +} diff --git a/package.json b/package.json index fb3944d..1771c17 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,14 @@ }, "scripts": { "build": "node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json", + "create:component-state-session": "node scripts/create-component-state-session.mjs", "test": "node scripts/test-reference-adapters.mjs --json", + "test:component-state": "node scripts/test-validate-component-state.mjs --json && node scripts/test-validate-component-state-artifacts.mjs && node scripts/test-generate-consumer-reference-evidence.mjs --json", + "test:component-state:runtime": "playwright test tests/component-state-evidence.spec.mjs --project=chromium", + "test:component-state:runtime-negative": "node scripts/test-component-state-sentinel.mjs", "test:sentinel": "playwright test tests/consumer-reference-sentinels.spec.mjs --project=chromium", - "validate": "node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json" + "validate": "node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json", + "validate:component-state": "node scripts/validate-component-state.mjs --json && node scripts/generate-consumer-reference-evidence.mjs --check --json" }, "devDependencies": { "@playwright/test": "1.61.0", diff --git a/scripts/capture-session-contract.mjs b/scripts/capture-session-contract.mjs new file mode 100644 index 0000000..a07c0e1 --- /dev/null +++ b/scripts/capture-session-contract.mjs @@ -0,0 +1,90 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { resolveProfileRecords } from "./profile-record-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +function finding(code, file, message) { + return { code, message, path: file }; +} + +export function sha256(bytes) { + return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`; +} + +export function sessionLink(receipt, receiptSha256) { + return { + attempt: receipt.attempt, + branch: receipt.branch, + environment: receipt.environment, + nonce: receipt.nonce, + receipt_sha256: receiptSha256, + revision: receipt.revision, + session_id: receipt.session_id, + started_at: receipt.started_at, + }; +} + +export function readCaptureSession(file, validate, failures) { + if (!fs.existsSync(file)) { + failures.push(finding("capture_session_missing", file, "capture session receipt is missing")); + return undefined; + } + const stat = fs.lstatSync(file); + if (!stat.isFile() || stat.isSymbolicLink()) { + failures.push(finding("capture_session_type_invalid", file, "capture session receipt must be a regular non-symlink file")); + return undefined; + } + const bytes = fs.readFileSync(file); + let receipt; + try { + receipt = parseStrictJson(bytes.toString("utf8")); + } catch (error) { + failures.push(finding("capture_session_json_invalid", file, error instanceof Error ? error.message : String(error))); + return undefined; + } + if (!validate(receipt)) { + for (const error of validate.errors ?? []) failures.push(finding("capture_session_schema_invalid", file, `${error.instancePath || "/"} ${error.message}`)); + } + const digest = sha256(bytes); + return { + artifact: { byte_length: bytes.length, media_type: "application/json", path: "capture-session.json", sha256: digest }, + bytes, + digest, + link: sessionLink(receipt, digest), + receipt, + }; +} + +function compare(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +export function canonicalIntended(profileRoot, failures) { + if (!fs.existsSync(profileRoot)) return []; + const intended = []; + for (const entry of fs.readdirSync(profileRoot, { withFileTypes: true }).filter((item) => item.isDirectory()).sort((left, right) => compare(left.name, right.name))) { + const root = path.join(profileRoot, entry.name); + if (!fs.existsSync(path.join(root, "profile.json"))) continue; + const resolved = resolveProfileRecords(root, failures); + const fixture = resolved?.records.fixture[0]?.value; + if (!resolved || !fixture) continue; + intended.push({ + profile_id: resolved.profile.id, + profile_name: entry.name, + scenarios: fixture.scenarios.map((scenario) => ({ channels: [...scenario.required_channels].sort(compare), id: scenario.id })).sort((left, right) => compare(left.id, right.id)), + }); + } + return intended; +} + +export function sameJson(left, right) { + return JSON.stringify(left) === JSON.stringify(right); +} + +export function withinSession(capturedAt, startedAt, completedAt) { + const captured = Date.parse(capturedAt); + const started = Date.parse(startedAt); + const completed = Date.parse(completedAt); + return Number.isFinite(captured) && Number.isFinite(started) && Number.isFinite(completed) && captured >= started && captured <= completed; +} diff --git a/scripts/component-state-contract.mjs b/scripts/component-state-contract.mjs new file mode 100644 index 0000000..62d49b5 --- /dev/null +++ b/scripts/component-state-contract.mjs @@ -0,0 +1,86 @@ +import fs from "node:fs"; +import path from "node:path"; +import Ajv2020 from "ajv/dist/2020.js"; +import { + validateComponentSemantics, + validateEvidenceSemantics, + validateFixtureSemantics, + validateStateSemantics, +} from "./component-state-semantics.mjs"; +import { validateEvidenceArtifacts as validateArtifactSet } from "./evidence-artifact-contract.mjs"; +import { resolveProfileRecords } from "./profile-record-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +function finding(code, file, message) { + return { code, message, path: file }; +} + +export function readRecord(file, failures) { + try { + return parseStrictJson(fs.readFileSync(file, "utf8")); + } catch (error) { + failures.push(finding(error instanceof SyntaxError ? "record_invalid_json" : "record_unreadable", file, error.message)); + return undefined; + } +} + +export function compileSchemas(schemaRoot) { + const ajv = new Ajv2020({ allErrors: true, formats: { "date-time": true }, strict: false }); + const captureSchema = JSON.parse(fs.readFileSync(path.join(schemaRoot, "capture-session.schema.json"), "utf8")); + ajv.addSchema(captureSchema); + const evidenceSchema = JSON.parse(fs.readFileSync(path.join(schemaRoot, "evidence-record.schema.json"), "utf8")); + const validateEvidence = ajv.compile(evidenceSchema); + return { + ax: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "ax-evidence.schema.json"), "utf8"))), + capture: ajv.getSchema(captureSchema.$id), + component: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "component-state.schema.json"), "utf8"))), + dom: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "dom-evidence.schema.json"), "utf8"))), + evidence: validateEvidence, + fixture: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "fixture-manifest.schema.json"), "utf8"))), + item: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "item.schema.json"), "utf8"))), + runtime: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "runtime-evidence-manifest.schema.json"), "utf8"))), + }; +} + +function addSchemaFailures(validate, value, file, code, failures) { + if (validate(value)) return; + for (const error of validate.errors ?? []) failures.push(finding(code, file, `${error.instancePath || "/"} ${error.message}`)); +} + +export function validateProfile(profileRoot, schemas) { + const failures = []; + const resolved = resolveProfileRecords(profileRoot, failures); + if (!resolved) return failures; + addSchemaFailures(schemas.item, resolved.profile, resolved.profileFile, "profile_schema_invalid", failures); + const files = Object.fromEntries(["component", "evidence", "fixture", "states"].map((kind) => [kind, resolved.records[kind][0]?.path ?? resolved.profileFile])); + const records = Object.fromEntries(["component", "evidence", "fixture", "states"].map((kind) => [kind, resolved.records[kind][0]?.value])); + if (records.component) addSchemaFailures(schemas.component, records.component, files.component, "component_schema_invalid", failures); + if (records.states) addSchemaFailures(schemas.component, records.states, files.states, "state_schema_invalid", failures); + if (records.fixture) addSchemaFailures(schemas.fixture, records.fixture, files.fixture, "fixture_schema_invalid", failures); + if (records.evidence) { + addSchemaFailures(schemas.evidence, records.evidence, files.evidence, "evidence_schema_invalid", failures); + } + if (records.component) validateComponentSemantics(records.component, files.component, failures); + if (records.states && records.component) validateStateSemantics(records.states, records.component, files.states, failures); + if (records.fixture && records.states && records.component) validateFixtureSemantics(records.fixture, records.states, records.component, files.fixture, failures); + if (records.fixture && records.states && records.evidence) validateEvidenceSemantics(records.evidence, records.fixture, records.states, files.evidence, failures); + return failures; +} + +export function validateEvidenceArtifacts(profileRoot, artifactRoot, evidenceOverride, schemas, session) { + const failures = []; + const resolved = resolveProfileRecords(profileRoot, failures); + const evidenceRecord = resolved?.records.evidence[0]; + const file = evidenceRecord?.path ?? resolved?.profileFile ?? path.join(profileRoot, "profile.json"); + const evidence = evidenceOverride ?? evidenceRecord?.value; + const fixture = resolved?.records.fixture[0]?.value; + const states = resolved?.records.states[0]?.value; + if (evidence && fixture && states) { + if (evidenceOverride) { + addSchemaFailures(schemas.evidence, evidence, file, "evidence_schema_invalid", failures); + validateEvidenceSemantics(evidence, fixture, states, file, failures); + } + failures.push(...validateArtifactSet({ artifactRoot, evidence, evidenceFile: file, fixture, profileId: resolved.profile.id, schemas, session, states })); + } + return failures; +} diff --git a/scripts/component-state-semantics.mjs b/scripts/component-state-semantics.mjs new file mode 100644 index 0000000..49461d5 --- /dev/null +++ b/scripts/component-state-semantics.mjs @@ -0,0 +1,120 @@ +function finding(code, file, message) { + return { code, message, path: file }; +} + +function duplicateValues(values, normalize = (value) => value) { + const seen = new Set(); + const duplicates = new Set(); + for (const value of values) { + const key = normalize(value); + if (seen.has(key)) duplicates.add(key); + seen.add(key); + } + return [...duplicates]; +} + +function sameSet(left, right) { + return JSON.stringify([...(left ?? [])].sort()) === JSON.stringify([...(right ?? [])].sort()); +} + +function validateApplicability(component, file, failures) { + for (const id of duplicateValues((component.semantic_modes ?? []).map((mode) => mode.id))) failures.push(finding("semantic_mode_duplicate", file, `semantic mode ${id} is duplicated`)); + for (const mode of component.semantic_modes ?? []) { + for (const name of duplicateValues((mode.aria_states ?? []).map((state) => state.name))) failures.push(finding("aria_state_duplicate", file, `${mode.id} repeats ${name}`)); + for (const state of mode.aria_states ?? []) { + if (["not_applicable", "prohibited"].includes(state.status) && !state.reason) failures.push(finding("applicability_reason_required", file, `${mode.id}.${state.name} requires a reason`)); + if (state.status === "conditional" && (!state.condition || !state.resolution)) failures.push(finding("conditional_unresolved", file, `${mode.id}.${state.name} requires condition and resolution`)); + if (state.status === "conditional" && ["not_applicable", "prohibited"].includes(state.resolution) && !state.reason) failures.push(finding("applicability_reason_required", file, `${mode.id}.${state.name} requires a resolution reason`)); + } + const required = mode.id === "toggle" ? "pressed" : mode.id === "disclosure" ? "expanded" : mode.id === "switch" ? "checked" : undefined; + const state = (mode.aria_states ?? []).find((candidate) => candidate.name === required); + if (required && (!state || !["required", "conditional"].includes(state.status) || (state.status === "conditional" && state.resolution !== "required"))) { + failures.push(finding("role_required_aria_state", file, `${mode.id} requires applicable aria-${required}`)); + } + if (mode.id === "switch" && (mode.aria_states ?? []).find((state) => state.name === "checked")?.values?.includes("mixed")) { + failures.push(finding("switch_mixed_prohibited", file, "switch aria-checked must not include mixed")); + } + } +} + +function validateSurface(scenario, state, visual, aria, dom, ax, code, failures, file) { + const present = new Set(scenario.states ?? []).has(state); + const surfaces = [ + (scenario.expected?.visual ?? []).includes(visual), + aria ? scenario.aria?.[aria] === "true" : present, + dom ? scenario.expected?.dom?.[dom] === "true" : present, + ax ? scenario.expected?.ax?.[ax] === true : present, + ]; + if (surfaces.some((value) => value !== present)) failures.push(finding(code, file, `${scenario.id} ${state} surfaces disagree`)); +} + +export function validateComponentSemantics(component, file, failures) { + validateApplicability(component, file, failures); + if (component.versions?.delivery_channel === "stable" && component.versions.lifecycle !== "stable") failures.push(finding("preview_rolled_stable", file, "preview components cannot be relabeled stable")); +} + +export function validateStateSemantics(states, component, file, failures) { + const scenarios = states.scenarios ?? []; + for (const id of duplicateValues(scenarios.map((scenario) => scenario.id))) failures.push(finding("scenario_duplicate", file, `scenario ${id} is duplicated`)); + for (const key of duplicateValues(scenarios, (scenario) => [...(scenario.states ?? [])].sort().join("\u0000"))) failures.push(finding("state_set_duplicate", file, `state set ${key} is duplicated`)); + const modes = new Map((component.semantic_modes ?? []).map((mode) => [mode.id, mode])); + for (const scenario of scenarios) { + const stateSet = new Set(scenario.states ?? []); + const mode = modes.get(scenario.semantic_mode); + if (!mode) failures.push(finding("scenario_mode_unknown", file, `${scenario.id} references an unknown semantic mode`)); + if (scenario.expected?.dom?.role !== mode?.role || scenario.expected?.ax?.role !== mode?.role) failures.push(finding("role_surface_mismatch", file, `${scenario.id} role surfaces disagree with its semantic mode`)); + validateSurface(scenario, "focus", "focus", undefined, "active", "focused", "focus_surface_mismatch", failures, file); + validateSurface(scenario, "pressed", "pressed", "pressed", "aria-pressed", "pressed", "pressed_surface_mismatch", failures, file); + validateSurface(scenario, "expanded", "expanded", "expanded", "aria-expanded", "expanded", "expanded_surface_mismatch", failures, file); + validateSurface(scenario, "busy", "busy", "busy", "aria-busy", "busy", "busy_surface_mismatch", failures, file); + validateSurface(scenario, "disabled", "disabled", "disabled", "disabled", "disabled", "disabled_surface_mismatch", failures, file); + if (stateSet.has("loading") && (!stateSet.has("busy") || !(scenario.expected?.visual ?? []).includes("loading"))) failures.push(finding("loading_busy_state_required", file, `${scenario.id} loading requires distinct visible busy state`)); + if (stateSet.has("loading") && (scenario.expected?.visual ?? []).includes("disabled") && !stateSet.has("disabled")) failures.push(finding("loading_disabled_conflated", file, `${scenario.id} renders loading as disabled`)); + if (stateSet.has("disabled") && scenario.expected?.activation !== "suppressed") failures.push(finding("disabled_activation", file, `${scenario.id} disabled activation must be suppressed`)); + if (!stateSet.has("disabled") && !stateSet.has("loading") && scenario.expected?.activation === "suppressed") failures.push(finding("normative_activation_conflict", file, `${scenario.id} suppresses activation without a suppressing state`)); + } +} + +export function validateFixtureSemantics(fixture, states, component, file, failures) { + const canonical = new Map((states.scenarios ?? []).map((scenario) => [scenario.id, scenario])); + const modes = new Map((component.semantic_modes ?? []).map((mode) => [mode.id, mode])); + for (const id of duplicateValues((fixture.scenarios ?? []).map((scenario) => scenario.id))) failures.push(finding("fixture_scenario_duplicate", file, `fixture scenario ${id} is duplicated`)); + for (const scenario of fixture.scenarios ?? []) { + const source = canonical.get(scenario.id); + if (!source) failures.push(finding("fixture_scenario_missing", file, `${scenario.id} has no canonical state scenario`)); + else if (source.semantic_mode !== scenario.semantic_mode) failures.push(finding("normative_mode_conflict", file, `${scenario.id} semantic modes disagree`)); + if (!modes.get(scenario.semantic_mode)?.keys?.includes(scenario.activation_key)) failures.push(finding("fixture_activation_key_invalid", file, `${scenario.id} activation key is not declared by its mode`)); + } + for (const id of canonical.keys()) if (!(fixture.scenarios ?? []).some((scenario) => scenario.id === id)) failures.push(finding("fixture_scenario_unexercised", file, `${id} lacks a runtime fixture`)); +} + +export function validateEvidenceSemantics(evidence, fixture, states, file, failures) { + if (Object.hasOwn(evidence, "aggregate_pass") || Object.hasOwn(evidence, "certification")) failures.push(finding("aggregate_pass_forbidden", file, "aggregate pass and certification claims are forbidden")); + const canonical = new Map((states.scenarios ?? []).map((scenario) => [scenario.id, scenario])); + const fixtures = new Map((fixture.scenarios ?? []).map((scenario) => [scenario.id, scenario])); + const passes = evidence.passes ?? []; + for (const id of duplicateValues(passes.map((pass) => pass.id))) failures.push(finding("evidence_pass_duplicate", file, `evidence pass ${id} is duplicated`)); + for (const key of duplicateValues(passes, (pass) => `${pass.scenario_id}\u0000${pass.channel}`)) failures.push(finding("evidence_channel_duplicate", file, `scenario channel ${key} is duplicated`)); + for (const artifact of duplicateValues(passes.map((pass) => pass.artifact?.path).filter(Boolean))) failures.push(finding("evidence_artifact_reused", file, `artifact ${artifact} is reused`)); + for (const digest of duplicateValues(passes.map((pass) => pass.artifact?.sha256).filter(Boolean))) failures.push(finding("evidence_artifact_content_reused", file, `artifact content ${digest} is reused`)); + const runIdentities = new Set(passes.map((pass) => JSON.stringify(pass.run))); + const environments = new Set(passes.map((pass) => JSON.stringify(pass.environment))); + if (runIdentities.size > 1 || environments.size > 1) failures.push(finding("evidence_runtime_identity_mismatch", file, "passes must share one actual run and environment")); + const sessions = new Set(passes.map((pass) => JSON.stringify(pass.session))); + if (sessions.size > 1) failures.push(finding("capture_session_mismatch", file, "passes must share one capture session")); + for (const pass of passes) { + const source = canonical.get(pass.scenario_id); + const scenario = fixtures.get(pass.scenario_id); + if (!source || !scenario) failures.push(finding("evidence_scenario_unknown", file, `${pass.id} references unknown scenario ${pass.scenario_id}`)); + if (pass.channel === "at" || pass.environment?.kind === "assistive_technology") failures.push(finding("at_evidence_unverified", file, `${pass.id} self-asserts an AT run without external attestation`)); + if (pass.session && (pass.run?.id !== pass.session.session_id || pass.run?.revision !== pass.session.revision || pass.run?.attempt !== pass.session.attempt || !sameSet([JSON.stringify(pass.environment)], [JSON.stringify(pass.session.environment)]))) failures.push(finding("capture_session_mismatch", file, `${pass.id} run or environment differs from its capture session`)); + if (scenario && !(scenario.required_channels ?? []).includes(pass.channel)) failures.push(finding("evidence_channel_mismatch", file, `${pass.id} channel is not required for its scenario`)); + if (source && (!sameSet(pass.scope?.state_set, source.states) || pass.scope?.semantic_mode !== source.semantic_mode)) failures.push(finding("evidence_scope_mismatch", file, `${pass.id} scope differs from canonical state JSON`)); + const expectedMedia = pass.channel === "visual" ? "image/png" : ["dom", "ax"].includes(pass.channel) ? "application/json" : undefined; + if (expectedMedia && pass.artifact?.media_type !== expectedMedia) failures.push(finding("evidence_media_type_mismatch", file, `${pass.id} media type does not match its channel`)); + } + for (const scenario of fixture.scenarios ?? []) { + const channels = new Set(passes.filter((pass) => pass.scenario_id === scenario.id).map((pass) => pass.channel)); + for (const channel of scenario.required_channels ?? []) if (!channels.has(channel)) failures.push(finding("required_channel_missing", file, `${scenario.id} lacks ${channel} evidence`)); + } +} diff --git a/scripts/consumer-reference-schema.mjs b/scripts/consumer-reference-schema.mjs index 31bdff7..6816718 100644 --- a/scripts/consumer-reference-schema.mjs +++ b/scripts/consumer-reference-schema.mjs @@ -37,6 +37,20 @@ function missingProperties(value, required) { } function validateProfileObjects(item, schema, add) { + if (Object.hasOwn(item, "profile_kind")) { + const governed = schema.allOf.find((rule) => rule.if?.required?.includes("profile_kind"))?.then; + for (const property of governed?.required ?? []) if (!Object.hasOwn(item, property)) add("profile_field_required", `governed profile requires ${property}`); + for (const property of ["component_records", "evidence_records", "fixture_records", "generated_records", "state_records"]) { + if (!Object.hasOwn(item, property)) continue; + const value = item[property]; + const definition = schema.properties[property]; + const validItems = Array.isArray(value) && value.every((entry) => { + if (definition.items.const !== undefined) return entry === definition.items.const; + return definition.items.enum.includes(entry); + }); + if (!Array.isArray(value) || value.length < definition.minItems || new Set(value).size !== value.length || !validItems) add("profile_reference_array_invalid", `${property} does not satisfy the governed profile schema`); + } + } if (Object.hasOwn(item, "environment_assumptions")) { const environment = item.environment_assumptions; const definition = schema.properties.environment_assumptions; diff --git a/scripts/create-component-state-session.mjs b/scripts/create-component-state-session.mjs new file mode 100644 index 0000000..6d4e3b3 --- /dev/null +++ b/scripts/create-component-state-session.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { chromium } from "playwright"; +import { canonicalIntended, sha256 } from "./capture-session-contract.mjs"; +import { compileSchemas } from "./component-state-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const options = { + json: false, + output: undefined, + profileRoot: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), +}; +const failures = []; + +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (["--output", "--root"].includes(argument)) { + const value = process.argv[index + 1]; + if (!value) failures.push({ code: "argument_value_required", message: `${argument} requires a value`, path: "" }); + else { + if (argument === "--output") options.output = path.resolve(value); + if (argument === "--root") options.profileRoot = path.resolve(value); + index += 1; + } + } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); +} + +if (!options.output) failures.push({ code: "argument_value_required", message: "--output is required", path: "" }); +else if (path.basename(options.output) !== "capture-session.json") failures.push({ code: "capture_session_path_invalid", message: "receipt filename must be capture-session.json", path: options.output }); +else if (fs.existsSync(options.output)) failures.push({ code: "capture_session_replay", message: "capture session receipt already exists and cannot be overwritten", path: options.output }); + +function git(...args) { + return execFileSync("git", args, { cwd: repositoryRoot, encoding: "utf8" }).trim(); +} + +const executable = chromium.executablePath(); +const revisionSegment = executable.split(path.sep).find((segment) => /^chromium(?:_headless_shell)?-\d+$/.test(segment)); +const packageJson = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "package.json"), "utf8")); +const intended = canonicalIntended(options.profileRoot, failures); +const receipt = { + attempt: Number(process.env.GITHUB_RUN_ATTEMPT ?? 1), + branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || git("branch", "--show-current") || "detached", + environment: { + browser: execFileSync(executable, ["--version"], { encoding: "utf8" }).trim(), + browser_revision: revisionSegment ?? path.basename(executable), + container_image: process.env.SENTINEL_CONTAINER_IMAGE ?? `local-host:${os.release()}`, + kind: "browser", + lockfile_sha256: sha256(fs.readFileSync(path.join(repositoryRoot, "package-lock.json"))), + node: process.version, + platform: process.platform === "linux" && process.arch === "x64" ? "linux/amd64" : `${process.platform}/${process.arch}`, + playwright: packageJson.devDependencies["@playwright/test"], + viewport: "1024x768", + }, + intended, + nonce: crypto.randomBytes(32).toString("hex"), + record_kind: "component_state_capture_session", + repository: process.env.GITHUB_REPOSITORY ?? "changeroa/StyleGallery", + revision: process.env.GITHUB_SHA ?? git("rev-parse", "HEAD"), + schema_version: "1.0", + session_id: crypto.randomUUID(), + started_at: new Date().toISOString(), +}; + +const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/schema")); +if (!schemas.capture(receipt)) { + for (const error of schemas.capture.errors ?? []) failures.push({ code: "capture_session_schema_invalid", message: `${error.instancePath || "/"} ${error.message}`, path: options.output ?? "" }); +} +if (failures.length === 0) { + fs.mkdirSync(path.dirname(options.output), { recursive: true }); + fs.writeFileSync(options.output, `${JSON.stringify(receipt, null, 2)}\n`); +} +const result = { failures, intendedArtifacts: intended.reduce((sum, profile) => sum + profile.scenarios.reduce((count, scenario) => count + scenario.channels.length, 0), 0), ok: failures.length === 0, receipt: options.output }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`created component-state capture session ${receipt.session_id}\n`); +else process.stderr.write(`${failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/evidence-artifact-contract.mjs b/scripts/evidence-artifact-contract.mjs new file mode 100644 index 0000000..85c0e8d --- /dev/null +++ b/scripts/evidence-artifact-contract.mjs @@ -0,0 +1,166 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { sameJson, withinSession } from "./capture-session-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); + +function finding(code, file, message) { + return { code, message, path: file }; +} + +function isInside(base, target) { + const relative = path.relative(base, target); + return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)); +} + +function isSafeArtifactPath(reference) { + return typeof reference === "string" + && reference.length > 0 + && !path.posix.isAbsolute(reference) + && !path.win32.isAbsolute(reference) + && !reference.includes("\\") + && !reference.includes("?") + && !reference.includes("#") + && !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference) + && path.posix.normalize(reference) === reference + && !reference.split("/").some((segment) => segment === "." || segment === ".."); +} + +function pngDimensions(bytes) { + if (bytes.length < 24 || !bytes.subarray(0, 8).equals(pngSignature) || bytes.toString("ascii", 12, 16) !== "IHDR") return undefined; + const width = bytes.readUInt32BE(16); + const height = bytes.readUInt32BE(20); + return width > 0 && height > 0 ? { height, width } : undefined; +} + +export function artifactMetadata(bytes, mediaType) { + const metadata = { + byte_length: bytes.length, + media_type: mediaType, + sha256: `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`, + }; + if (mediaType === "image/png") Object.assign(metadata, pngDimensions(bytes)); + return metadata; +} + +function validateIntegrity(pass, bytes, evidenceFile, failures) { + const actual = artifactMetadata(bytes, pass.artifact.media_type); + if (actual.sha256 !== pass.artifact.sha256 || actual.byte_length !== pass.artifact.byte_length) failures.push(finding("evidence_artifact_integrity", evidenceFile, `${pass.id} hash or byte length does not match`)); + if (pass.channel === "visual") { + if (!actual.width || !actual.height) failures.push(finding("evidence_png_invalid", evidenceFile, `${pass.id} is not a valid PNG with IHDR dimensions`)); + else if (actual.width !== pass.artifact.width || actual.height !== pass.artifact.height) failures.push(finding("evidence_png_dimensions_mismatch", evidenceFile, `${pass.id} PNG dimensions do not match`)); + } + return actual; +} + +function observedDomValue(document, key) { + if (key === "active") return String(document.active); + if (key === "role") return document.role; + return document.attributes?.[key]; +} + +function validateDom(document, pass, scenario, fixture, profileId, evidenceFile, failures) { + if (document.schema_version !== "1.0" || document.channel !== "dom" || document.profile_id !== profileId || document.scenario_id !== pass.scenario_id || document.semantic_mode !== scenario.semantic_mode) { + failures.push(finding("evidence_dom_identity_mismatch", evidenceFile, `${pass.id} DOM identity does not match its pass`)); + } + if (document.activation_key !== fixture.activation_key) failures.push(finding("evidence_runtime_identity_mismatch", evidenceFile, `${pass.id} activation key differs from the canonical fixture`)); + for (const [key, expected] of Object.entries(scenario.expected.dom)) if (observedDomValue(document, key) !== expected) failures.push(finding("evidence_dom_content_mismatch", evidenceFile, `${pass.id} DOM ${key} differs from canonical state`)); + const visual = JSON.stringify([...(document.visual_states ?? [])].sort()); + if (visual !== JSON.stringify([...scenario.expected.visual].sort())) failures.push(finding("evidence_dom_content_mismatch", evidenceFile, `${pass.id} visual states differ from canonical state`)); + const expectedActivations = scenario.expected.activation === "allowed" ? 1 : 0; + if (document.activation_count !== expectedActivations) failures.push(finding("evidence_dom_content_mismatch", evidenceFile, `${pass.id} activation result differs from canonical state`)); +} + +function validateAx(document, pass, scenario, fixture, profileId, evidenceFile, failures) { + if (document.schema_version !== "1.0" || document.channel !== "ax" || document.profile_id !== profileId || document.scenario_id !== pass.scenario_id || document.semantic_mode !== scenario.semantic_mode || document.name !== fixture.label) { + failures.push(finding("evidence_ax_identity_mismatch", evidenceFile, `${pass.id} AX identity does not match its pass`)); + } + if (document.role !== scenario.expected.ax.role) failures.push(finding("evidence_ax_content_mismatch", evidenceFile, `${pass.id} AX role differs from canonical state`)); + for (const [key, expected] of Object.entries(scenario.expected.ax)) { + const actual = key === "role" ? document.role : document.properties?.[key]; + if (actual !== expected) failures.push(finding("evidence_ax_content_mismatch", evidenceFile, `${pass.id} AX ${key} differs from canonical state`)); + } +} + +function parseArtifactJson(bytes, pass, scenario, fixture, profileId, evidenceFile, failures, schemas, session) { + let document; + try { + document = parseStrictJson(bytes.toString("utf8")); + } catch (error) { + failures.push(finding("evidence_json_invalid", evidenceFile, `${pass.id} is not structured JSON: ${error instanceof Error ? error.message : String(error)}`)); + return undefined; + } + const validate = pass.channel === "dom" ? schemas.dom : pass.channel === "ax" ? schemas.ax : undefined; + if (validate && !validate(document)) { + const code = pass.channel === "dom" ? "evidence_dom_schema_invalid" : "evidence_ax_schema_invalid"; + for (const error of validate.errors ?? []) failures.push(finding(code, evidenceFile, `${pass.id} ${error.instancePath || "/"} ${error.message}`)); + return undefined; + } + if (!sameJson(document.capture_session, pass.session)) failures.push(finding("capture_session_mismatch", evidenceFile, `${pass.id} artifact session differs from its pass`)); + if (document.captured_at !== pass.recorded_at) failures.push(finding("evidence_recorded_at_mismatch", evidenceFile, `${pass.id} recorded_at is not derived from artifact content`)); + if (session && !withinSession(document.captured_at, session.started_at, session.completed_at)) failures.push(finding("capture_session_time_outside", evidenceFile, `${pass.id} was not captured within its session`)); + if (pass.channel === "dom") validateDom(document, pass, scenario, fixture, profileId, evidenceFile, failures); + else if (pass.channel === "ax") validateAx(document, pass, scenario, fixture, profileId, evidenceFile, failures); + else failures.push(finding("evidence_channel_content_unknown", evidenceFile, `${pass.id} has no structured content contract`)); + return document; +} + +function readArtifact(pass, artifactRoot, evidenceFile, failures) { + const reference = pass.artifact?.path; + if (!isSafeArtifactPath(reference)) { + failures.push(finding("evidence_artifact_escape", evidenceFile, `${pass.id} artifact path is unsafe`)); + return undefined; + } + const root = path.resolve(artifactRoot); + const target = path.resolve(root, reference); + if (!isInside(root, target) || !fs.existsSync(target)) { + failures.push(finding("evidence_artifact_missing", evidenceFile, `${pass.id} artifact is missing`)); + return undefined; + } + const metadata = fs.lstatSync(target); + if (metadata.isSymbolicLink()) { + failures.push(finding("evidence_artifact_symlink", evidenceFile, `${pass.id} artifact must not be a symlink`)); + return undefined; + } + if (!metadata.isFile()) { + failures.push(finding("evidence_artifact_type_invalid", evidenceFile, `${pass.id} artifact must be a regular file`)); + return undefined; + } + const realRoot = fs.realpathSync(root); + const realTarget = fs.realpathSync(target); + if (!isInside(realRoot, realTarget) || realTarget !== path.join(realRoot, reference)) { + failures.push(finding("evidence_artifact_redirect", evidenceFile, `${pass.id} artifact resolves through a redirect`)); + return undefined; + } + return fs.readFileSync(realTarget); +} + +export function validateEvidenceArtifacts({ artifactRoot, evidence, evidenceFile, fixture, profileId, schemas, session, states }) { + const failures = []; + const scenarios = new Map(states.scenarios.map((scenario) => [scenario.id, scenario])); + const fixtures = new Map(fixture.scenarios.map((scenario) => [scenario.id, scenario])); + const capturedTimes = new Map(); + for (const pass of evidence.passes ?? []) { + const scenario = scenarios.get(pass.scenario_id); + const runtimeFixture = fixtures.get(pass.scenario_id); + if (!scenario || !runtimeFixture || !pass.artifact?.media_type) continue; + const bytes = readArtifact(pass, artifactRoot, evidenceFile, failures); + if (!bytes) continue; + validateIntegrity(pass, bytes, evidenceFile, failures); + if (pass.artifact.media_type === "application/json") { + const document = parseArtifactJson(bytes, pass, scenario, runtimeFixture, profileId, evidenceFile, failures, schemas, session); + if (document?.captured_at) { + if (!capturedTimes.has(pass.scenario_id)) capturedTimes.set(pass.scenario_id, new Set()); + capturedTimes.get(pass.scenario_id).add(document.captured_at); + } + } + } + for (const [scenarioId, times] of capturedTimes) { + if (times.size !== 1) failures.push(finding("capture_session_time_mismatch", evidenceFile, `${scenarioId} DOM and AX captured_at values differ`)); + const capturedAt = [...times][0]; + for (const pass of evidence.passes.filter((entry) => entry.scenario_id === scenarioId)) if (pass.recorded_at !== capturedAt) failures.push(finding("evidence_recorded_at_mismatch", evidenceFile, `${pass.id} recorded_at is not derived from its scenario artifacts`)); + } + return failures; +} diff --git a/scripts/finalize-component-state-evidence.mjs b/scripts/finalize-component-state-evidence.mjs new file mode 100644 index 0000000..b57ed03 --- /dev/null +++ b/scripts/finalize-component-state-evidence.mjs @@ -0,0 +1,155 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { artifactMetadata } from "./evidence-artifact-contract.mjs"; +import { canonicalIntended, readCaptureSession, sameJson } from "./capture-session-contract.mjs"; +import { compileSchemas, validateEvidenceArtifacts } from "./component-state-contract.mjs"; +import { resolveProfileRecords } from "./profile-record-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const options = { + artifactRoot: undefined, + json: false, + output: undefined, + profileRoot: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), + sessionReceipt: undefined, + writeCanonical: false, +}; +const failures = []; + +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (argument === "--write-canonical") options.writeCanonical = true; + else if (["--artifact-root", "--output", "--root", "--session-receipt"].includes(argument)) { + const value = process.argv[index + 1]; + if (!value) failures.push({ code: "argument_value_required", message: `${argument} requires a value`, path: "" }); + else { + if (argument === "--artifact-root") options.artifactRoot = path.resolve(value); + if (argument === "--output") options.output = path.resolve(value); + if (argument === "--root") options.profileRoot = path.resolve(value); + if (argument === "--session-receipt") options.sessionReceipt = path.resolve(value); + index += 1; + } + } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); +} + +if (!options.artifactRoot) failures.push({ code: "argument_value_required", message: "--artifact-root is required", path: "" }); +const output = options.output ?? (options.artifactRoot ? path.join(options.artifactRoot, "runtime-manifest.json") : undefined); +if (output && fs.existsSync(output)) failures.push({ code: "capture_session_replay", message: "completed session manifest already exists and cannot be overwritten", path: output }); +const receiptFile = options.sessionReceipt ?? (options.artifactRoot ? path.join(options.artifactRoot, "capture-session.json") : ""); +if (options.artifactRoot && receiptFile !== path.join(options.artifactRoot, "capture-session.json")) failures.push({ code: "capture_session_path_invalid", message: "session receipt must be the artifact-root capture-session.json", path: receiptFile }); +const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/schema")); +const capture = receiptFile ? readCaptureSession(receiptFile, schemas.capture, failures) : undefined; +const intended = canonicalIntended(options.profileRoot, failures); +if (capture && !sameJson(capture.receipt.intended, intended)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from canonical profile scenarios", path: receiptFile }); + +const completedAt = new Date().toISOString(); +const completedSession = capture ? { + attempt: capture.receipt.attempt, + branch: capture.receipt.branch, + completed_at: completedAt, + environment: capture.receipt.environment, + intended: capture.receipt.intended, + nonce: capture.receipt.nonce, + receipt: capture.artifact, + receipt_sha256: capture.digest, + repository: capture.receipt.repository, + revision: capture.receipt.revision, + session_id: capture.receipt.session_id, + started_at: capture.receipt.started_at, +} : undefined; +const run = capture ? { + attempt: capture.receipt.attempt, + id: capture.receipt.session_id, + repository: capture.receipt.repository, + revision: capture.receipt.revision, + source: process.env.GITHUB_ACTIONS === "true" ? "github_actions" : "local", +} : undefined; +const channelFiles = Object.freeze({ ax: "ax.json", dom: "dom.json", visual: "png" }); +const records = []; +const expectedFiles = new Set(); +const canonicalWrites = []; + +for (const profileIntent of intended) { + const profileRoot = path.join(options.profileRoot, profileIntent.profile_name); + const profileFailures = []; + const resolved = resolveProfileRecords(profileRoot, profileFailures); + failures.push(...profileFailures); + const fixture = resolved?.records.fixture[0]?.value; + const states = resolved?.records.states[0]?.value; + if (!resolved || !fixture || !states || !capture || !run || !completedSession) continue; + const stateById = new Map(states.scenarios.map((scenario) => [scenario.id, scenario])); + const passes = []; + for (const scenario of fixture.scenarios) { + const canonical = stateById.get(scenario.id); + const documents = {}; + for (const channel of ["dom", "ax"]) { + const file = path.join(options.artifactRoot, "runtime", `${profileIntent.profile_name}-${scenario.id}.${channel}.json`); + try { documents[channel] = parseStrictJson(fs.readFileSync(file, "utf8")); } + catch (error) { failures.push({ code: "evidence_json_invalid", message: error instanceof Error ? error.message : String(error), path: file }); } + } + const capturedTimes = new Set(Object.values(documents).map((document) => document?.captured_at).filter(Boolean)); + if (capturedTimes.size !== 1) failures.push({ code: "capture_session_time_mismatch", message: `${scenario.id} DOM and AX must share one captured_at`, path: profileRoot }); + const recordedAt = [...capturedTimes][0] ?? ""; + for (const channel of scenario.required_channels) { + const suffix = channelFiles[channel]; + const artifactPath = `runtime/${profileIntent.profile_name}-${scenario.id}.${suffix}`; + expectedFiles.add(path.basename(artifactPath)); + const absolute = path.join(options.artifactRoot, artifactPath); + if (!fs.existsSync(absolute) || !fs.lstatSync(absolute).isFile() || fs.lstatSync(absolute).isSymbolicLink()) { + failures.push({ code: "runtime_artifact_missing", message: `${artifactPath} is missing or not a regular file`, path: absolute }); + continue; + } + const mediaType = channel === "visual" ? "image/png" : "application/json"; + passes.push({ + artifact: { path: artifactPath, ...artifactMetadata(fs.readFileSync(absolute), mediaType) }, + channel, + environment: capture.receipt.environment, + id: `${scenario.id}-${channel}`, + recorded_at: recordedAt, + result: { observed: `Recorded ${channel} evidence for ${scenario.id}; this is not certification.`, status: "passed" }, + run, + scenario_id: scenario.id, + session: capture.link, + scope: { component: "button", semantic_mode: scenario.semantic_mode, state_set: canonical.states }, + }); + } + } + const evidence = { + claim_boundary: "Scenario artifacts are evidence, not visual regression baselines or accessibility certification.", + component_id: "button", + passes, + profile_id: resolved.profile.id, + schema_version: "1.0", + }; + failures.push(...validateEvidenceArtifacts(profileRoot, options.artifactRoot, evidence, schemas, completedSession)); + records.push(evidence); + if (resolved.records.evidence[0]) canonicalWrites.push([resolved.records.evidence[0].path, evidence]); +} + +const runtimeRoot = options.artifactRoot ? path.join(options.artifactRoot, "runtime") : ""; +if (runtimeRoot && fs.existsSync(runtimeRoot)) { + for (const entry of fs.readdirSync(runtimeRoot, { withFileTypes: true })) if (!entry.isFile() || !expectedFiles.has(entry.name)) failures.push({ code: "runtime_artifact_unmanifested", message: `${entry.name} is not part of the closed artifact set`, path: path.join(runtimeRoot, entry.name) }); +} + +const manifest = { + claim_boundary: "Scenario artifacts are evidence, not visual regression baselines or accessibility certification.", + environment: capture?.receipt.environment, + recorded_at: completedAt, + record_kind: "component_state_runtime_manifest", + records, + run, + schema_version: "1.0", + session: completedSession, +}; +if (!schemas.runtime(manifest)) for (const error of schemas.runtime.errors ?? []) failures.push({ code: "runtime_manifest_schema_invalid", message: `${error.instancePath || "/"} ${error.message}`, path: options.output ?? "" }); +if (failures.length === 0 && options.writeCanonical) for (const [file, evidence] of canonicalWrites) fs.writeFileSync(file, `${JSON.stringify(evidence, null, 2)}\n`); +if (failures.length === 0 && output) fs.writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`); +const result = { artifactCount: expectedFiles.size, failures, manifest: output, ok: failures.length === 0, profileCount: records.length, sessionId: capture?.receipt.session_id }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`finalized ${result.artifactCount} component state artifacts\n`); +else process.stderr.write(`${failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/generate-consumer-reference-evidence.mjs b/scripts/generate-consumer-reference-evidence.mjs new file mode 100644 index 0000000..49766f4 --- /dev/null +++ b/scripts/generate-consumer-reference-evidence.mjs @@ -0,0 +1,153 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { resolveProfileRecords } from "./profile-record-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const options = { check: false, json: false, root: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local") }; +const failures = []; +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (argument === "--check") options.check = true; + else if (argument === "--root") { + const value = process.argv[index + 1]; + if (!value) failures.push({ code: "argument_value_required", message: "--root requires a directory", path: "" }); + else { options.root = path.resolve(value); index += 1; } + } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); +} + +function compare(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function isSorted(values) { + return values.every((value, index) => index === 0 || compare(values[index - 1], value) <= 0); +} + +function duplicateValues(values) { + const seen = new Set(); + return [...new Set(values.filter((value) => seen.has(value) || !seen.add(value)))]; +} + +function escapeMarkdown(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("|", "|") + .replaceAll("`", "`") + .replace(/\r\n?|\n/g, "
"); +} + +function codeCell(value) { + return `${escapeMarkdown(value)}`; +} + +function markdownTable(headers, rows) { + return [`| ${headers.map(escapeMarkdown).join(" | ")} |`, `| ${headers.map(() => "---").join(" | ")} |`, ...rows.map((row) => `| ${row.join(" | ")} |`)].join("\n"); +} + +function frontmatter(title, description) { + return `---\ntype: Generated Evidence\ntitle: ${JSON.stringify(String(title))}\ndescription: ${JSON.stringify(String(description))}\n---\n\n`; +} + +function navigation(parentLabel, parentPath, nextLabel, nextPath) { + return `## IA Navigation\n\nParent: [${parentLabel}](${parentPath}).\nNext: [${nextLabel}](${nextPath}).`; +} + +function renderStateMatrix(profile, states) { + const rows = [...states.scenarios].sort((a, b) => compare(a.id, b.id)).map((scenario) => [ + codeCell(scenario.id), + escapeMarkdown(scenario.semantic_mode), + [...scenario.states].sort(compare).map(codeCell).join(", "), + Object.entries(scenario.aria).sort(([left], [right]) => compare(left, right)).map(([name, value]) => codeCell(`aria-${name}=${value}`)).join("
") || "none", + scenario.expected.visual.map(codeCell).join(", "), + escapeMarkdown(scenario.expected.activation), + ]); + return `${frontmatter(`${profile} button state matrix`, "Compound state sets and channel expectations derived from canonical JSON.")}\n# ${escapeMarkdown(profile)} button state matrix\n\n${markdownTable(["Scenario", "Mode", "State set", "ARIA", "Visual", "Activation"], rows)}\n\n${navigation("Governed Local Profiles", "../../index.md", "Keyboard Matrix", "keyboard-matrix.md")}\n`; +} + +function renderKeyboardMatrix(profile, component, fixture) { + const fixtureByMode = new Map(fixture.scenarios.map((scenario) => [scenario.semantic_mode, scenario.id])); + const rows = [...component.semantic_modes].sort((a, b) => compare(a.id, b.id)).map((mode) => [ + codeCell(mode.id), escapeMarkdown(mode.element), escapeMarkdown(mode.role), [...mode.keys].sort(compare).map(codeCell).join(", "), codeCell(mode.activation), fixtureByMode.has(mode.id) ? codeCell(fixtureByMode.get(mode.id)) : "not exercised", + ]); + return `${frontmatter(`${profile} button keyboard matrix`, "Keyboard behavior and live fixture coverage derived from canonical JSON.")}\n# ${escapeMarkdown(profile)} button keyboard matrix\n\n${markdownTable(["Mode", "Element", "Role", "Keys", "Behavior", "Runtime scenario"], rows)}\n\n${navigation("State Matrix", "state-matrix.md", "Evidence Coverage", "evidence-coverage.md")}\n`; +} + +function renderEvidenceCoverage(profile, fixture, evidence) { + const rows = [...fixture.scenarios].sort((a, b) => compare(a.id, b.id)).map((scenario) => { + const passes = evidence.passes.filter((pass) => pass.scenario_id === scenario.id).sort((a, b) => compare(a.channel, b.channel)); + return [ + codeCell(scenario.id), + scenario.required_channels.map(codeCell).join(", "), + passes.map((pass) => codeCell(pass.channel)).join(", "), + String(passes.length), + passes.map((pass) => codeCell(pass.artifact.path)).join("
"), + ]; + }); + return `${frontmatter(`${profile} button evidence coverage`, "Claim-scoped evidence channels and artifacts derived from canonical JSON.")}\n# ${escapeMarkdown(profile)} button evidence coverage\n\nClaim boundary: ${escapeMarkdown(evidence.claim_boundary)}\n\n${markdownTable(["Scenario", "Required channels", "Recorded channels", "Pass records", "Artifacts"], rows)}\n\n${navigation("Keyboard Matrix", "keyboard-matrix.md", "Reference Profiles", "../../../index.md")}\n`; +} + +function generatedRowIds(content) { + return content.split("\n").map((line) => /^\| ([^<]+)<\/code> \|/.exec(line)?.[1]).filter(Boolean); +} + +function classifyDrift(file, expectedIds, expected, actual) { + const ids = generatedRowIds(actual); + for (const id of duplicateValues(ids)) failures.push({ code: "generated_duplicate_row", message: `${id} is duplicated`, path: file }); + if (!isSorted(ids)) failures.push({ code: "generated_unsorted", message: "generated rows must be sorted", path: file }); + const missing = expectedIds.filter((id) => !ids.includes(id)); + for (const id of missing) failures.push({ code: "generated_missing_row", message: `${id} is missing`, path: file }); + if (ids.length !== expectedIds.length) failures.push({ code: "generated_source_count_mismatch", message: `expected ${expectedIds.length} rows, found ${ids.length}`, path: file }); + if (actual !== expected) failures.push({ code: "generated_stale", message: "generated table differs from canonical JSON", path: file }); +} + +function writeOrCheck(file, expectedIds, content) { + if (options.check) { + if (!fs.existsSync(file)) failures.push({ code: "generated_missing", message: "generated table is missing", path: file }); + else classifyDrift(file, expectedIds, content, fs.readFileSync(file, "utf8")); + return; + } + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); +} + +const profiles = fs.existsSync(options.root) + ? fs.readdirSync(options.root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && fs.existsSync(path.join(options.root, entry.name, "profile.json"))).map((entry) => entry.name).sort(compare) + : []; +let generated = 0; +for (const profile of profiles) { + const profileRoot = path.join(options.root, profile); + const resolved = resolveProfileRecords(profileRoot, failures); + if (!resolved) continue; + const component = resolved.records.component[0]?.value; + const fixture = resolved.records.fixture[0]?.value; + const states = resolved.records.states[0]?.value; + const evidence = resolved.records.evidence[0]?.value; + if (!component || !fixture || !states || !evidence) continue; + const scenarioIds = states.scenarios.map((scenario) => scenario.id); + for (const id of duplicateValues(scenarioIds)) failures.push({ code: "scenario_duplicate", message: `${id} is duplicated`, path: profileRoot }); + if (!isSorted(scenarioIds)) failures.push({ code: "source_unsorted", message: "canonical scenario IDs must be sorted", path: profileRoot }); + const fixtureIds = fixture.scenarios.map((scenario) => scenario.id); + for (const id of fixtureIds.filter((id) => !scenarioIds.includes(id))) failures.push({ code: "fixture_scenario_missing", message: `${id} has no canonical scenario`, path: profileRoot }); + const outputs = new Map([ + ["state-matrix.md", [[...scenarioIds].sort(compare), renderStateMatrix(profile, states)]], + ["keyboard-matrix.md", [component.semantic_modes.map((mode) => mode.id).sort(compare), renderKeyboardMatrix(profile, component, fixture)]], + ["evidence-coverage.md", [[...fixtureIds].sort(compare), renderEvidenceCoverage(profile, fixture, evidence)]], + ]); + for (const record of resolved.records.generated) { + const output = outputs.get(path.basename(record.path)); + if (!output) { failures.push({ code: "generated_reference_unknown", message: `${record.reference} has no renderer`, path: resolved.profileFile }); continue; } + writeOrCheck(record.path, output[0], output[1]); + generated += 1; + } +} +const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()]; +const result = { checkedProfiles: profiles.length, failures: uniqueFailures, generated, mode: options.check ? "check" : "write", ok: uniqueFailures.length === 0, warnings: [] }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`${options.check ? "checked" : "generated"} ${generated} consumer reference evidence tables\n`); +else process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/profile-record-contract.mjs b/scripts/profile-record-contract.mjs new file mode 100644 index 0000000..6dc5863 --- /dev/null +++ b/scripts/profile-record-contract.mjs @@ -0,0 +1,140 @@ +import fs from "node:fs"; +import path from "node:path"; +import { parseStrictJson } from "./strict-json.mjs"; + +const referenceKinds = Object.freeze({ + component: { field: "component_records", pattern: /^components\/[a-z0-9-]+\.component\.json$/, required: 1 }, + evidence: { field: "evidence_records", pattern: /^evidence\/[a-z0-9-]+\.evidence\.json$/, required: 1 }, + fixture: { field: "fixture_records", pattern: /^fixtures\/[a-z0-9-]+\.fixture\.json$/, required: 1 }, + generated: { field: "generated_records", pattern: /^generated\/[a-z0-9-]+\.md$/, required: 3 }, + states: { field: "state_records", pattern: /^states\/[a-z0-9-]+\.states\.json$/, required: 1 }, +}); + +function finding(code, file, message) { + return { code, message, path: file }; +} + +function isInside(base, target) { + const relative = path.relative(base, target); + return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)); +} + +function isNormalizedRelative(reference) { + return typeof reference === "string" + && reference.length > 0 + && !path.posix.isAbsolute(reference) + && !path.win32.isAbsolute(reference) + && !reference.includes("\\") + && !reference.includes("?") + && !reference.includes("#") + && !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference) + && path.posix.normalize(reference) === reference + && !reference.split("/").some((segment) => segment === "." || segment === ".."); +} + +function resolveReference(profileRoot, reference, kind, profileFile, failures) { + const definition = referenceKinds[kind]; + if (!isNormalizedRelative(reference)) { + failures.push(finding("profile_reference_outside", profileFile, `${definition.field} contains an unsafe reference`)); + return undefined; + } + if (!definition.pattern.test(reference)) { + failures.push(finding("profile_reference_unknown", profileFile, `${definition.field} contains unknown reference ${reference}`)); + return undefined; + } + const target = path.resolve(profileRoot, reference); + if (!isInside(profileRoot, target)) { + failures.push(finding("profile_reference_outside", profileFile, `${reference} escapes its profile`)); + return undefined; + } + if (!fs.existsSync(target)) { + failures.push(finding("profile_reference_missing", profileFile, `${reference} does not exist`)); + return undefined; + } + const metadata = fs.lstatSync(target); + if (metadata.isSymbolicLink()) { + failures.push(finding("profile_reference_symlink", profileFile, `${reference} must not be a symlink`)); + return undefined; + } + if (!metadata.isFile()) { + failures.push(finding("profile_reference_type_invalid", profileFile, `${reference} must be a regular file`)); + return undefined; + } + const realRoot = fs.realpathSync(profileRoot); + const realTarget = fs.realpathSync(target); + if (!isInside(realRoot, realTarget)) { + failures.push(finding("profile_reference_outside", profileFile, `${reference} resolves outside its profile`)); + return undefined; + } + if (realTarget !== path.join(realRoot, reference)) { + failures.push(finding("profile_reference_redirect", profileFile, `${reference} resolves through a redirect`)); + return undefined; + } + if (kind === "generated") return { path: realTarget, reference }; + try { + return { path: realTarget, reference, value: parseStrictJson(fs.readFileSync(realTarget, "utf8")) }; + } catch (error) { + failures.push(finding("profile_reference_json_invalid", reference, error instanceof Error ? error.message : String(error))); + return undefined; + } +} + +function resolveFixtureLinks(resolved, profileFile, failures) { + const fixture = resolved.fixture[0]; + const component = resolved.component[0]; + const states = resolved.states[0]; + if (!fixture || !component || !states) return; + for (const [field, expected] of [["component_record", component.path], ["state_record", states.path]]) { + const reference = fixture.value[field]; + const safe = typeof reference === "string" + && reference.length > 0 + && !path.posix.isAbsolute(reference) + && !path.win32.isAbsolute(reference) + && !reference.includes("\\") + && !reference.includes("?") + && !reference.includes("#") + && !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference) + && path.posix.normalize(reference) === reference; + if (!safe) { + failures.push(finding("fixture_reference_invalid", fixture.reference, `${field} must be normalized and profile-local`)); + continue; + } + const actual = path.resolve(path.dirname(fixture.path), reference); + if (actual !== expected) failures.push(finding("fixture_reference_mismatch", fixture.reference, `${field} does not resolve to the profile-declared record`)); + } + if (fixture.value.profile_id !== component.value.profile_id || fixture.value.profile_id !== states.value.profile_id) { + failures.push(finding("profile_identity_mismatch", profileFile, "fixture links cross profile identities")); + } +} + +export function resolveProfileRecords(profileRoot, failures) { + const profileFile = path.join(profileRoot, "profile.json"); + if (!fs.existsSync(profileFile) || fs.lstatSync(profileFile).isSymbolicLink() || !fs.lstatSync(profileFile).isFile()) { + failures.push(finding("profile_file_invalid", profileFile, "profile.json must be a regular non-symlink file")); + return undefined; + } + let profile; + try { + profile = parseStrictJson(fs.readFileSync(profileFile, "utf8")); + } catch (error) { + failures.push(finding("profile_json_invalid", profileFile, error instanceof Error ? error.message : String(error))); + return undefined; + } + const resolved = {}; + for (const [kind, definition] of Object.entries(referenceKinds)) { + const references = profile[definition.field]; + if (!Array.isArray(references)) { + failures.push(finding("profile_reference_required", profileFile, `${definition.field} must contain exactly ${definition.required} references`)); + resolved[kind] = []; + continue; + } + if (new Set(references).size !== references.length) failures.push(finding("profile_reference_duplicate", profileFile, `${definition.field} must not repeat references`)); + if (references.length !== definition.required) failures.push(finding("profile_reference_required", profileFile, `${definition.field} must contain exactly ${definition.required} references`)); + resolved[kind] = references.map((reference) => resolveReference(profileRoot, reference, kind, profileFile, failures)).filter(Boolean); + } + for (const record of [resolved.component[0], resolved.evidence[0], resolved.fixture[0], resolved.states[0]].filter(Boolean)) { + if (record.value.profile_id !== profile.id) failures.push(finding("profile_identity_mismatch", record.reference, `record profile_id must equal ${profile.id}`)); + } + resolveFixtureLinks(resolved, profileFile, failures); + return { profile, profileFile, records: resolved }; +} diff --git a/scripts/test-component-state-sentinel.mjs b/scripts/test-component-state-sentinel.mjs new file mode 100644 index 0000000..c0ec557 --- /dev/null +++ b/scripts/test-component-state-sentinel.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const cases = [ + ["key_runtime_mismatch", "runtime-key-mismatch", "state_activation_key_parity", "action-focused"], + ["pressed_false", "runtime-pressed-false", "pressed_surface_mismatch", "toggle-focused-pressed"], + ["visual_ready", "runtime-visual-ready", "visual_surface_parity", "toggle-focused-pressed"], + ["unknown_scenario", "runtime-unknown-scenario", "state_runtime_unknown_scenario", "action-focused"], + ["expanded_ax_mismatch", "runtime-expanded-ax-mismatch", "expanded_surface_mismatch", "disclosure-expanded-loading"], + ["disabled_activation", "runtime-disabled-activation", "state_activation_key_parity", "action-disabled-busy"], +]; + +const results = cases.map(([name, mutation, expected, scenario]) => { + const child = spawnSync( + path.join(repositoryRoot, "node_modules", ".bin", "playwright"), + ["test", "tests/component-state-evidence.spec.mjs", "--project=chromium", "--reporter=line", "--grep", scenario], + { cwd: repositoryRoot, encoding: "utf8", env: { ...process.env, STATE_MUTATION: mutation } }, + ); + const output = `${child.stdout}\n${child.stderr}`; + return { + actual: { named: output.includes(expected), status: child.status }, + expected, + name, + ok: child.status !== 0 && output.includes(expected), + }; +}); + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +if (!report.ok) process.exitCode = 1; diff --git a/scripts/test-generate-consumer-reference-evidence.mjs b/scripts/test-generate-consumer-reference-evidence.mjs new file mode 100644 index 0000000..c1673a6 --- /dev/null +++ b/scripts/test-generate-consumer-reference-evidence.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const sourceRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); +const generator = path.join(repositoryRoot, "scripts/generate-consumer-reference-evidence.mjs"); + +function run(root, check = false, environment = {}) { + const args = [generator, "--root", root, "--json"]; + if (check) args.push("--check"); + const child = spawnSync(process.execPath, args, { cwd: repositoryRoot, encoding: "utf8", env: { ...process.env, ...environment } }); + let report = { failures: [], ok: false, parse_error: true }; + try { report = JSON.parse(child.stdout); } catch { report = { failures: [], ok: false, parse_error: true }; } + return { report, status: child.status }; +} + +function editJson(file, mutate) { + const value = JSON.parse(fs.readFileSync(file, "utf8")); + mutate(value); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr5-generator-")); +const results = []; + +function freshRoot(name) { + const root = path.join(tempRoot, name); + fs.cpSync(sourceRoot, root, { recursive: true }); + return root; +} + +function expectFailure(name, expected, root) { + const child = run(root, true); + results.push({ expected, name, ok: child.status > 0 && child.report.failures.some((failure) => failure.code === expected) }); +} + +function mutateGenerated(name, expected, mutate) { + const root = freshRoot(name); + run(root); + const file = path.join(root, "editorial/generated/state-matrix.md"); + const lines = fs.readFileSync(file, "utf8").split("\n"); + mutate(lines); + fs.writeFileSync(file, lines.join("\n")); + expectFailure(name, expected, root); +} + +try { + const canonicalRoot = freshRoot("canonical"); + const generated = run(canonicalRoot); + results.push({ expected: "canonical_generation", name: "canonical", ok: generated.status === 0 && generated.report.ok && generated.report.scaffold !== true }); + const checked = run(canonicalRoot, true); + results.push({ expected: "deterministic_zero_drift", name: "check", ok: checked.status === 0 && checked.report.ok && checked.report.scaffold !== true }); + + mutateGenerated("stale", "generated_stale", (lines) => { lines.push("stale"); }); + mutateGenerated("generated_unsorted", "generated_unsorted", (lines) => { + const first = lines.findIndex((line) => line.startsWith("| ")); + [lines[first], lines[first + 1]] = [lines[first + 1], lines[first]]; + }); + mutateGenerated("generated_duplicate", "generated_duplicate_row", (lines) => { + const first = lines.findIndex((line) => line.startsWith("| ")); + lines.splice(first + 1, 0, lines[first]); + }); + mutateGenerated("generated_missing", "generated_missing_row", (lines) => { + const first = lines.findIndex((line) => line.startsWith("| ")); + lines.splice(first, 1); + }); + mutateGenerated("source_count", "generated_source_count_mismatch", (lines) => { + const first = lines.findIndex((line) => line.startsWith("| ")); + lines.splice(first, 1); + }); + + const unsortedRoot = freshRoot("source_unsorted"); + editJson(path.join(unsortedRoot, "editorial/states/button.states.json"), (value) => { value.scenarios.reverse(); }); + expectFailure("source_unsorted", "source_unsorted", unsortedRoot); + + const duplicateRoot = freshRoot("scenario_duplicate"); + editJson(path.join(duplicateRoot, "editorial/states/button.states.json"), (value) => { value.scenarios.push({ ...value.scenarios[0] }); }); + expectFailure("scenario_duplicate", "scenario_duplicate", duplicateRoot); + + const missingRoot = freshRoot("fixture_scenario_missing"); + editJson(path.join(missingRoot, "editorial/states/button.states.json"), (value) => { value.scenarios = value.scenarios.filter((scenario) => scenario.id !== "action-focused"); }); + expectFailure("fixture_scenario_missing", "fixture_scenario_missing", missingRoot); + + const injectionRoot = freshRoot("markdown_injection"); + editJson(path.join(injectionRoot, "editorial/components/button.component.json"), (value) => { value.semantic_modes[0].element = "|`\nnext"; }); + const injection = run(injectionRoot); + const injectionOutput = fs.readFileSync(path.join(injectionRoot, "editorial/generated/keyboard-matrix.md"), "utf8"); + results.push({ + expected: "escaped_markdown_injection", + name: "markdown_injection", + ok: injection.status === 0 + && injectionOutput.includes("<script>alert(1)</script>|`
next") + && !injectionOutput.includes(""), + }); + + const referenceRoot = freshRoot("canonical_reference_discovery"); + fs.copyFileSync(path.join(referenceRoot, "editorial/components/button.component.json"), path.join(referenceRoot, "editorial/components/alternate.component.json")); + editJson(path.join(referenceRoot, "editorial/components/alternate.component.json"), (value) => { value.semantic_modes[0].element = "canonical alternate element"; }); + editJson(path.join(referenceRoot, "editorial/profile.json"), (value) => { value.component_records = ["components/alternate.component.json"]; }); + editJson(path.join(referenceRoot, "editorial/fixtures/button.fixture.json"), (value) => { value.component_record = "../components/alternate.component.json"; }); + const reference = run(referenceRoot); + const referenceOutput = fs.readFileSync(path.join(referenceRoot, "editorial/generated/keyboard-matrix.md"), "utf8"); + results.push({ expected: "canonical_profile_reference", name: "canonical_reference_discovery", ok: reference.status === 0 && referenceOutput.includes("canonical alternate element") }); + + const localeRoot = freshRoot("locale_determinism"); + const firstLocale = run(localeRoot, false, { LANG: "C", LC_ALL: "C" }); + const firstBytes = fs.readFileSync(path.join(localeRoot, "editorial/generated/state-matrix.md")); + const secondLocale = run(localeRoot, false, { LANG: "tr_TR.UTF-8", LC_ALL: "tr_TR.UTF-8" }); + const secondBytes = fs.readFileSync(path.join(localeRoot, "editorial/generated/state-matrix.md")); + results.push({ expected: "locale_independent_bytes", name: "locale_determinism", ok: firstLocale.status === 0 && secondLocale.status === 0 && firstBytes.equals(secondBytes) }); +} finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); +} + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +assert.equal(report.ok, true); diff --git a/scripts/test-validate-component-state-artifacts.mjs b/scripts/test-validate-component-state-artifacts.mjs new file mode 100644 index 0000000..7f2f946 --- /dev/null +++ b/scripts/test-validate-component-state-artifacts.mjs @@ -0,0 +1,177 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { artifactMetadata } from "./evidence-artifact-contract.mjs"; +import { sessionLink, sha256 } from "./capture-session-contract.mjs"; +import { resolveProfileRecords } from "./profile-record-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const sourceProfiles = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); +const validator = path.join(repositoryRoot, "scripts/validate-component-state.mjs"); +const finalizer = path.join(repositoryRoot, "scripts/finalize-component-state-evidence.mjs"); +const creator = path.join(repositoryRoot, "scripts/create-component-state-session.mjs"); +const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64"); + +function writeJson(file, value) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function prepareCanonical() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-state-artifacts-base-")); + const profileRoot = path.join(root, "profiles"); + const artifactRoot = path.join(root, "artifacts"); + fs.cpSync(sourceProfiles, profileRoot, { recursive: true }); + const receiptFile = path.join(artifactRoot, "capture-session.json"); + const created = spawnSync(process.execPath, [creator, "--root", profileRoot, "--output", receiptFile, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + if (created.status !== 0) throw new Error(`canonical session creation failed: ${created.stdout}${created.stderr}`); + const receiptBytes = fs.readFileSync(receiptFile); + const receipt = JSON.parse(receiptBytes); + const captureLink = sessionLink(receipt, sha256(receiptBytes)); + const capturedAt = new Date().toISOString(); + for (const entry of fs.readdirSync(profileRoot, { withFileTypes: true }).filter((item) => item.isDirectory())) { + const resolved = resolveProfileRecords(path.join(profileRoot, entry.name), []); + const fixtures = resolved.records.fixture[0].value; + const states = new Map(resolved.records.states[0].value.scenarios.map((scenario) => [scenario.id, scenario])); + for (const fixture of fixtures.scenarios) { + const scenario = states.get(fixture.id); + const prefix = path.join(artifactRoot, "runtime", `${entry.name}-${fixture.id}`); + const attributes = Object.fromEntries(Object.entries(scenario.expected.dom).filter(([key]) => !["active", "role"].includes(key))); + writeJson(`${prefix}.dom.json`, { + activation_count: scenario.expected.activation === "allowed" ? 1 : 0, + activation_key: fixture.activation_key, + active: scenario.expected.dom.active === "true", + attributes, + capture_session: captureLink, + captured_at: capturedAt, + channel: "dom", + profile_id: resolved.profile.id, + role: scenario.expected.dom.role, + scenario_id: fixture.id, + schema_version: "1.0", + semantic_mode: scenario.semantic_mode, + visual_states: scenario.expected.visual, + }); + writeJson(`${prefix}.ax.json`, { + capture_session: captureLink, + captured_at: capturedAt, + channel: "ax", + name: fixture.label, + profile_id: resolved.profile.id, + properties: Object.fromEntries(Object.entries(scenario.expected.ax).filter(([key]) => key !== "role")), + role: scenario.expected.ax.role, + scenario_id: fixture.id, + schema_version: "1.0", + semantic_mode: scenario.semantic_mode, + }); + fs.mkdirSync(path.dirname(`${prefix}.png`), { recursive: true }); + fs.writeFileSync(`${prefix}.png`, Buffer.concat([png, Buffer.from(`${entry.name}:${fixture.id}`)])); + } + } + const manifest = path.join(artifactRoot, "runtime-manifest.json"); + const child = spawnSync(process.execPath, [finalizer, "--root", profileRoot, "--artifact-root", artifactRoot, "--output", manifest, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + if (child.status !== 0) throw new Error(`canonical artifact finalization failed: ${child.stdout}${child.stderr}`); + return { artifactRoot, manifest, profileRoot, root }; +} + +function codes(output) { + return (output.failures ?? []).map((failure) => failure.code); +} + +function findPass(manifest, scenario, channel, record = 0) { + return manifest.records[record].passes.find((pass) => pass.scenario_id === scenario && pass.channel === channel); +} + +function refresh(pass, artifactRoot, preserveDimensions = false) { + const file = path.join(artifactRoot, pass.artifact.path); + const dimensions = { height: pass.artifact.height, width: pass.artifact.width }; + pass.artifact = { path: pass.artifact.path, ...artifactMetadata(fs.readFileSync(file), pass.artifact.media_type) }; + if (preserveDimensions) Object.assign(pass.artifact, dimensions); +} + +function mutateJsonArtifact(manifest, artifactRoot, scenario, channel, mutate) { + const pass = findPass(manifest, scenario, channel); + const file = path.join(artifactRoot, pass.artifact.path); + const document = JSON.parse(fs.readFileSync(file)); + mutate(document); + writeJson(file, document); + refresh(pass, artifactRoot); + return pass; +} + +const cases = [ + { name: "canonical", valid: true }, + { expect: "evidence_channel_duplicate", name: "channel_substitution", mutate: (m) => { findPass(m, "action-focused", "ax").channel = "dom"; } }, + { expect: "evidence_png_invalid", name: "dummy_visual_bytes", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); fs.writeFileSync(path.join(a, p.artifact.path), "not a png"); refresh(p, a, true); } }, + { expect: "evidence_json_invalid", name: "png_as_dom", mutate: (m, a) => { const p = findPass(m, "action-focused", "dom"); fs.writeFileSync(path.join(a, p.artifact.path), png); refresh(p, a); } }, + { expect: "evidence_artifact_symlink", name: "artifact_symlink", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); const f = path.join(a, p.artifact.path); fs.rmSync(f); fs.symlinkSync("/etc/hosts", f); } }, + { expect: "evidence_artifact_reused", name: "artifact_path_reuse", mutate: (m) => { const p = findPass(m, "action-loading-busy", "visual"); p.artifact = structuredClone(findPass(m, "action-focused", "visual").artifact); } }, + { expect: "evidence_artifact_content_reused", name: "artifact_content_reuse", mutate: (m, a) => { const source = findPass(m, "action-focused", "visual"); const target = findPass(m, "action-loading-busy", "visual"); fs.copyFileSync(path.join(a, source.artifact.path), path.join(a, target.artifact.path)); refresh(target, a); } }, + { expect: "evidence_scenario_unknown", name: "unknown_scenario", mutate: (m) => { findPass(m, "action-focused", "dom").scenario_id = "unknown-scenario"; } }, + { expect: "runtime_manifest_profile_unknown", name: "unknown_profile", mutate: (m) => { m.records[0].profile_id = "unknown-profile"; } }, + { expect: "evidence_artifact_integrity", name: "forged_hash", mutate: (m) => { findPass(m, "action-focused", "visual").artifact.sha256 = `sha256:${"0".repeat(64)}`; } }, + { expect: "evidence_artifact_missing", name: "missing_artifact", mutate: (m) => { findPass(m, "action-focused", "visual").artifact.path = "runtime/missing.png"; } }, + { expect: "evidence_artifact_type_invalid", name: "directory_artifact", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); p.artifact.path = "runtime/directory.png"; fs.mkdirSync(path.join(a, p.artifact.path)); } }, + { expect: "evidence_artifact_escape", name: "outside_artifact", mutate: (m) => { findPass(m, "action-focused", "visual").artifact.path = "../outside.png"; } }, + { expect: "evidence_dom_content_mismatch", name: "pressed_dom_false", mutate: (m, a) => { const p = findPass(m, "toggle-focused-pressed", "dom"); const f = path.join(a, p.artifact.path); const d = JSON.parse(fs.readFileSync(f)); d.attributes["aria-pressed"] = "false"; writeJson(f, d); refresh(p, a); } }, + { expect: "evidence_dom_content_mismatch", name: "disabled_dom_false", mutate: (m, a) => { const p = findPass(m, "action-disabled-busy", "dom"); const f = path.join(a, p.artifact.path); const d = JSON.parse(fs.readFileSync(f)); d.attributes.disabled = "false"; writeJson(f, d); refresh(p, a); } }, + { expect: "evidence_runtime_identity_mismatch", name: "activation_key_mismatch", mutate: (m, a) => { const p = findPass(m, "action-focused", "dom"); const f = path.join(a, p.artifact.path); const d = JSON.parse(fs.readFileSync(f)); d.activation_key = "Space"; writeJson(f, d); refresh(p, a); } }, + { expect: "evidence_recorded_at_mismatch", name: "stale_runtime", mutate: (m) => { m.recorded_at = "2000-01-01T00:00:00.000Z"; for (const record of m.records) for (const pass of record.passes) pass.recorded_at = m.recorded_at; } }, + { expect: "evidence_dom_schema_invalid", name: "unexpected_certification", mutate: (m, a) => { const p = findPass(m, "action-focused", "dom"); const f = path.join(a, p.artifact.path); const d = JSON.parse(fs.readFileSync(f)); d.certification = true; writeJson(f, d); refresh(p, a); } }, + { expect: "evidence_dom_schema_invalid", name: "dom_extra_nested_field", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.attributes.certification = "true"; }); } }, + { expect: "evidence_dom_schema_invalid", name: "dom_null_type", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.active = null; }); } }, + { expect: "evidence_dom_schema_invalid", name: "dom_missing_key", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { delete d.activation_count; }); } }, + { expect: "evidence_ax_schema_invalid", name: "ax_extra_nested_field", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { d.properties.certification = true; }); } }, + { expect: "evidence_ax_schema_invalid", name: "ax_null_type", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { d.properties.focused = null; }); } }, + { expect: "evidence_ax_schema_invalid", name: "ax_missing_key", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { delete d.role; }); } }, + { expect: "capture_session_receipt_mismatch", name: "receipt_tamper", mutate: (m, a) => { const f = path.join(a, "capture-session.json"); const r = JSON.parse(fs.readFileSync(f)); r.nonce = "0".repeat(64); writeJson(f, r); } }, + { expect: "capture_session_missing", name: "receipt_missing", mutate: (m, a) => { fs.rmSync(path.join(a, "capture-session.json")); } }, + { expect: "capture_session_mismatch", name: "cross_session_artifact_mix", mutate: (m, a) => { const target = findPass(m, "action-focused", "dom"); const sourceManifest = JSON.parse(fs.readFileSync(alternate.manifest)); const source = findPass(sourceManifest, "action-focused", "dom"); fs.copyFileSync(path.join(alternate.artifactRoot, source.artifact.path), path.join(a, target.artifact.path)); refresh(target, a); } }, + { expect: "capture_session_mismatch", name: "artifact_revision_drift", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.capture_session.revision = "0".repeat(40); }); } }, + { expect: "capture_session_mismatch", name: "artifact_environment_drift", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.capture_session.environment.browser = "Different browser"; }); } }, + { expect: "capture_session_mismatch", name: "artifact_attempt_drift", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.capture_session.attempt += 1; }); } }, + { expect: "capture_session_time_outside", name: "artifact_time_outside_session", mutate: (m, a) => { for (const channel of ["dom", "ax"]) { const p = mutateJsonArtifact(m, a, "action-focused", channel, (d) => { d.captured_at = "2000-01-01T00:00:00.000Z"; }); p.recorded_at = "2000-01-01T00:00:00.000Z"; } findPass(m, "action-focused", "visual").recorded_at = "2000-01-01T00:00:00.000Z"; } }, + { expect: "capture_session_time_outside", name: "completed_before_started", mutate: (m) => { m.session.completed_at = "2000-01-01T00:00:00.000Z"; m.recorded_at = m.session.completed_at; } }, + { expect: "at_evidence_unverified", name: "fake_at", mutate: (m) => { findPass(m, "action-focused", "ax").channel = "at"; } }, + { expect: "runtime_manifest_identity_mismatch", name: "run_identity_mismatch", mutate: (m) => { findPass(m, "action-focused", "dom").run.id = "different-run"; } }, +]; + +const base = prepareCanonical(); +const alternate = prepareCanonical(); +const results = cases.map((testCase) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `stylegallery-state-artifacts-${testCase.name}-`)); + try { + fs.cpSync(base.root, root, { recursive: true }); + const artifactRoot = path.join(root, "artifacts"); + const manifestFile = path.join(artifactRoot, "runtime-manifest.json"); + const manifest = JSON.parse(fs.readFileSync(manifestFile)); + testCase.mutate?.(manifest, artifactRoot); + writeJson(manifestFile, manifest); + const child = spawnSync(process.execPath, [validator, "--root", path.join(root, "profiles"), "--artifact-root", artifactRoot, "--runtime-manifest", manifestFile, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const output = JSON.parse(child.stdout); + const actual = codes(output); + const ok = testCase.valid ? child.status === 0 && output.ok === true : child.status !== 0 && output.ok === false && actual.includes(testCase.expect); + return { actual: { codes: actual, status: child.status }, expected: testCase.expect ?? "valid", name: testCase.name, ok }; + } finally { + fs.rmSync(root, { force: true, recursive: true }); + } +}); + +const closedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-state-artifacts-closed-")); +fs.cpSync(base.root, closedRoot, { recursive: true }); +fs.writeFileSync(path.join(closedRoot, "artifacts/runtime/unmanifested.txt"), "rogue"); +const closed = spawnSync(process.execPath, [finalizer, "--root", path.join(closedRoot, "profiles"), "--artifact-root", path.join(closedRoot, "artifacts"), "--json"], { cwd: repositoryRoot, encoding: "utf8" }); +const closedOutput = JSON.parse(closed.stdout); +results.push({ actual: { codes: codes(closedOutput), status: closed.status }, expected: "runtime_artifact_unmanifested", name: "unmanifested_closed_set", ok: closed.status !== 0 && codes(closedOutput).includes("runtime_artifact_unmanifested") }); +results.push({ actual: { codes: codes(closedOutput), status: closed.status }, expected: "capture_session_replay", name: "session_replay", ok: closed.status !== 0 && codes(closedOutput).includes("capture_session_replay") }); +fs.rmSync(closedRoot, { force: true, recursive: true }); +fs.rmSync(base.root, { force: true, recursive: true }); +fs.rmSync(alternate.root, { force: true, recursive: true }); + +const failures = results.filter((result) => !result.ok).map((result) => `${result.name}:${result.expected}`); +const report = { adversarial: results.length - 1, failures, ok: failures.length === 0, results }; +console.log(JSON.stringify(report, null, 2)); +if (!report.ok) process.exitCode = 1; diff --git a/scripts/test-validate-component-state.mjs b/scripts/test-validate-component-state.mjs new file mode 100644 index 0000000..49857e9 --- /dev/null +++ b/scripts/test-validate-component-state.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const sourceRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); +const validator = path.join(repositoryRoot, "scripts/validate-component-state.mjs"); + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function writeJson(file, value) { + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function edit(root, profile, relative, mutate) { + const file = path.join(root, profile, relative); + const value = readJson(file); + mutate(value); + writeJson(file, value); +} + +function scenario(value, id) { + return value.scenarios.find((candidate) => candidate.id === id); +} + +function pass(value, scenarioId, channel) { + return value.passes.find((candidate) => candidate.scenario_id === scenarioId && candidate.channel === channel); +} + +function run(root) { + const child = spawnSync(process.execPath, [validator, "--root", root, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + let report = { failures: [], ok: false, parse_error: true }; + try { report = JSON.parse(child.stdout); } catch { report = { failures: [], ok: false, parse_error: true }; } + return { report, status: child.status }; +} + +const cases = [ + ["unresolved_conditional", "conditional_unresolved", (root) => edit(root, "editorial", "components/button.component.json", (value) => { delete value.semantic_modes[0].aria_states[2].resolution; })], + ["missing_applicability_reason", "applicability_reason_required", (root) => edit(root, "editorial", "components/button.component.json", (value) => { delete value.semantic_modes[0].aria_states[0].reason; })], + ["missing_role_required_state", "role_required_aria_state", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.semantic_modes[1].aria_states = value.semantic_modes[1].aria_states.filter((state) => state.name !== "pressed"); })], + ["switch_mixed", "switch_mixed_prohibited", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.semantic_modes[1] = { ...value.semantic_modes[1], aria_states: [{ name: "checked", status: "required", values: ["false", "mixed"] }], id: "switch", role: "switch" }; })], + ["missing_environment", "evidence_schema_invalid", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { delete value.passes[0].environment; })], + ["false_aggregate_pass", "aggregate_pass_forbidden", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { value.aggregate_pass = true; })], + ["normative_conflict", "normative_activation_conflict", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-focused").expected.activation = "suppressed"; })], + ["preview_rolled_stable", "preview_rolled_stable", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.versions.delivery_channel = "stable"; })], + ["expanded_surface", "expanded_surface_mismatch", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "disclosure-expanded-loading").expected.ax.expanded = false; })], + ["disabled_activation", "disabled_activation", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-disabled-busy").expected.activation = "allowed"; })], + ["loading_without_busy", "loading_busy_state_required", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-loading-busy").states = ["focus", "loading"]; })], + ["loading_as_disabled", "loading_disabled_conflated", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-loading-busy").expected.visual.push("disabled"); })], + ["required_channel", "required_channel_missing", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { value.passes = value.passes.filter((candidate) => !(candidate.scenario_id === "action-focused" && candidate.channel === "ax")); })], + ["self_asserted_at", "at_evidence_unverified", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { value.passes[0].channel = "at"; value.passes[0].environment.kind = "assistive_technology"; })], + ["duplicate_mode", "semantic_mode_duplicate", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.semantic_modes.push(structuredClone(value.semantic_modes[0])); })], + ["duplicate_pass", "evidence_pass_duplicate", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { value.passes.push(structuredClone(value.passes[0])); })], + ["duplicate_state_set", "state_set_duplicate", (root) => edit(root, "editorial", "states/button.states.json", (value) => { const copy = structuredClone(value.scenarios[0]); copy.id = "duplicate-disabled-busy"; copy.states.reverse(); value.scenarios.push(copy); })], + ["pressed_surface", "pressed_surface_mismatch", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "toggle-focused-pressed").expected.dom["aria-pressed"] = "false"; })], + ["busy_surface", "busy_surface_mismatch", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-loading-busy").expected.ax.busy = false; })], + ["disabled_surface", "disabled_surface_mismatch", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-disabled-busy").expected.dom.disabled = "false"; })], + ["unknown_evidence_scenario", "evidence_scenario_unknown", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { value.passes[0].scenario_id = "unknown-scenario"; })], + ["duplicate_channel", "evidence_channel_duplicate", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { const copy = structuredClone(value.passes[0]); copy.id = "unique-pass-id"; copy.artifact.path = "runtime/unique.png"; copy.artifact.sha256 = `sha256:${"1".repeat(64)}`; value.passes.push(copy); })], + ["activation_key", "fixture_activation_key_invalid", (root) => edit(root, "editorial", "fixtures/button.fixture.json", (value) => { value.scenarios[1].activation_key = "Escape"; })], + ["unexercised_scenario", "fixture_scenario_unexercised", (root) => edit(root, "editorial", "fixtures/button.fixture.json", (value) => { value.scenarios = value.scenarios.filter((candidate) => candidate.id !== "action-loading-busy"); })], + ["profile_field_missing", "profile_reference_required", (root) => edit(root, "editorial", "profile.json", (value) => { delete value.component_records; })], + ["profile_reference_duplicate", "profile_reference_duplicate", (root) => edit(root, "editorial", "profile.json", (value) => { value.component_records.push(value.component_records[0]); })], + ["profile_reference_unknown", "profile_reference_unknown", (root) => edit(root, "editorial", "profile.json", (value) => { value.component_records[0] = "mystery/button.json"; })], + ["profile_reference_outside", "profile_reference_outside", (root) => edit(root, "editorial", "profile.json", (value) => { value.component_records[0] = "../terminal/components/button.component.json"; })], + ["profile_reference_missing", "profile_reference_missing", (root) => fs.rmSync(path.join(root, "editorial/components/button.component.json"))], + ["profile_reference_symlink", "profile_reference_symlink", (root) => { const file = path.join(root, "editorial/components/button.component.json"); fs.rmSync(file); fs.symlinkSync(path.join(root, "terminal/components/button.component.json"), file); }], + ["profile_reference_redirect", "profile_reference_redirect", (root) => { const directory = path.join(root, "editorial/components"); const actual = path.join(root, "editorial/components-real"); fs.renameSync(directory, actual); fs.symlinkSync(actual, directory); }], + ["cross_profile_swap", "profile_identity_mismatch", (root) => fs.copyFileSync(path.join(root, "terminal/components/button.component.json"), path.join(root, "editorial/components/button.component.json"))], + ["generated_reference_missing", "profile_reference_missing", (root) => fs.rmSync(path.join(root, "editorial/generated/state-matrix.md"))], +]; + +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr5-state-")); +const results = []; +try { + for (const [name, expected, mutate] of cases) { + const root = path.join(tempRoot, name); + fs.cpSync(sourceRoot, root, { recursive: true }); + mutate(root); + const child = run(root); + const codes = child.report.failures.map((failure) => failure.code); + results.push({ actual: { codes, scaffold: child.report.scaffold === true, status: child.status }, expected, name, ok: child.status > 0 && codes.includes(expected) && child.report.scaffold !== true }); + } + const canonical = run(sourceRoot); + results.push({ actual: { status: canonical.status }, expected: "canonical_valid", name: "canonical", ok: canonical.status === 0 && canonical.report.ok === true && canonical.report.scaffold !== true }); +} finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); +} + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +assert.equal(report.ok, true); diff --git a/scripts/test-validate-consumer-reference.mjs b/scripts/test-validate-consumer-reference.mjs index 2b97af6..544a23e 100644 --- a/scripts/test-validate-consumer-reference.mjs +++ b/scripts/test-validate-consumer-reference.mjs @@ -31,6 +31,26 @@ const baseItem = { }, }; +function applyGovernedProfile(item) { + Object.assign(item, { + artifact_mode: "governed_local", + component_records: ["components/button.component.json"], + default: false, + environment_assumptions: { reset: { body_margin: "0", box_sizing: "border-box", figure_margin: "0" }, user_agent_styles: "Preserve declared browser defaults." }, + evidence_records: ["evidence/button.evidence.json"], + example_only: true, + fixture_records: ["fixtures/button.fixture.json"], + generated_records: ["generated/evidence-coverage.md", "generated/keyboard-matrix.md", "generated/state-matrix.md"], + layout_source_sha: "775430bbaf4ee208a642220f440f6926d79c90a3", + local_foundations: "local-foundations.json", + profile_kind: "governed_local", + related_fixture_set_id: "layout-identity-adversarial-pair", + selection: { method: "profile_path", required: true }, + state_records: ["states/button.states.json"], + tokens: "tokens.dtcg.json", + }); +} + const behaviorCases = [ { expect: null, name: "valid_declared_handoff" }, { expect: null, mutate: (item) => { item.handoff = { reason: "This fixture has no consumer-specific reference requirement.", status: "not_applicable" }; }, name: "valid_not_applicable_handoff" }, @@ -114,6 +134,14 @@ const parityCases = [ { expect: "support_status_invalid", mutate: (item) => { item.support.status = "paused"; }, name: "schema_support_status_enum", rules: ["support.status.enum"], schemaValid: false }, { expect: "stable_support_ended", mutate: (item) => { item.maturity = "stable"; item.support.status = "ended"; }, name: "schema_stable_support_all_of", rules: ["allOf.stable_ended"], schemaValid: false }, { expect: "deprecated_migration_required", mutate: (item) => { item.maturity = "deprecated"; }, name: "schema_deprecated_all_of", rules: ["allOf.deprecated_required"], schemaValid: false }, + ...["component_records", "evidence_records", "fixture_records", "generated_records", "state_records"].map((field) => ({ + expect: "profile_field_required", + mutate: (item) => { applyGovernedProfile(item); delete item[field]; }, + name: `schema_governed_required_${field}`, + rules: [`allOf.governed.required.${field}`], + schemaValid: false, + })), + { expect: "profile_reference_array_invalid", mutate: (item) => { applyGovernedProfile(item); item.generated_records[2] = item.generated_records[1]; }, name: "schema_governed_generated_unique", rules: ["generated_records.uniqueItems"], schemaValid: false }, { expect: null, mutate: (item) => { item.maturity = "stable"; }, name: "schema_valid_stable_active", rules: ["maturity.enum", "allOf.stable_ended"], schemaValid: true }, { expect: null, mutate: (item) => { item.handoff = { reason: "This reference is intentionally not applicable.", status: "not_applicable" }; item.maturity = "deprecated"; item.removal_trigger = "Remove after migration completes."; item.replacement = "replacement-item"; item.support.status = "ended"; }, name: "schema_valid_deprecated", rules: ["handoff.not_applicable.required", "handoff.reason.type", "handoff.reason.minLength", "handoff.reason.pattern", "allOf.deprecated_required", "support.status.enum"], schemaValid: true }, ...validArtifactCases, @@ -129,6 +157,8 @@ const expectedSchemaRuleNames = [ "owner.enforcement.const", "owner.name.type", "owner.name.minLength", "removal_trigger.type", "removal_trigger.minLength", "replacement.type", "replacement.minLength", "review_independence.const", "schema_version.const", "support.type", "support.additionalProperties", "support.required.status", "support.status.enum", "allOf.stable_ended", "allOf.deprecated_required", + ...["component_records", "evidence_records", "fixture_records", "generated_records", "state_records"].map((field) => `allOf.governed.required.${field}`), + "generated_records.uniqueItems", ]; function cloneItem() { diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index c1e3503..b972587 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -79,6 +79,9 @@ const files = { "", ].join("\n"), ".github/workflows/validate.yml": [ + "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", + "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", + "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", "node -c scripts/validate-governance.mjs", "node -c scripts/test-validate-governance.mjs", "node scripts/validate-governance.mjs --json", @@ -94,6 +97,10 @@ const files = { "node scripts/validate-consumer-reference.mjs --json", "node scripts/test-validate-consumer-reference.mjs --json", "node scripts/test-consumer-reference-sentinel.mjs", + "node scripts/create-component-state-session.mjs", + "STATE_SESSION_RECEIPT=receipt.json", + "node scripts/finalize-component-state-evidence.mjs", + "--runtime-manifest manifest.json", "node scripts/validate-baseline-manifest.mjs --json", "node scripts/test-validate-baseline-manifest.mjs --json", "node scripts/test-summarize-sentinel-calibration.mjs", @@ -122,6 +129,7 @@ const files = { "| Layout recipes | `recipes/*.md` | Manual | None | `scripts/validate-okf.mjs` | Recipe owner |", "| Quality gates and evidence | `quality/**/*.md` | Manual | None | `scripts/validate-okf.mjs` | Quality owner |", "| Consumer reference contract | `consumer-reference/contract.md` | Manual | None | `stable` | Receiver changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", + "| Component-state evidence matrices | Declared component, state, fixture, and evidence records | `scripts/generate-consumer-reference-evidence.mjs` | Six profile matrices | `generated` | Source or generator changes. | `scripts/validate-component-state.mjs` | Validation owner |", "| Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/calibration-record.schema.json`, `consumer-reference/baselines/*.json` | Playwright | Snapshot and raw calibration evidence | `experimental` | Renderer or baseline changes. | `scripts/validate-baseline-manifest.mjs` | Validation owner |", "| Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner |", "| Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner |", @@ -137,6 +145,15 @@ const files = { "Do not hand-edit generated artifacts.", "`scripts/pattern-data.mjs`", "`scripts/generate-patterns.mjs`", + "`scripts/generate-consumer-reference-evidence.mjs`", + "Browser state evidence begins with `scripts/create-component-state-session.mjs`", + "Validation uses the receipt and completed manifest interval, not a wall-clock maximum age", + "design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md", + "design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md", + "design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md", + "design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md", + "design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md", + "design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md", "", "## Lifecycle States", "`draft` `stable` `deprecated` `experimental` `generated`", @@ -222,6 +239,11 @@ const cases = [ { name: "missing_domain_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail.", "Domain fixtures must fail.") }, expect: "quality/evidence/executable-evidence.md: missing Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail." }, { name: "missing_sentinel_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/test-consumer-reference-sentinel.mjs\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-consumer-reference-sentinel.mjs" }, { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, + { + name: "floating_action_ref", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", "uses: actions/checkout@v4") }, + expect: ".github/workflows/validate.yml: floating or unlabeled action ref uses: actions/checkout@v4", + }, { name: "missing_scoped_checkout_sha_trust", mutate: { diff --git a/scripts/test-validate-reference-profiles.mjs b/scripts/test-validate-reference-profiles.mjs index 2e5a605..64ac14c 100644 --- a/scripts/test-validate-reference-profiles.mjs +++ b/scripts/test-validate-reference-profiles.mjs @@ -14,14 +14,18 @@ const layoutSha = "775430bbaf4ee208a642220f440f6926d79c90a3"; const baseProfile = { artifact_mode: "governed_local", + component_records: ["components/button.component.json"], default: false, environment_assumptions: { reset: { body_margin: "0", box_sizing: "border-box", figure_margin: "0" }, user_agent_styles: "Preserve browser defaults except for the declared reset.", }, example_only: true, + evidence_records: ["evidence/button.evidence.json"], + fixture_records: ["fixtures/button.fixture.json"], fixture_independence: "related", handoff: { record: profilePath, status: "declared" }, + generated_records: ["generated/evidence-coverage.md", "generated/keyboard-matrix.md", "generated/state-matrix.md"], id: "editorial-reference-profile", layout_source_sha: layoutSha, local_foundations: "local-foundations.json", @@ -32,6 +36,7 @@ const baseProfile = { review_independence: "single_account", schema_version: "1.0", selection: { method: "profile_path", required: true }, + state_records: ["states/button.states.json"], support: { status: "active" }, tokens: "tokens.dtcg.json", }; diff --git a/scripts/validate-component-state.mjs b/scripts/validate-component-state.mjs new file mode 100644 index 0000000..20d9e7d --- /dev/null +++ b/scripts/validate-component-state.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { canonicalIntended, readCaptureSession, sameJson, withinSession } from "./capture-session-contract.mjs"; +import { compileSchemas, readRecord, validateEvidenceArtifacts, validateProfile } from "./component-state-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const options = { artifactRoot: undefined, json: false, root: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), runtimeManifest: undefined }; +const failures = []; +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (["--artifact-root", "--runtime-manifest", "--root"].includes(argument)) { + const value = process.argv[index + 1]; + if (!value) failures.push({ code: "argument_value_required", message: `${argument} requires a path`, path: "" }); + else { + if (argument === "--artifact-root") options.artifactRoot = path.resolve(value); + if (argument === "--runtime-manifest") options.runtimeManifest = path.resolve(value); + if (argument === "--root") options.root = path.resolve(value); + index += 1; + } + } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); +} + +const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/schema")); +let runtimeManifest; +let capture; +if (options.runtimeManifest) { + runtimeManifest = readRecord(options.runtimeManifest, failures); + if (!options.artifactRoot) failures.push({ code: "runtime_manifest_artifact_root_required", message: "--runtime-manifest requires --artifact-root", path: "" }); + if (runtimeManifest && !schemas.runtime(runtimeManifest)) for (const error of schemas.runtime.errors ?? []) failures.push({ code: "runtime_manifest_schema_invalid", message: `${error.instancePath || "/"} ${error.message}`, path: options.runtimeManifest }); + if (options.artifactRoot) capture = readCaptureSession(path.join(options.artifactRoot, "capture-session.json"), schemas.capture, failures); + if (capture && runtimeManifest) { + const session = runtimeManifest.session; + if (!session || !sameJson(session.receipt, capture.artifact) || session.receipt_sha256 !== capture.digest) failures.push({ code: "capture_session_receipt_mismatch", message: "manifest receipt metadata does not match capture-session.json", path: options.runtimeManifest }); + const expectedLink = capture.link; + for (const key of ["session_id", "nonce", "started_at", "revision", "branch", "attempt", "environment"]) if (!sameJson(session?.[key], expectedLink[key])) failures.push({ code: "capture_session_mismatch", message: `manifest ${key} differs from receipt`, path: options.runtimeManifest }); + for (const key of ["repository", "intended"]) if (!sameJson(session?.[key], capture.receipt[key])) failures.push({ code: "capture_session_mismatch", message: `manifest ${key} differs from receipt`, path: options.runtimeManifest }); + if (runtimeManifest.recorded_at !== session?.completed_at) failures.push({ code: "evidence_recorded_at_mismatch", message: "manifest recorded_at must equal session completed_at", path: options.runtimeManifest }); + if (!withinSession(session?.completed_at, capture.receipt.started_at, session?.completed_at)) failures.push({ code: "capture_session_time_outside", message: "completed_at precedes session start", path: options.runtimeManifest }); + const canonical = canonicalIntended(options.root, failures); + if (!sameJson(capture.receipt.intended, canonical)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from current canonical records", path: options.runtimeManifest }); + const run = runtimeManifest.run; + if (run?.id !== capture.receipt.session_id || run?.repository !== capture.receipt.repository || run?.revision !== capture.receipt.revision || run?.attempt !== capture.receipt.attempt) failures.push({ code: "capture_session_mismatch", message: "manifest run differs from receipt", path: options.runtimeManifest }); + for (const record of runtimeManifest.records ?? []) { + for (const pass of record.passes ?? []) { + if (!sameJson(pass.environment, runtimeManifest.environment) || !sameJson(pass.run, run)) failures.push({ code: "runtime_manifest_identity_mismatch", message: `${pass.id} does not match the closed manifest identity`, path: options.runtimeManifest }); + if (!sameJson(pass.session, capture.link) || !sameJson(pass.environment, capture.receipt.environment) || !sameJson(pass.run, run)) failures.push({ code: "capture_session_mismatch", message: `${pass.id} pass identity differs from receipt`, path: options.runtimeManifest }); + } + } + } +} else if (options.artifactRoot) failures.push({ code: "runtime_manifest_required", message: "artifact validation requires --runtime-manifest", path: "" }); + +const profiles = fs.existsSync(options.root) + ? fs.readdirSync(options.root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && fs.existsSync(path.join(options.root, entry.name, "profile.json"))).map((entry) => entry.name).sort() + : []; +if (profiles.length === 0) failures.push({ code: "profile_root_empty", message: "governed-local root must contain profiles", path: options.root }); +for (const profile of profiles) { + const profileRoot = path.join(options.root, profile); + failures.push(...validateProfile(profileRoot, schemas)); + const profileId = JSON.parse(fs.readFileSync(path.join(profileRoot, "profile.json"), "utf8")).id; + const matching = runtimeManifest?.records?.filter((record) => record.profile_id === profileId) ?? []; + if (runtimeManifest && matching.length !== 1) failures.push({ code: "runtime_manifest_profile_count", message: `${profileId} requires exactly one runtime evidence record`, path: options.runtimeManifest }); + if (options.artifactRoot && runtimeManifest) failures.push(...validateEvidenceArtifacts(profileRoot, options.artifactRoot, matching[0], schemas, runtimeManifest.session)); +} +if (runtimeManifest) { + const known = new Set(profiles.map((profile) => JSON.parse(fs.readFileSync(path.join(options.root, profile, "profile.json"), "utf8")).id)); + for (const record of runtimeManifest.records ?? []) if (!known.has(record.profile_id)) failures.push({ code: "runtime_manifest_profile_unknown", message: `${record.profile_id} is not a governed profile`, path: options.runtimeManifest }); +} +if (runtimeManifest && options.artifactRoot) { + const expected = new Set((runtimeManifest.records ?? []).flatMap((record) => (record.passes ?? []).map((pass) => pass.artifact?.path)).filter(Boolean)); + if (expected.size !== 30) failures.push({ code: "runtime_artifact_count_mismatch", message: `manifest requires exactly 30 unique artifacts, found ${expected.size}`, path: options.runtimeManifest }); + const runtimeRoot = path.join(options.artifactRoot, "runtime"); + const actual = fs.existsSync(runtimeRoot) ? fs.readdirSync(runtimeRoot, { withFileTypes: true }) : []; + for (const entry of actual) { + const reference = `runtime/${entry.name}`; + if (!entry.isFile() || !expected.has(reference)) failures.push({ code: "runtime_artifact_unmanifested", message: `${reference} is not a declared regular session artifact`, path: runtimeRoot }); + } + for (const reference of expected) if (!fs.existsSync(path.join(options.artifactRoot, reference))) failures.push({ code: "runtime_artifact_missing", message: `${reference} is missing`, path: runtimeRoot }); +} + +const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()]; +const result = { checkedProfiles: profiles.length, failures: uniqueFailures, ok: uniqueFailures.length === 0, warnings: [] }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`ok: ${profiles.length} governed-local component profiles\n`); +else process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/validate-consumer-reference.mjs b/scripts/validate-consumer-reference.mjs index 0bf454f..2a3db8c 100644 --- a/scripts/validate-consumer-reference.mjs +++ b/scripts/validate-consumer-reference.mjs @@ -2,12 +2,14 @@ import fs from "node:fs"; import path from "node:path"; +import Ajv2020 from "ajv/dist/2020.js"; import { isPlainObject, validateItemSchema } from "./consumer-reference-schema.mjs"; import { validateReferenceProfile, validateReferenceProfileSet } from "./reference-profile-contract.mjs"; const root = process.cwd(); const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "item.schema.json"), "utf8")); +const validateFullItemSchema = new Ajv2020({ allErrors: true, strict: false }).compile(schema); const failures = []; const warnings = []; const canonicalProfilePaths = [ @@ -161,6 +163,9 @@ function validateItem(relative) { } throw error; } + if (!validateFullItemSchema(item)) { + for (const error of validateFullItemSchema.errors ?? []) addFailure("item_schema_invalid", relative, `${error.instancePath || "/"} ${error.message}`); + } for (const finding of validateItemSchema(item, schema)) addFailure(finding.code, relative, finding.message); if (!isPlainObject(item)) return; validateHandoff(item, relative); diff --git a/scripts/validate-domains.mjs b/scripts/validate-domains.mjs index 424fa3d..e6b3861 100644 --- a/scripts/validate-domains.mjs +++ b/scripts/validate-domains.mjs @@ -184,10 +184,12 @@ function checkLeaf(domain, relative, expectedSourcePath, titles) { } function rejectUndeclaredDomainDocuments() { + const generatedReference = /^design-engineering\/reference-profiles\/governed-local\/(?:editorial|terminal)\/generated\/(?:state-matrix|keyboard-matrix|evidence-coverage)\.md$/; for (const domain of domains) { const declared = new Set([`${domain.slug}/index.md`, ...domain.leaves.map(([leaf]) => leaf), ...(domain.referenceDocuments ?? [])]); for (const absolute of walkMarkdown(path.join(root, domain.slug))) { const relative = path.relative(root, absolute); + if (generatedReference.test(relative)) continue; if (!declared.has(relative)) failures.push(`${relative}: undeclared governed domain document`); } } diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index c565d85..2c23cd6 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -46,6 +46,11 @@ const requiredCodeowners = [ "/scripts/validate-*.mjs @changeroa", "/scripts/test-validate-*.mjs @changeroa", ]; +const immutableActionPins = [ + "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", + "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", + "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", +]; function read(relative) { const target = path.join(root, relative); @@ -86,6 +91,7 @@ function requireGovernanceMatrix() { "Layout recipes", "Quality gates and evidence", "Consumer reference contract", + "Component-state evidence matrices", "Proposed Chromium sentinel", "Domain manifest and scope decision", "Layout domain hub", @@ -104,6 +110,14 @@ function requireGovernanceMatrix() { requireIncludes("GOVERNANCE.md", "`scripts/validate-patterns.mjs`, `scripts/validate-catalog.mjs`, `scripts/validate-governance.mjs`"); requireIncludes("GOVERNANCE.md", "Source-lineage URL changes, generated drift, category changes, or pattern count changes."); requireIncludes("GOVERNANCE.md", "Generated structure changes, generated-warning changes, or generated metadata changes."); + requireIncludes("GOVERNANCE.md", "`scripts/generate-consumer-reference-evidence.mjs`"); + requireIncludes("GOVERNANCE.md", "Browser state evidence begins with `scripts/create-component-state-session.mjs`"); + requireIncludes("GOVERNANCE.md", "Validation uses the receipt and completed manifest interval, not a wall-clock maximum age"); + for (const profile of ["editorial", "terminal"]) { + for (const artifact of ["state-matrix.md", "keyboard-matrix.md", "evidence-coverage.md"]) { + requireIncludes("GOVERNANCE.md", `design-engineering/reference-profiles/governed-local/${profile}/generated/${artifact}`); + } + } } function requireLifecycleStates() { @@ -143,6 +157,10 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "node scripts/validate-consumer-reference.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-consumer-reference.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-consumer-reference-sentinel.mjs"); + requireIncludes(".github/workflows/validate.yml", "node scripts/create-component-state-session.mjs"); + requireIncludes(".github/workflows/validate.yml", "STATE_SESSION_RECEIPT="); + requireIncludes(".github/workflows/validate.yml", "node scripts/finalize-component-state-evidence.mjs"); + requireIncludes(".github/workflows/validate.yml", "--runtime-manifest"); requireIncludes(".github/workflows/validate.yml", "node scripts/validate-baseline-manifest.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-baseline-manifest.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-summarize-sentinel-calibration.mjs"); @@ -159,6 +177,15 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "contents: read"); } +function requireImmutableActions() { + const relative = ".github/workflows/validate.yml"; + const workflow = read(relative); + for (const line of workflow.split("\n").filter((entry) => /^\s*uses:\s+actions\//.test(entry))) { + if (!/^\s*uses:\s+actions\/[a-z0-9-]+@[a-f0-9]{40}\s+#\s+v\d+\s*$/.test(line)) failures.push(`${relative}: floating or unlabeled action ref ${line.trim()}`); + } + for (const pin of immutableActionPins) if (!workflow.includes(pin)) failures.push(`${relative}: missing immutable action pin ${pin}`); +} + function requireRootLinks() { for (const relative of ["README.md", "index.md", "AGENTS.md"]) { requirePattern(relative, /\[[^\]]+\]\(GOVERNANCE\.md\)/, "link target GOVERNANCE.md"); @@ -196,6 +223,7 @@ requireLifecycleStates(); requireOwnership(); requireStalenessDecision(); requireCiwiring(); +requireImmutableActions(); requireRootLinks(); requireEvidenceMap(); requireSentinelProvenanceBoundary(); diff --git a/tests/component-state-evidence.spec.mjs b/tests/component-state-evidence.spec.mjs new file mode 100644 index 0000000..9b3e57c --- /dev/null +++ b/tests/component-state-evidence.spec.mjs @@ -0,0 +1,129 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; +import { readCaptureSession } from "../scripts/capture-session-contract.mjs"; +import { compileSchemas } from "../scripts/component-state-contract.mjs"; +import { componentStateFixture, componentStateProfiles, renderComponentState } from "./helpers/render-component-state.mjs"; + +const mutation = process.env.STATE_MUTATION ?? "none"; +const artifactRoot = process.env.STATE_ARTIFACT_DIR; +const sessionFile = process.env.STATE_SESSION_RECEIPT; +const playwrightVersion = JSON.parse(fs.readFileSync(path.resolve(path.dirname(new URL(import.meta.url).pathname), "../package.json"), "utf8")).devDependencies["@playwright/test"]; +const sessionFailures = []; +const schemas = compileSchemas(path.resolve(path.dirname(new URL(import.meta.url).pathname), "../consumer-reference/schema")); +const captureSession = sessionFile ? readCaptureSession(path.resolve(sessionFile), schemas.capture, sessionFailures) : undefined; +if (artifactRoot && (!captureSession || sessionFailures.length > 0)) throw new Error(`valid STATE_SESSION_RECEIPT is required for capture: ${JSON.stringify(sessionFailures)}`); + +function normalizeBoolean(value) { + return value === true || value === 1 || value === "true" || value === "1"; +} + +async function axObservation(session, fixture, scenario, capturedAt, captureLink) { + const tree = await session.send("Accessibility.getFullAXTree"); + const node = tree.nodes.find((candidate) => candidate.role?.value === scenario.role && candidate.name?.value === scenario.label); + expect(node, `ax_${scenario.id}`).toBeTruthy(); + const raw = Object.fromEntries(node.properties.map((property) => [property.name, property.value.value])); + const properties = {}; + for (const [key, expected] of Object.entries(scenario.expected.ax)) { + if (key !== "role") properties[key] = typeof expected === "boolean" ? normalizeBoolean(raw[key]) : raw[key]; + } + return { capture_session: captureLink, captured_at: capturedAt, channel: "ax", name: node.name.value, profile_id: fixture.profileId, properties, role: node.role.value, scenario_id: scenario.id, schema_version: "1.0", semantic_mode: scenario.semantic_mode }; +} + +async function domObservation(control, fixture, scenario, capturedAt, captureLink) { + const observation = await control.evaluate((element, expected) => { + const attributes = {}; + for (const key of Object.keys(expected.dom)) { + if (key.startsWith("aria-") || key === "disabled") attributes[key] = key === "disabled" ? String(element.disabled) : element.getAttribute(key); + } + return { + activation_count: Number(element.dataset.activations), + activation_key: expected.activationKey, + active: document.activeElement === element, + attributes, + channel: "dom", + profile_id: expected.profileId, + role: element.tagName === "BUTTON" ? "button" : element.getAttribute("role"), + scenario_id: expected.scenarioId, + schema_version: "1.0", + semantic_mode: expected.mode, + visual_states: element.dataset.visual.split(/\s+/).filter(Boolean), + }; + }, { activationKey: scenario.activation_key, dom: scenario.expected.dom, mode: scenario.semantic_mode, profileId: fixture.profileId, scenarioId: scenario.id }); + return { ...observation, capture_session: captureLink, captured_at: capturedAt }; +} + +async function captureEvidence(page, fixture, scenario, session) { + if (!artifactRoot) return; + fs.mkdirSync(artifactRoot, { recursive: true }); + const viewport = page.viewportSize(); + const intendedProfile = captureSession.receipt.intended.find((profile) => profile.profile_id === fixture.profileId && profile.profile_name === fixture.profileName); + const intendedScenario = intendedProfile?.scenarios.find((entry) => entry.id === scenario.id); + expect(intendedScenario?.channels, "capture_session_intent_mismatch").toEqual(["ax", "dom", "visual"]); + expect(captureSession.receipt.environment.browser, "capture_session_browser_mismatch").toContain(page.context().browser().version()); + expect(captureSession.receipt.environment.playwright, "capture_session_playwright_mismatch").toBe(playwrightVersion); + expect(captureSession.receipt.environment.viewport, "capture_session_viewport_mismatch").toBe(`${viewport.width}x${viewport.height}`); + const capturedAt = new Date().toISOString(); + const control = page.locator("#control"); + const stem = `${fixture.profileName}-${scenario.id}`; + const png = await page.locator(".capture").screenshot({ animations: "disabled", caret: "hide" }); + const dom = await domObservation(control, fixture, scenario, capturedAt, captureSession.link); + const ax = await axObservation(session, fixture, scenario, capturedAt, captureSession.link); + fs.writeFileSync(path.join(artifactRoot, `${stem}.png`), png); + fs.writeFileSync(path.join(artifactRoot, `${stem}.dom.json`), `${JSON.stringify(dom, null, 2)}\n`); + fs.writeFileSync(path.join(artifactRoot, `${stem}.ax.json`), `${JSON.stringify(ax, null, 2)}\n`); +} + +for (const profile of componentStateProfiles()) { + const fixture = componentStateFixture(profile); + for (const canonical of fixture.scenarios) { + test(`${profile} ${canonical.id} follows canonical keyboard, visual, DOM, and AX state`, async ({ page }) => { + const scenario = await renderComponentState(page, fixture, canonical.id, mutation); + const control = page.locator("#control"); + const session = await page.context().newCDPSession(page); + await expect(page.locator("[data-scenario-id]"), "state_runtime_unknown_scenario").toHaveCount(1); + await expect(page.locator(".capture"), "state_capture_wrapper").toHaveCSS("padding", "6px"); + + if (scenario.states.includes("disabled")) { + await expect(control, "disabled_surface_mismatch").toBeDisabled(); + await control.click({ force: true }); + } else { + await page.keyboard.press("Tab"); + await expect(control, "focus_surface_mismatch").toBeFocused(); + if (scenario.states.includes("focus")) { + await expect(control, "focus_outline_visible").toHaveCSS("outline-width", "3px"); + await expect(control, "focus_outline_offset_visible").toHaveCSS("outline-offset", "3px"); + } + await page.keyboard.press(scenario.activation_key); + } + + const expectedActivations = scenario.expected.activation === "allowed" ? "1" : "0"; + await expect(control, "state_activation_key_parity").toHaveAttribute("data-activations", expectedActivations); + await expect(control, "visual_surface_parity").toHaveAttribute("data-visual", scenario.expected.visual.join(" ")); + for (const [name, value] of Object.entries(scenario.aria)) await expect(control, `${name}_surface_mismatch`).toHaveAttribute(`aria-${name}`, value); + if (scenario.states.includes("expanded")) { + const panel = page.locator("#controlled-panel"); + await expect(panel, "expanded_surface_mismatch").toBeVisible(); + const clearance = await control.evaluate((element) => { + const controlled = document.getElementById("controlled-panel"); + const style = getComputedStyle(element); + const outlineEdge = element.getBoundingClientRect().bottom + + Number.parseFloat(style.outlineWidth) + + Math.max(0, Number.parseFloat(style.outlineOffset)); + return controlled.getBoundingClientRect().top - outlineEdge; + }); + expect(clearance, "focus_ring_clearance").toBeGreaterThanOrEqual(8); + } + + const dom = await domObservation(control, fixture, scenario); + for (const [name, expected] of Object.entries(scenario.expected.dom)) { + const actual = name === "active" ? String(dom.active) : name === "role" ? dom.role : dom.attributes[name]; + expect(actual, `${name}_surface_mismatch`).toBe(expected); + } + const ax = await axObservation(session, fixture, scenario); + expect(ax.role, "role_surface_mismatch").toBe(scenario.expected.ax.role); + for (const [name, expected] of Object.entries(scenario.expected.ax)) if (name !== "role") expect(ax.properties[name], `${name}_surface_mismatch`).toBe(expected); + await captureEvidence(page, fixture, scenario, session); + }); + } +} diff --git a/tests/helpers/render-component-state.mjs b/tests/helpers/render-component-state.mjs new file mode 100644 index 0000000..2d66586 --- /dev/null +++ b/tests/helpers/render-component-state.mjs @@ -0,0 +1,132 @@ +import fs from "node:fs"; +import path from "node:path"; +import { resolveProfileRecords } from "../../scripts/profile-record-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../.."); +const governedRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function tokenValue(tokens, reference) { + const segments = reference.slice(1, -1).split("."); + let current = tokens; + for (const segment of segments) current = current[segment]; + return current.$value; +} + +function cssColor(value) { + const channels = value.components.map((component) => Math.round(component * 255)); + return `rgb(${channels.join(" ")} / ${value.alpha})`; +} + +function cssDimension(value) { + return `${value.value}${value.unit}`; +} + +function escapeHtml(value) { + return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); +} + +export function componentStateProfiles() { + return fs.readdirSync(governedRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(governedRoot, entry.name, "profile.json"))) + .map((entry) => entry.name) + .sort(); +} + +export function componentStateFixture(profileName) { + const profileRoot = path.join(governedRoot, profileName); + const failures = []; + const resolved = resolveProfileRecords(profileRoot, failures); + if (!resolved || failures.length > 0) throw new Error(`invalid governed profile ${profileName}: ${JSON.stringify(failures)}`); + const foundations = readJson(path.join(profileRoot, resolved.profile.local_foundations)); + const tokens = readJson(path.join(profileRoot, resolved.profile.tokens)); + const component = resolved.records.component[0].value; + const fixture = resolved.records.fixture[0].value; + const states = resolved.records.states[0].value; + const modes = new Map(component.semantic_modes.map((mode) => [mode.id, mode])); + const scenarios = new Map(states.scenarios.map((scenario) => [scenario.id, scenario])); + return { + colors: { + accent: cssColor(tokenValue(tokens, foundations.bindings.accent)), + canvas: cssColor(tokenValue(tokens, foundations.bindings.canvas)), + text: cssColor(tokenValue(tokens, foundations.bindings.text)), + }, + font: foundations.bindings.body_font_family, + profileId: resolved.profile.id, + profileName, + scenarios: fixture.scenarios.map((runtime) => ({ + ...runtime, + aria: scenarios.get(runtime.id).aria, + expected: scenarios.get(runtime.id).expected, + states: scenarios.get(runtime.id).states, + role: modes.get(runtime.semantic_mode).role, + })), + spacing: { + page: cssDimension(tokens.space.page.$value), + rhythm: cssDimension(tokenValue(tokens, foundations.bindings.content_rhythm)), + }, + }; +} + +export async function renderComponentState(page, fixture, scenarioId, mutation = "none") { + const scenario = fixture.scenarios.find((candidate) => candidate.id === scenarioId); + if (!scenario) throw new Error(`unknown canonical scenario ${scenarioId}`); + const isDisabled = scenario.states.includes("disabled") && mutation !== "runtime-disabled-activation"; + const initialVisual = scenario.expected.visual.filter((state) => !["expanded", "pressed"].includes(state)); + const attributes = Object.entries(scenario.aria) + .map(([name, value]) => `aria-${name}="${escapeHtml(name === "expanded" || name === "pressed" ? "false" : value)}"`) + .join(" "); + const panel = scenario.semantic_mode === "disclosure" ? `` : ""; + const unknown = mutation === "runtime-unknown-scenario" ? '' : ""; + const runtime = JSON.stringify({ + activation: scenario.expected.activation, + activationKey: scenario.activation_key, + mode: scenario.semantic_mode, + mutation, + visual: scenario.expected.visual, + }).replaceAll("<", "\\u003c"); + await page.setContent(` +
+
+ + ${panel} +
+ ${unknown} +
+ + `); + return scenario; +} From 7589f6bfa65371c42387fa7fbf70bbd28d1d407c Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 03:47:20 +0900 Subject: [PATCH 08/44] Fix component-state evidence workflow paths --- .github/workflows/validate.yml | 23 +++-- scripts/component-state-workflow-contract.mjs | 66 +++++++++++++ scripts/test-validate-governance.mjs | 97 ++++++++++++++++++- scripts/validate-governance.mjs | 8 ++ 4 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 scripts/component-state-workflow-contract.mjs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6dab732..0bc4a22 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -44,6 +44,7 @@ jobs: node -c scripts/test-validate-ia.mjs node -c scripts/validate-vocabulary.mjs node -c scripts/test-validate-vocabulary.mjs + node -c scripts/component-state-workflow-contract.mjs node -c scripts/validate-governance.mjs node -c scripts/test-validate-governance.mjs node -c scripts/validate-domains.mjs @@ -149,7 +150,6 @@ jobs: run: | node scripts/validate-component-state.mjs --json node scripts/test-validate-component-state.mjs --json - node scripts/test-validate-component-state-artifacts.mjs node scripts/generate-consumer-reference-evidence.mjs --check --json node scripts/test-generate-consumer-reference-evidence.mjs --json @@ -201,6 +201,8 @@ jobs: name: Chromium component-state evidence (nonblocking) runs-on: ubuntu-24.04 continue-on-error: true + env: + STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state container: image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a options: --platform linux/amd64 @@ -216,25 +218,28 @@ jobs: - name: Install exact component-state dependencies run: npm ci --ignore-scripts --no-audit --no-fund + - name: Run component-state artifact and session adversarial harness + run: node scripts/test-validate-component-state-artifacts.mjs + - name: Capture and validate closed component-state evidence shell: bash env: - STATE_ARTIFACT_DIR: ${{ runner.temp }}/consumer-reference-state/runtime SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a run: | set -o pipefail node scripts/create-component-state-session.mjs \ - --output "${{ runner.temp }}/consumer-reference-state/capture-session.json" \ + --output "$STATE_EVIDENCE_ROOT/capture-session.json" \ --json - STATE_SESSION_RECEIPT="${{ runner.temp }}/consumer-reference-state/capture-session.json" \ + STATE_ARTIFACT_DIR="$STATE_EVIDENCE_ROOT/runtime" \ + STATE_SESSION_RECEIPT="$STATE_EVIDENCE_ROOT/capture-session.json" \ npm run test:component-state:runtime -- --reporter=line node scripts/finalize-component-state-evidence.mjs \ - --artifact-root "${{ runner.temp }}/consumer-reference-state" \ - --output "${{ runner.temp }}/consumer-reference-state/runtime-manifest.json" \ + --artifact-root "$STATE_EVIDENCE_ROOT" \ + --output "$STATE_EVIDENCE_ROOT/runtime-manifest.json" \ --json node scripts/validate-component-state.mjs \ - --artifact-root "${{ runner.temp }}/consumer-reference-state" \ - --runtime-manifest "${{ runner.temp }}/consumer-reference-state/runtime-manifest.json" \ + --artifact-root "$STATE_EVIDENCE_ROOT" \ + --runtime-manifest "$STATE_EVIDENCE_ROOT/runtime-manifest.json" \ --json - name: Run component-state runtime negative controls @@ -245,7 +250,7 @@ jobs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: consumer-reference-state-${{ github.run_id }}-${{ github.sha }} - path: ${{ runner.temp }}/consumer-reference-state/ + path: ${{ env.STATE_EVIDENCE_ROOT }}/ if-no-files-found: error retention-days: 14 diff --git a/scripts/component-state-workflow-contract.mjs b/scripts/component-state-workflow-contract.mjs new file mode 100644 index 0000000..d9dfc96 --- /dev/null +++ b/scripts/component-state-workflow-contract.mjs @@ -0,0 +1,66 @@ +const artifactHarness = "node scripts/test-validate-component-state-artifacts.mjs"; +const pinnedContainer = "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a"; +const sharedRoot = "$STATE_EVIDENCE_ROOT"; + +function jobSection(workflow, jobName) { + const marker = ` ${jobName}:\n`; + const start = workflow.indexOf(marker); + if (start === -1) return ""; + const remainder = workflow.slice(start + marker.length); + const nextJob = remainder.search(/^ [a-z0-9][a-z0-9-]*:\s*$/m); + return nextJob === -1 ? remainder : remainder.slice(0, nextJob); +} + +function normalized(line) { + return line.trim().replace(/\s*\\$/, ""); +} + +function commandCount(section, command) { + return section.split("\n").filter((line) => normalized(line).replace(/^(?:-\s+)?run:\s+/, "") === command).length; +} + +function invocation(section, command) { + const lines = section.split("\n"); + const starts = lines.flatMap((line, index) => normalized(line) === command ? [index] : []); + if (starts.length !== 1) return []; + const start = starts[0]; + const indentation = lines[start].search(/\S/); + const block = [command]; + for (let index = start + 1; index < lines.length; index += 1) { + if (lines[index].trim() && lines[index].search(/\S/) <= indentation) break; + if (lines[index].trim()) block.push(normalized(lines[index])); + } + return block; +} + +function hasAdjacentLines(section, first, second) { + const lines = section.split("\n"); + return lines.some((line, index) => line === first && lines[index + 1] === second); +} + +export function componentStateWorkflowFailures(workflow) { + const failures = []; + const validateJob = jobSection(workflow, "validate"); + const componentJob = jobSection(workflow, "component-state-evidence"); + const totalHarnesses = commandCount(workflow, artifactHarness); + const componentHarnesses = commandCount(componentJob, artifactHarness); + if (commandCount(validateJob, artifactHarness) > 0) failures.push("browser-dependent artifact/session harness must not run in validate job"); + if (componentHarnesses === 0) failures.push("component-state artifact/session harness must run in Playwright container job"); + if (totalHarnesses !== 1 || componentHarnesses !== 1) failures.push("artifact/session harness must run exactly once and only in component-state-evidence job"); + if (!hasAdjacentLines(componentJob, " container:", ` image: ${pinnedContainer}`)) failures.push("component-state container.image must equal pinned Playwright digest"); + if (/runner\.temp|RUNNER_TEMP/.test(componentJob)) failures.push("component-state Playwright container job must not use runner temp paths"); + if (!hasAdjacentLines(componentJob, " env:", " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state")) failures.push("missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state"); + + const runtime = invocation(componentJob, `STATE_ARTIFACT_DIR="${sharedRoot}/runtime"`); + if (!runtime.includes(`STATE_SESSION_RECEIPT="${sharedRoot}/capture-session.json"`)) failures.push("component-state runtime must bind receipt under shared root"); + const finalizer = invocation(componentJob, "node scripts/finalize-component-state-evidence.mjs"); + if (!finalizer.includes(`--artifact-root "${sharedRoot}"`)) failures.push("component-state finalizer must use shared artifact root"); + if (!finalizer.includes(`--output "${sharedRoot}/runtime-manifest.json"`)) failures.push("component-state finalizer must write manifest under shared root"); + const validator = invocation(componentJob, "node scripts/validate-component-state.mjs"); + if (!validator.includes(`--artifact-root "${sharedRoot}"`)) failures.push("component-state validator must use shared artifact root"); + if (!validator.includes(`--runtime-manifest "${sharedRoot}/runtime-manifest.json"`)) failures.push("component-state validator must read manifest under shared root"); + const creator = invocation(componentJob, "node scripts/create-component-state-session.mjs"); + if (!creator.includes(`--output "${sharedRoot}/capture-session.json"`)) failures.push("missing shared component-state workspace path --output \"$STATE_EVIDENCE_ROOT/capture-session.json\""); + if (!componentJob.split("\n").includes(" path: ${{ env.STATE_EVIDENCE_ROOT }}/")) failures.push("missing shared component-state workspace path path: ${{ env.STATE_EVIDENCE_ROOT }}/"); + return failures; +} diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index b972587..d20d7fa 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -79,9 +79,12 @@ const files = { "", ].join("\n"), ".github/workflows/validate.yml": [ + "jobs:", + " validate:", "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", + "node -c scripts/component-state-workflow-contract.mjs", "node -c scripts/validate-governance.mjs", "node -c scripts/test-validate-governance.mjs", "node scripts/validate-governance.mjs --json", @@ -97,10 +100,39 @@ const files = { "node scripts/validate-consumer-reference.mjs --json", "node scripts/test-validate-consumer-reference.mjs --json", "node scripts/test-consumer-reference-sentinel.mjs", - "node scripts/create-component-state-session.mjs", - "STATE_SESSION_RECEIPT=receipt.json", - "node scripts/finalize-component-state-evidence.mjs", - "--runtime-manifest manifest.json", + " component-state-evidence:", + " env:", + " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", + " container:", + " image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + " steps:", + " - name: Run artifact harness", + " run: node scripts/test-validate-component-state-artifacts.mjs", + " - name: Capture evidence", + " env:", + " SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + " run: |", + " node scripts/create-component-state-session.mjs \\", + " --output \"$STATE_EVIDENCE_ROOT/capture-session.json\" \\", + " --json", + " STATE_ARTIFACT_DIR=\"$STATE_EVIDENCE_ROOT/runtime\" \\", + " STATE_SESSION_RECEIPT=\"$STATE_EVIDENCE_ROOT/capture-session.json\" \\", + " npm run test:component-state:runtime -- --reporter=line", + " node scripts/finalize-component-state-evidence.mjs \\", + " --artifact-root \"$STATE_EVIDENCE_ROOT\" \\", + " --output \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\", + " --json", + " node scripts/validate-component-state.mjs \\", + " --artifact-root \"$STATE_EVIDENCE_ROOT\" \\", + " --runtime-manifest \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\", + " --json", + " - name: Upload evidence", + " with:", + " path: ${{ env.STATE_EVIDENCE_ROOT }}/", + " chromium-sentinel:", + " steps:", + " - run: node scripts/test-consumer-reference-sentinel.mjs", + " chromium-calibration:", "node scripts/validate-baseline-manifest.mjs --json", "node scripts/test-validate-baseline-manifest.mjs --json", "node scripts/test-summarize-sentinel-calibration.mjs", @@ -237,7 +269,62 @@ const cases = [ { name: "missing_domain_family", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("| Motion domain guidance |", "| Motion reference notes |") }, expect: "GOVERNANCE.md: missing Motion domain guidance" }, { name: "missing_domain_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/validate-domains.mjs --json\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/validate-domains.mjs --json" }, { name: "missing_domain_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail.", "Domain fixtures must fail.") }, expect: "quality/evidence/executable-evidence.md: missing Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail." }, - { name: "missing_sentinel_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/test-consumer-reference-sentinel.mjs\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-consumer-reference-sentinel.mjs" }, + { name: "missing_sentinel_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replaceAll("node scripts/test-consumer-reference-sentinel.mjs", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-consumer-reference-sentinel.mjs" }, + { + name: "browser_artifact_harness_in_static_job", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" component-state-evidence:\n", "node scripts/test-validate-component-state-artifacts.mjs\n component-state-evidence:\n") }, + expect: ".github/workflows/validate.yml: browser-dependent artifact/session harness must not run in validate job", + }, + { + name: "browser_artifact_harness_missing_container_job", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" run: node scripts/test-validate-component-state-artifacts.mjs\n", "") }, + expect: ".github/workflows/validate.yml: component-state artifact/session harness must run in Playwright container job", + }, + { + name: "duplicate_artifact_harness_in_chromium_sentinel", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: node scripts/test-consumer-reference-sentinel.mjs\n", " - run: node scripts/test-consumer-reference-sentinel.mjs\n - run: node scripts/test-validate-component-state-artifacts.mjs\n") }, + expect: ".github/workflows/validate.yml: artifact/session harness must run exactly once and only in component-state-evidence job", + }, + { + name: "unpinned_component_container_with_pinned_env", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", " image: mcr.microsoft.com/playwright:v1.61.0-noble") }, + expect: ".github/workflows/validate.yml: component-state container.image must equal pinned Playwright digest", + }, + { + name: "session_receipt_outside_shared_root", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" STATE_SESSION_RECEIPT=\"$STATE_EVIDENCE_ROOT/capture-session.json\" \\\n", " STATE_SESSION_RECEIPT=\"/tmp/capture-session.json\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state runtime must bind receipt under shared root", + }, + { + name: "finalizer_output_outside_shared_root", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" --output \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --output \"/tmp/runtime-manifest.json\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state finalizer must write manifest under shared root", + }, + { + name: "validator_manifest_outside_shared_root", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" --runtime-manifest \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --runtime-manifest \"/tmp/runtime-manifest.json\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state validator must read manifest under shared root", + }, + { + name: "finalizer_artifact_root_outside_shared_root", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state finalizer must use shared artifact root", + }, + { + name: "validator_artifact_root_outside_shared_root", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" node scripts/validate-component-state.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/validate-component-state.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state validator must use shared artifact root", + }, + { + name: "runner_temp_in_component_job", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", "STATE_EVIDENCE_ROOT: ${{ runner.temp }}/consumer-reference-state") }, + expect: ".github/workflows/validate.yml: component-state Playwright container job must not use runner temp paths", + }, + { + name: "component_state_workspace_root_drift", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replaceAll(".tmp/consumer-reference-state", "state-output") }, + expect: ".github/workflows/validate.yml: missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", + }, { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, { name: "floating_action_ref", diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 2c23cd6..18309c5 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; +import { componentStateWorkflowFailures } from "./component-state-workflow-contract.mjs"; const args = new Set(process.argv.slice(2)); const json = args.has("--json"); @@ -142,6 +143,7 @@ function requireStalenessDecision() { } function requireCiwiring() { + requireIncludes(".github/workflows/validate.yml", "node -c scripts/component-state-workflow-contract.mjs"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/validate-governance.mjs"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/test-validate-governance.mjs"); requireIncludes(".github/workflows/validate.yml", "node scripts/validate-governance.mjs --json"); @@ -177,6 +179,11 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "contents: read"); } +function requireComponentStateCiIsolation() { + const relative = ".github/workflows/validate.yml"; + for (const failure of componentStateWorkflowFailures(read(relative))) failures.push(`${relative}: ${failure}`); +} + function requireImmutableActions() { const relative = ".github/workflows/validate.yml"; const workflow = read(relative); @@ -223,6 +230,7 @@ requireLifecycleStates(); requireOwnership(); requireStalenessDecision(); requireCiwiring(); +requireComponentStateCiIsolation(); requireImmutableActions(); requireRootLinks(); requireEvidenceMap(); From 9965afad8e526646267be894b3164b99be4efbf9 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 05:24:23 +0900 Subject: [PATCH 09/44] Define shared reference promotion policy --- .github/CODEOWNERS | 3 + .github/workflows/validate.yml | 16 ++ DOMAINS.md | 6 + GOVERNANCE.md | 19 ++ consumer-reference/contract.md | 10 + .../promotion/invalid-blocking-evidence.json | 26 ++ .../promotion/invalid-count-only.json | 29 ++ .../invalid-missing-owner-migration.json | 25 ++ ...nvalid-normative-bypass-no-regression.json | 25 ++ .../invalid-related-counted-independent.json | 45 ++++ .../invalid-stable-ended-support.json | 26 ++ .../fixtures/promotion/manifest.json | 15 ++ .../promotion/valid-deferred-example.json | 67 +++++ .../promotion/valid-normative-bypass.json | 67 +++++ consumer-reference/index.md | 2 + .../policies/shared-experimental.json | 37 +++ .../schema/promotion-rfc.schema.json | 219 +++++++++++++++ quality/evidence/executable-evidence.md | 1 + scripts/governance-test-fixture.mjs | 221 ++++++++++++++++ scripts/governed-profile-registry.mjs | 125 +++++++++ scripts/promotion-attestation-contract.mjs | 91 +++++++ scripts/promotion-boundary-test-contract.mjs | 92 +++++++ scripts/promotion-fixture-inventory.mjs | 109 ++++++++ scripts/promotion-governance-contract.mjs | 28 ++ scripts/reference-profile-contract.mjs | 3 +- scripts/test-validate-domains.mjs | 14 +- scripts/test-validate-governance.mjs | 204 +------------- scripts/test-validate-promotion-rfc.mjs | 250 ++++++++++++++++++ scripts/validate-consumer-reference.mjs | 6 +- scripts/validate-domains.mjs | 17 ++ scripts/validate-governance.mjs | 14 + scripts/validate-promotion-rfc.mjs | 224 ++++++++++++++++ 32 files changed, 1832 insertions(+), 204 deletions(-) create mode 100644 consumer-reference/fixtures/promotion/invalid-blocking-evidence.json create mode 100644 consumer-reference/fixtures/promotion/invalid-count-only.json create mode 100644 consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json create mode 100644 consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json create mode 100644 consumer-reference/fixtures/promotion/invalid-related-counted-independent.json create mode 100644 consumer-reference/fixtures/promotion/invalid-stable-ended-support.json create mode 100644 consumer-reference/fixtures/promotion/manifest.json create mode 100644 consumer-reference/fixtures/promotion/valid-deferred-example.json create mode 100644 consumer-reference/fixtures/promotion/valid-normative-bypass.json create mode 100644 consumer-reference/policies/shared-experimental.json create mode 100644 consumer-reference/schema/promotion-rfc.schema.json create mode 100644 scripts/governance-test-fixture.mjs create mode 100644 scripts/governed-profile-registry.mjs create mode 100644 scripts/promotion-attestation-contract.mjs create mode 100644 scripts/promotion-boundary-test-contract.mjs create mode 100644 scripts/promotion-fixture-inventory.mjs create mode 100644 scripts/promotion-governance-contract.mjs create mode 100644 scripts/test-validate-promotion-rfc.mjs create mode 100644 scripts/validate-promotion-rfc.mjs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d398067..a639616 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,6 +17,9 @@ /consumer-reference/adapters/ @changeroa /consumer-reference/generated/ @changeroa /consumer-reference/baselines/ @changeroa +/consumer-reference/policies/ @changeroa +/consumer-reference/fixtures/promotion/ @changeroa +/consumer-reference/schema/promotion-rfc.schema.json @changeroa /tests/ @changeroa /playwright.config.mjs @changeroa diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0bc4a22..748f44e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -45,6 +45,12 @@ jobs: node -c scripts/validate-vocabulary.mjs node -c scripts/test-validate-vocabulary.mjs node -c scripts/component-state-workflow-contract.mjs + node -c scripts/promotion-governance-contract.mjs + node -c scripts/promotion-fixture-inventory.mjs + node -c scripts/promotion-boundary-test-contract.mjs + node -c scripts/governed-profile-registry.mjs + node -c scripts/promotion-attestation-contract.mjs + node -c scripts/governance-test-fixture.mjs node -c scripts/validate-governance.mjs node -c scripts/test-validate-governance.mjs node -c scripts/validate-domains.mjs @@ -53,6 +59,8 @@ jobs: node -c scripts/test-validate-webpage-workflow.mjs node -c scripts/validate-consumer-reference.mjs node -c scripts/test-validate-consumer-reference.mjs + node -c scripts/validate-promotion-rfc.mjs + node -c scripts/test-validate-promotion-rfc.mjs node -c consumer-reference/adapters/style-dictionary.config.mjs node -c scripts/reference-token-contract.mjs node -c scripts/build-reference-artifacts.mjs @@ -95,6 +103,9 @@ jobs: node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/fixture-manifest.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/evidence-record.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/runtime-evidence-manifest.schema.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/promotion-rfc.schema.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/policies/shared-experimental.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/fixtures/promotion/manifest.json"))' git diff --check - name: Generated artifacts are current @@ -141,6 +152,11 @@ jobs: node scripts/validate-consumer-reference.mjs --json node scripts/test-validate-consumer-reference.mjs --json + - name: Shared promotion policy contract + run: | + node scripts/validate-promotion-rfc.mjs --json + node scripts/test-validate-promotion-rfc.mjs --json + - name: Portable token artifact integrity run: | node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json diff --git a/DOMAINS.md b/DOMAINS.md index 6e7c392..79b7312 100644 --- a/DOMAINS.md +++ b/DOMAINS.md @@ -47,6 +47,12 @@ All five new domain leaves are independent method rewrites inspired by [emilkowa External adaptations begin `experimental`. Promotion to `stable` requires a demonstrated reader or review task, evidence that the document improves that task, a named review owner, and no unresolved provenance or platform-version debt. Remove or revise a page when its source revision changes materially, a platform claim becomes stale, a local quality gate contradicts it, or a reader test shows the route is misleading. +### Consumer Reference Promotion + +Consumer-reference promotion does not add a domain. The StyleGallery-local `>=2` independent-consumer gateway applies only to consumer-local → shared-experimental invariant eligibility. Editorial and terminal are related examples in one fixture set, so neither counts as an independent consumer. Shared stable has no numeric adoption threshold. + +Normative correctness may waive adoption count only; regression evidence, invariant scope, owner and support capacity, compatibility, migration, deprecation, rollback, and provenance duties remain required. A failed stable contract is restricted, maintained, rolled back, or deprecated with migration, never silently relabeled experimental. Promotion records are JSON-only; the committed examples are synthetic, `example_only`, deferred, and contain zero adopter attestations. + Consumer-reference maturity is governed independently from artifact mode. Stable records cannot have ended support, and related repository fixtures are not independent adoption evidence. ## IA Navigation diff --git a/GOVERNANCE.md b/GOVERNANCE.md index ff046cd..46bee28 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -25,6 +25,7 @@ Use this file before editing repository documentation. It names which file is au | Portable token source | `consumer-reference/fixtures/token-portability/valid-reference.json`, `consumer-reference/schema/portable-tokens.schema.json` | `scripts/build-reference-artifacts.mjs` with Style Dictionary `5.5.0` | `consumer-reference/generated/tokens.css`, `consumer-reference/generated/manifest.json` | `stable` restricted adapter contract, `generated` output | Allowed token shape, adapter/version pin, source token count, warning, declaration, or content hash changes. | `scripts/validate-reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | | Governed local reference profiles | `design-engineering/reference-profiles/governed-local/**` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Layout revision, identity values, UA/reset assumptions, explicit selection, or fixture relationship changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Design Engineering owner with Validation owner | | Component-state evidence matrices | Each profile's declared `components/*.component.json`, `states/*.states.json`, `fixtures/*.fixture.json`, and `evidence/*.evidence.json` records | `scripts/generate-consumer-reference-evidence.mjs` | `design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md` | `generated` output from `experimental` canonical records | Declared record paths, capture-session identity, scenario/mode/channel counts, claim boundary, generated escaping, or generator output changes. | `scripts/validate-component-state.mjs`, `scripts/test-validate-component-state.mjs`, `scripts/test-validate-component-state-artifacts.mjs`, `scripts/test-generate-consumer-reference-evidence.mjs` | Design Engineering owner with Validation owner | +| Shared promotion policy | `consumer-reference/policies/shared-experimental.json`, `consumer-reference/schema/promotion-rfc.schema.json` | Manual | None; promotion fixtures are synthetic examples | `stable` policy with `example_only`, deferred fixtures | Gateway scope, independence basis, evidence, ownership, support, compatibility, migration, deprecation, rollback, provenance, or decision changes. | `scripts/validate-promotion-rfc.mjs`, `scripts/test-validate-promotion-rfc.mjs` | Repository governance owner with actual consumer, support, migration, rollback, and Validation owners | | Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/{baseline-manifest,calibration-record}.schema.json`, `consumer-reference/baselines/*.json` | Playwright `1.61.0` in the digest-pinned `linux/amd64` image | `tests/snapshots/consumer-reference-card-grid.png`, raw GitHub Actions calibration artifact | `experimental`, nonblocking while owner approval is pending | Renderer source, exact image/platform/tool pin, baseline bytes, computed layout contract, or calibration metadata changes. | `scripts/validate-baseline-manifest.mjs`, `scripts/test-validate-baseline-manifest.mjs`, `scripts/test-summarize-sentinel-calibration.mjs`, Playwright | Repository governance owner with Validation owner | | Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner | | Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner | @@ -100,6 +101,7 @@ Consumer-reference ownership records the current truth as `owner.enforcement: "p | `guides/**`, `GUIDE.md`, `recipes/**` | Planning-doc owner | Planning flow, task routes, recipe composition boundaries. | | `quality/**` | Quality owner | Claim boundaries, executable evidence, review gates. | | `consumer-reference/**` | Repository governance owner with Validation owner | Required handoff, repository-local record safety, lifecycle separation, ownership truth, and reverse-dependency guard. | +| `consumer-reference/policies/**`, `consumer-reference/fixtures/promotion/**`, `consumer-reference/schema/promotion-rfc.schema.json`, `scripts/*promotion-rfc.mjs` | Repository governance owner with actual consumer, support, migration, rollback, and Validation owners | Invariant-only scope, independence truth, claim-scoped evidence, compatibility, support capacity, lifecycle duties, and zero promotion/adoption overclaim. | | `consumer-reference/adapters/**`, `consumer-reference/generated/**`, `scripts/*reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | Restricted token ingress, exact adapter pin, warning handling, token/declaration counts, hashes, preserved aliases, and generated drift. | | `DOMAINS.md`, `layout/**` | Repository governance owner with Pattern-data owner | Domain routing and preservation of the stable Layout path contract. | | `motion/**` | Motion domain owner | Motion terminology, review procedure, practice classification, and evidence boundaries. | @@ -109,6 +111,16 @@ Consumer-reference ownership records the current truth as `owner.enforcement: "p | `platform-guides/**` | Platform Guides domain owner | Platform/source/version limits, comparison boundaries, and stale review. | | `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs`, `.github/workflows/validate.yml` | Validation owner | Validator scope, negative fixtures, CI parity. | +## Shared Promotion Governance + +The StyleGallery-local `>=2` independent-consumer gateway applies only to consumer-local → shared-experimental invariant eligibility. It is neither a universal rule nor a stable threshold. Shared stable has no numeric adoption threshold; it requires claim-scoped evidence, active owner and support capacity, compatibility review, known-issue and exception visibility, migration and deprecation readiness, rollback triggers, resolved provenance debt, and an explicit scope decision. + +Normative accessibility, security, standards, or correctness fixes may waive adoption count only. They never waive regression proof, scope control, owner acceptance, support capacity, compatibility, migration, deprecation, or rollback. Palette, typography, imagery, motion character, and component skin remain consumer-local even when repeated. + +The committed RFC fixtures are synthetic `example_only` proposals with `decision: "deferred"`; they contain zero durable adopter attestations and record no promotion. Editorial and terminal remain related fixtures in one set and never count as independent consumers. Current review truth remains `review_independence: "single_account"`; booleans and multiple role labels do not create independent review. + +If evidence fails, restrict, maintain, or roll back the candidate. A consumed stable contract may be deprecated only with a named migration; it must never be silently relabeled shared-experimental. The canonical JSON policy and RFC records own these decisions; no YAML promotion record is valid. + ## Staleness Control Decision: no scheduled stale-content workflow yet. @@ -181,6 +193,13 @@ node scripts/validate-component-state.mjs \ npm run test:component-state:runtime-negative ``` +For shared promotion governance, also run: + +```sh +node scripts/validate-promotion-rfc.mjs --json +node scripts/test-validate-promotion-rfc.mjs --json +``` + Do not pass `--update-snapshots` in CI. Calibration runs exactly 20 times on the manifest's digest-pinned `linux/amd64` container and uploads raw Playwright JSON, strict exit records, PNG, DOM, AX, metadata, and post-assertion comparison evidence. Failed or incomplete calibration still uploads its truthful raw evidence without writing a completed aggregate. It remains nonblocking until the named owner explicitly approves the baseline. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. diff --git a/consumer-reference/contract.md b/consumer-reference/contract.md index 9b6eb75..99ad61a 100644 --- a/consumer-reference/contract.md +++ b/consumer-reference/contract.md @@ -48,6 +48,16 @@ Consumer-reference infrastructure is owned by repository governance and validati Dependency flows from a consumer or profile to Layout. `layout/**`, `patterns/**`, `scripts/pattern-data.mjs`, and `CATALOG.md` must not import consumer-reference records, profile data, tokens, decorative values, or themes. +## Promotion Boundary + +`consumer-reference/policies/shared-experimental.json` is the canonical StyleGallery-local promotion policy. Its `>=2` independent-consumer gateway applies only to consumer-local → shared-experimental invariant eligibility. It does not promote visual identity and it does not define a stable threshold. Shared stable has no numeric adoption threshold. + +An RFC must record invariant scope, claim-scoped and regression evidence, canonical independence basis, actual owner and support capacity, compatibility, known issues and exceptions, migration and deprecation readiness, rollback triggers, provenance debt, and an explicit deferred or rejected decision. Normative accessibility, security, standards, or correctness fixes may waive adoption count only; every other duty remains binding. + +The tracked examples are synthetic `example_only: true`, `decision: "deferred"`, and have zero adopter attestations. They are not accepted or promoted RFCs. Editorial and terminal share one related fixture set and cannot be counted as independent. Review truth is the enum `review_independence: "single_account"`; boolean aliases are invalid. + +Promotion data is JSON-only. Palette, typography, imagery, motion character, and component skin remain consumer-local. A stable contract whose evidence later fails must be restricted, maintained, rolled back, or deprecated with migration; it cannot be silently relabeled experimental. + The Chromium reference sentinel is an evidence probe, not a Layout theme or product conformance claim. Its proposed baseline is compared in the immutable `linux/amd64` Playwright image, CI never updates snapshots, and the ordinary sentinel remains nonblocking while `baseline_owner_approval` is `pending`. Twenty identical committed-workflow repeats calibrate repeatability; they do not substitute for explicit `@changeroa` baseline approval. Each aggregate run is derived from a zero exit record, the exact passing Playwright test identity, hash-verified PNG/DOM/AX bytes, and comparator proof written only after the zero-diff assertion passes; producer metadata cannot claim completion or a diff. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. diff --git a/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json b/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json new file mode 100644 index 0000000..5d040ac --- /dev/null +++ b/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json @@ -0,0 +1,26 @@ +{ + "attestations": [], + "compatibility": { "breaking": false, "surface": "Synthetic invariant contract surface." }, + "decision": "deferred", + "deprecation": { "plan": "No deprecation while deferred.", "required": false }, + "eligibility_basis": "full_contract", + "evidence": [ + { "id": "blocking", "kind": "blocking", "reference": "synthetic-fixture:blocking", "status": "failed" }, + { "id": "regression", "kind": "regression", "reference": "synthetic-fixture:regression", "status": "passed" } + ], + "example_only": true, + "exceptions": [], + "id": "invalid-blocking-evidence", + "known_issues": ["The blocking evidence failed."], + "migration": { "plan": "No migration while deferred.", "required": false }, + "normative_bypass": "none", + "owner": { "enforcement": "placeholder", "migration": "Synthetic migration owner", "rollback": "Synthetic rollback owner", "scope": "Repository governance owner", "support": "Synthetic support owner" }, + "provenance": { "debt_resolved": false, "source": "Synthetic fixture only" }, + "review_independence": "single_account", + "rollback": { "action": "Keep deferred.", "triggers": ["Blocking evidence fails."] }, + "schema_version": "1.0", + "scope": { "aesthetic": false, "kind": "invariant", "summary": "Incorrectly proceed while blocking evidence is failed." }, + "stable_scope_decision": false, + "support": { "capacity": "committed", "status": "active" }, + "transition": { "from": "consumer_local", "to": "shared_experimental" } +} diff --git a/consumer-reference/fixtures/promotion/invalid-count-only.json b/consumer-reference/fixtures/promotion/invalid-count-only.json new file mode 100644 index 0000000..66e339e --- /dev/null +++ b/consumer-reference/fixtures/promotion/invalid-count-only.json @@ -0,0 +1,29 @@ +{ + "attestations": [ + { "attested_by": "Synthetic owner A", "context_id": "context-a", "consumer_id": "consumer-a", "count_toward_gate": true, "organization_id": "organization-a", "relationship": "independent" }, + { "attested_by": "Synthetic owner B", "context_id": "context-b", "consumer_id": "consumer-b", "count_toward_gate": true, "organization_id": "organization-b", "relationship": "independent" } + ], + "compatibility": { "breaking": false, "surface": "Synthetic invariant contract surface." }, + "decision": "deferred", + "deprecation": { "plan": "No deprecation while deferred.", "required": false }, + "eligibility_basis": "consumer_count_only", + "evidence": [ + { "id": "claim", "kind": "claim_scoped", "reference": "synthetic-fixture:claim", "status": "passed" }, + { "id": "regression", "kind": "regression", "reference": "synthetic-fixture:regression", "status": "passed" } + ], + "example_only": true, + "exceptions": [], + "id": "invalid-count-only", + "known_issues": [], + "migration": { "plan": "No migration while deferred.", "required": false }, + "normative_bypass": "none", + "owner": { "enforcement": "placeholder", "migration": "Synthetic migration owner", "rollback": "Synthetic rollback owner", "scope": "Repository governance owner", "support": "Synthetic support owner" }, + "provenance": { "debt_resolved": false, "source": "Synthetic fixture only" }, + "review_independence": "single_account", + "rollback": { "action": "Keep deferred.", "triggers": ["Operational duties are incomplete."] }, + "schema_version": "1.0", + "scope": { "aesthetic": false, "kind": "invariant", "summary": "Incorrectly use adoption count as the whole promotion decision." }, + "stable_scope_decision": false, + "support": { "capacity": "committed", "status": "active" }, + "transition": { "from": "consumer_local", "to": "shared_experimental" } +} diff --git a/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json b/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json new file mode 100644 index 0000000..b61c5a5 --- /dev/null +++ b/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json @@ -0,0 +1,25 @@ +{ + "attestations": [], + "compatibility": { "breaking": true, "surface": "Synthetic breaking contract surface." }, + "decision": "deferred", + "deprecation": { "plan": "A migration is required before deprecation.", "required": true }, + "eligibility_basis": "full_contract", + "evidence": [ + { "id": "claim", "kind": "claim_scoped", "reference": "synthetic-fixture:claim", "status": "passed" }, + { "id": "regression", "kind": "regression", "reference": "synthetic-fixture:regression", "status": "passed" } + ], + "example_only": true, + "exceptions": [], + "id": "invalid-missing-owner-migration", + "known_issues": [], + "normative_bypass": "none", + "owner": { "enforcement": "placeholder", "rollback": "Synthetic rollback owner", "scope": "Repository governance owner", "support": "Synthetic support owner" }, + "provenance": { "debt_resolved": false, "source": "Synthetic fixture only" }, + "review_independence": "single_account", + "rollback": { "action": "Keep deferred.", "triggers": ["Migration duty is unowned."] }, + "schema_version": "1.0", + "scope": { "aesthetic": false, "kind": "invariant", "summary": "Incorrectly omit migration plan and migration ownership." }, + "stable_scope_decision": false, + "support": { "capacity": "committed", "status": "active" }, + "transition": { "from": "consumer_local", "to": "shared_experimental" } +} diff --git a/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json b/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json new file mode 100644 index 0000000..d164722 --- /dev/null +++ b/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json @@ -0,0 +1,25 @@ +{ + "attestations": [], + "compatibility": { "breaking": false, "surface": "Synthetic correctness contract surface." }, + "decision": "deferred", + "deprecation": { "plan": "No deprecation while deferred.", "required": false }, + "eligibility_basis": "full_contract", + "evidence": [ + { "id": "claim", "kind": "claim_scoped", "reference": "synthetic-fixture:claim", "status": "passed" } + ], + "example_only": true, + "exceptions": [], + "id": "invalid-normative-bypass-no-regression", + "known_issues": [], + "migration": { "plan": "Apply only after compatibility review.", "required": false }, + "normative_bypass": "normative_correctness", + "owner": { "enforcement": "placeholder", "migration": "Synthetic migration owner", "rollback": "Synthetic rollback owner", "scope": "Repository governance owner", "support": "Synthetic support owner" }, + "provenance": { "debt_resolved": false, "source": "Synthetic fixture only" }, + "review_independence": "single_account", + "rollback": { "action": "Keep deferred.", "triggers": ["Regression evidence is absent or fails."] }, + "schema_version": "1.0", + "scope": { "aesthetic": false, "kind": "normative_correctness", "summary": "Incorrectly bypass adoption count without regression proof." }, + "stable_scope_decision": false, + "support": { "capacity": "committed", "status": "active" }, + "transition": { "from": "consumer_local", "to": "shared_experimental" } +} diff --git a/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json b/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json new file mode 100644 index 0000000..125426f --- /dev/null +++ b/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json @@ -0,0 +1,45 @@ +{ + "attestations": [ + { + "attested_by": "Synthetic owner", + "context_id": "editorial", + "consumer_id": "editorial-reference-profile", + "count_toward_gate": true, + "organization_id": "stylegallery", + "profile_record": "design-engineering/reference-profiles/governed-local/editorial/profile.json", + "relationship": "independent" + }, + { + "attested_by": "Synthetic owner", + "context_id": "terminal", + "consumer_id": "terminal-reference-profile", + "count_toward_gate": true, + "organization_id": "stylegallery", + "profile_record": "design-engineering/reference-profiles/governed-local/terminal/profile.json", + "relationship": "independent" + } + ], + "compatibility": { "breaking": false, "surface": "Synthetic invariant contract surface." }, + "decision": "deferred", + "deprecation": { "plan": "No deprecation while deferred.", "required": false }, + "eligibility_basis": "full_contract", + "evidence": [ + { "id": "claim", "kind": "claim_scoped", "reference": "synthetic-fixture:claim", "status": "passed" }, + { "id": "regression", "kind": "regression", "reference": "synthetic-fixture:regression", "status": "passed" } + ], + "example_only": true, + "exceptions": [], + "id": "invalid-related-counted-independent", + "known_issues": [], + "migration": { "plan": "No migration while deferred.", "required": false }, + "normative_bypass": "none", + "owner": { "enforcement": "placeholder", "migration": "Synthetic migration owner", "rollback": "Synthetic rollback owner", "scope": "Repository governance owner", "support": "Synthetic support owner" }, + "provenance": { "debt_resolved": false, "source": "Synthetic fixture only" }, + "review_independence": "single_account", + "rollback": { "action": "Keep deferred.", "triggers": ["Independence is not proven."] }, + "schema_version": "1.0", + "scope": { "aesthetic": false, "kind": "invariant", "summary": "Incorrectly count related local profiles as independent consumers." }, + "stable_scope_decision": false, + "support": { "capacity": "committed", "status": "active" }, + "transition": { "from": "consumer_local", "to": "shared_experimental" } +} diff --git a/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json b/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json new file mode 100644 index 0000000..d936718 --- /dev/null +++ b/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json @@ -0,0 +1,26 @@ +{ + "attestations": [], + "compatibility": { "breaking": false, "surface": "Synthetic stable contract surface." }, + "decision": "deferred", + "deprecation": { "plan": "A consumed stable contract requires a named migration before deprecation.", "required": true }, + "eligibility_basis": "full_contract", + "evidence": [ + { "id": "claim", "kind": "claim_scoped", "reference": "synthetic-fixture:claim", "status": "passed" }, + { "id": "regression", "kind": "regression", "reference": "synthetic-fixture:regression", "status": "passed" } + ], + "example_only": true, + "exceptions": [], + "id": "invalid-stable-ended-support", + "known_issues": [], + "migration": { "plan": "Migrate every consumed surface before removal.", "required": true }, + "normative_bypass": "none", + "owner": { "enforcement": "placeholder", "migration": "Synthetic migration owner", "rollback": "Synthetic rollback owner", "scope": "Repository governance owner", "support": "Synthetic support owner" }, + "provenance": { "debt_resolved": true, "source": "Synthetic fixture only" }, + "review_independence": "single_account", + "rollback": { "action": "Maintain or deprecate with migration.", "triggers": ["Stable support capacity ends."] }, + "schema_version": "1.0", + "scope": { "aesthetic": false, "kind": "invariant", "summary": "Incorrectly propose stable status after support has ended." }, + "stable_scope_decision": true, + "support": { "capacity": "No remaining support capacity", "status": "ended" }, + "transition": { "from": "shared_experimental", "to": "stable" } +} diff --git a/consumer-reference/fixtures/promotion/manifest.json b/consumer-reference/fixtures/promotion/manifest.json new file mode 100644 index 0000000..c4bc9f2 --- /dev/null +++ b/consumer-reference/fixtures/promotion/manifest.json @@ -0,0 +1,15 @@ +{ + "id": "promotion-fixture-inventory", + "invalid_records": [ + { "expected_code": "promotion_blocking_evidence_failed", "file": "invalid-blocking-evidence.json" }, + { "expected_code": "promotion_count_only_forbidden", "file": "invalid-count-only.json" }, + { "expected_code": "promotion_owner_migration_required", "file": "invalid-missing-owner-migration.json" }, + { "expected_code": "promotion_normative_regression_required", "file": "invalid-normative-bypass-no-regression.json" }, + { "expected_code": "promotion_related_consumer_counted", "file": "invalid-related-counted-independent.json" }, + { "expected_code": "stable_support_ended", "file": "invalid-stable-ended-support.json" } + ], + "policy_record": "consumer-reference/policies/shared-experimental.json", + "schema_record": "consumer-reference/schema/promotion-rfc.schema.json", + "schema_version": "1.0", + "valid_records": ["valid-deferred-example.json", "valid-normative-bypass.json"] +} diff --git a/consumer-reference/fixtures/promotion/valid-deferred-example.json b/consumer-reference/fixtures/promotion/valid-deferred-example.json new file mode 100644 index 0000000..702dfbb --- /dev/null +++ b/consumer-reference/fixtures/promotion/valid-deferred-example.json @@ -0,0 +1,67 @@ +{ + "attestations": [], + "compatibility": { + "breaking": false, + "surface": "Synthetic invariant contract surface." + }, + "decision": "deferred", + "deprecation": { + "plan": "No deprecation is required while this synthetic proposal remains deferred.", + "required": false + }, + "eligibility_basis": "full_contract", + "evidence": [ + { + "id": "claim-scoped-example", + "kind": "claim_scoped", + "reference": "synthetic-fixture:claim-scoped-example", + "status": "passed" + }, + { + "id": "regression-example", + "kind": "regression", + "reference": "synthetic-fixture:regression-example", + "status": "passed" + } + ], + "example_only": true, + "exceptions": [], + "id": "valid-deferred-example", + "known_issues": [], + "migration": { + "plan": "No migration begins unless a later separately reviewed decision changes the deferred status.", + "required": false + }, + "normative_bypass": "none", + "owner": { + "enforcement": "placeholder", + "migration": "Synthetic migration owner", + "rollback": "Synthetic rollback owner", + "scope": "Repository governance owner", + "support": "Synthetic support owner" + }, + "provenance": { + "debt_resolved": false, + "source": "Synthetic fixture only" + }, + "review_independence": "single_account", + "rollback": { + "action": "Keep the proposal deferred and remove any unsupported shared routing.", + "triggers": ["Blocking evidence fails or the scope expands beyond the reviewed invariant."] + }, + "schema_version": "1.0", + "scope": { + "aesthetic": false, + "kind": "invariant", + "summary": "Demonstrate a synthetic invariant promotion proposal without claiming adoption." + }, + "stable_scope_decision": false, + "support": { + "capacity": "committed", + "status": "active" + }, + "transition": { + "from": "consumer_local", + "to": "shared_experimental" + } +} diff --git a/consumer-reference/fixtures/promotion/valid-normative-bypass.json b/consumer-reference/fixtures/promotion/valid-normative-bypass.json new file mode 100644 index 0000000..cdf3a5d --- /dev/null +++ b/consumer-reference/fixtures/promotion/valid-normative-bypass.json @@ -0,0 +1,67 @@ +{ + "attestations": [], + "compatibility": { + "breaking": false, + "surface": "Synthetic correctness contract surface." + }, + "decision": "deferred", + "deprecation": { + "plan": "No deprecation is required for this deferred synthetic correction.", + "required": false + }, + "eligibility_basis": "full_contract", + "evidence": [ + { + "id": "claim-scoped-correctness", + "kind": "claim_scoped", + "reference": "synthetic-fixture:claim-scoped-correctness", + "status": "passed" + }, + { + "id": "regression-correctness", + "kind": "regression", + "reference": "synthetic-fixture:regression-correctness", + "status": "passed" + } + ], + "example_only": true, + "exceptions": [], + "id": "valid-normative-bypass", + "known_issues": [], + "migration": { + "plan": "Apply only after compatibility review; keep a bounded rollback path.", + "required": false + }, + "normative_bypass": "normative_correctness", + "owner": { + "enforcement": "placeholder", + "migration": "Synthetic migration owner", + "rollback": "Synthetic rollback owner", + "scope": "Repository governance owner", + "support": "Synthetic support owner" + }, + "provenance": { + "debt_resolved": false, + "source": "Synthetic fixture only" + }, + "review_independence": "single_account", + "rollback": { + "action": "Keep the correction deferred or revert it if regression evidence fails.", + "triggers": ["Regression evidence fails or compatibility review finds an unsupported impact."] + }, + "schema_version": "1.0", + "scope": { + "aesthetic": false, + "kind": "normative_correctness", + "summary": "Demonstrate the adoption-count bypass without waiving regression or lifecycle duties." + }, + "stable_scope_decision": false, + "support": { + "capacity": "committed", + "status": "active" + }, + "transition": { + "from": "consumer_local", + "to": "shared_experimental" + } +} diff --git a/consumer-reference/index.md b/consumer-reference/index.md index 820baed..25597c0 100644 --- a/consumer-reference/index.md +++ b/consumer-reference/index.md @@ -14,6 +14,8 @@ Out of scope: profiles, themes, components, browser runtime, decorative styling, - [Receiver contract](contract.md) - Required handoff shape, record-path boundary, lifecycle fields, ownership truth, and dependency direction. - [Consumer reference item schema](schema/item.schema.json) - Machine-readable shape for the current receiver item. +- [Promotion RFC schema](schema/promotion-rfc.schema.json) - Closed JSON shape for deferred or rejected invariant-promotion proposals. +- [Shared-experimental promotion policy](policies/shared-experimental.json) - Count scope, normative bypass, lifecycle, and zero-promotion claim boundary. - [Chromium calibration schema](schema/calibration-record.schema.json) - Immutable environment, repetition, and evidence-cardinality contract for the proposed nonblocking sentinel. - [Chromium baseline manifest schema](schema/baseline-manifest.schema.json) - Exact proposed source, baseline, platform, and calibration routing contract. - `baselines/` - Proposed Chromium geometry baseline metadata and a pending committed-CI calibration record; these files do not constitute baseline-owner approval. diff --git a/consumer-reference/policies/shared-experimental.json b/consumer-reference/policies/shared-experimental.json new file mode 100644 index 0000000..d601f35 --- /dev/null +++ b/consumer-reference/policies/shared-experimental.json @@ -0,0 +1,37 @@ +{ + "artifact_mode": "schema_only", + "boundaries": { + "excluded_content": ["component_skin", "imagery", "motion_character", "palette", "typography"], + "promoted_content": ["claim_scoped_evidence", "invariant_behavior", "invariant_semantics"] + }, + "claims": { + "adopter_attestations": 0, + "promotion_occurred": false + }, + "gateway": { + "applies_to": "invariant_eligibility_only", + "minimum_independent_consumers": 2, + "stable_numeric_threshold": null, + "transition": { + "from": "consumer_local", + "to": "shared_experimental" + } + }, + "id": "shared-experimental-policy", + "lifecycle": { + "failure_actions": ["deprecate_with_migration", "maintain", "restrict", "rollback"], + "stable_relabel": "forbidden" + }, + "normative_bypass": { + "categories": ["accessibility", "correctness", "security", "standards"], + "retains": ["compatibility", "deprecation", "migration", "owner_acceptance", "regression_proof", "rollback", "scope_control", "support_capacity"], + "waives": ["adoption_count"] + }, + "owner": { + "enforcement": "placeholder", + "name": "Repository governance owner" + }, + "review_independence": "single_account", + "schema_version": "1.0", + "status": "active" +} diff --git a/consumer-reference/schema/promotion-rfc.schema.json b/consumer-reference/schema/promotion-rfc.schema.json new file mode 100644 index 0000000..796940b --- /dev/null +++ b/consumer-reference/schema/promotion-rfc.schema.json @@ -0,0 +1,219 @@ +{ + "$defs": { + "attestation": { + "additionalProperties": false, + "properties": { + "attested_by": { "minLength": 1, "type": "string" }, + "context_id": { "$ref": "#/$defs/slug" }, + "consumer_id": { "$ref": "#/$defs/slug" }, + "count_toward_gate": { "type": "boolean" }, + "organization_id": { "$ref": "#/$defs/slug" }, + "profile_record": { + "pattern": "^(?!/)(?!.*//)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?![A-Za-z][A-Za-z0-9+.-]*:)[^\\\\?#]+\\.json$", + "type": "string" + }, + "relationship": { "enum": ["independent", "related"] } + }, + "required": ["attested_by", "context_id", "consumer_id", "count_toward_gate", "organization_id", "relationship"], + "type": "object" + }, + "evidence": { + "additionalProperties": false, + "properties": { + "id": { "$ref": "#/$defs/slug" }, + "kind": { "enum": ["blocking", "claim_scoped", "regression"] }, + "reference": { "minLength": 1, "type": "string" }, + "status": { "enum": ["failed", "passed"] } + }, + "required": ["id", "kind", "reference", "status"], + "type": "object" + }, + "slug": { + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "type": "string" + } + }, + "$id": "https://stylegallery.example/consumer-reference/schema/promotion-rfc.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { "transition": { "properties": { "to": { "const": "stable" } }, "required": ["to"], "type": "object" } }, + "required": ["transition"] + }, + "then": { + "properties": { + "provenance": { "properties": { "debt_resolved": { "const": true } }, "required": ["debt_resolved"], "type": "object" }, + "stable_scope_decision": { "const": true }, + "support": { "properties": { "status": { "const": "active" } }, "required": ["status"], "type": "object" }, + "evidence": { + "allOf": [ + { "contains": { "properties": { "kind": { "const": "claim_scoped" }, "status": { "const": "passed" } }, "required": ["kind", "status"], "type": "object" } }, + { "contains": { "properties": { "kind": { "const": "regression" }, "status": { "const": "passed" } }, "required": ["kind", "status"], "type": "object" } } + ], + "type": "array" + } + } + } + }, + { + "if": { "properties": { "normative_bypass": { "const": "normative_correctness" } }, "required": ["normative_bypass"] }, + "then": { "properties": { "scope": { "properties": { "kind": { "const": "normative_correctness" } }, "required": ["kind"], "type": "object" } } } + }, + { + "if": { "properties": { "compatibility": { "properties": { "breaking": { "const": true } }, "required": ["breaking"], "type": "object" } }, "required": ["compatibility"] }, + "then": { "properties": { "migration": { "properties": { "required": { "const": true } }, "required": ["required"], "type": "object" } } } + } + ], + "properties": { + "attestations": { + "items": { "$ref": "#/$defs/attestation" }, + "type": "array" + }, + "compatibility": { + "additionalProperties": false, + "properties": { + "breaking": { "type": "boolean" }, + "surface": { "minLength": 1, "type": "string" } + }, + "required": ["breaking", "surface"], + "type": "object" + }, + "decision": { "enum": ["deferred", "rejected"] }, + "deprecation": { + "additionalProperties": false, + "properties": { + "plan": { "minLength": 1, "type": "string" }, + "required": { "type": "boolean" } + }, + "required": ["plan", "required"], + "type": "object" + }, + "eligibility_basis": { "const": "full_contract" }, + "evidence": { + "items": { "$ref": "#/$defs/evidence" }, + "minItems": 1, + "type": "array" + }, + "example_only": { "const": true }, + "exceptions": { + "items": { "minLength": 1, "type": "string" }, + "type": "array" + }, + "id": { "$ref": "#/$defs/slug" }, + "known_issues": { + "items": { "minLength": 1, "type": "string" }, + "type": "array" + }, + "migration": { + "additionalProperties": false, + "properties": { + "plan": { "minLength": 1, "type": "string" }, + "required": { "type": "boolean" } + }, + "required": ["plan", "required"], + "type": "object" + }, + "normative_bypass": { "enum": ["none", "normative_correctness"] }, + "owner": { + "additionalProperties": false, + "properties": { + "enforcement": { "const": "placeholder" }, + "migration": { "minLength": 1, "type": "string" }, + "rollback": { "minLength": 1, "type": "string" }, + "scope": { "minLength": 1, "type": "string" }, + "support": { "minLength": 1, "type": "string" } + }, + "required": ["enforcement", "migration", "rollback", "scope", "support"], + "type": "object" + }, + "provenance": { + "additionalProperties": false, + "properties": { + "debt_resolved": { "type": "boolean" }, + "source": { "minLength": 1, "type": "string" } + }, + "required": ["debt_resolved", "source"], + "type": "object" + }, + "review_independence": { "const": "single_account" }, + "rollback": { + "additionalProperties": false, + "properties": { + "action": { "minLength": 1, "type": "string" }, + "triggers": { + "items": { "minLength": 1, "type": "string" }, + "minItems": 1, + "type": "array" + } + }, + "required": ["action", "triggers"], + "type": "object" + }, + "schema_version": { "const": "1.0" }, + "scope": { + "additionalProperties": false, + "properties": { + "aesthetic": { "const": false }, + "kind": { "enum": ["invariant", "normative_correctness"] }, + "summary": { "minLength": 1, "type": "string" } + }, + "required": ["aesthetic", "kind", "summary"], + "type": "object" + }, + "stable_scope_decision": { "type": "boolean" }, + "support": { + "additionalProperties": false, + "properties": { + "capacity": { "const": "committed" }, + "status": { "const": "active" } + }, + "required": ["capacity", "status"], + "type": "object" + }, + "transition": { + "additionalProperties": false, + "oneOf": [ + { + "properties": { "from": { "const": "consumer_local" }, "to": { "const": "shared_experimental" } }, + "required": ["from", "to"] + }, + { + "properties": { "from": { "const": "shared_experimental" }, "to": { "const": "stable" } }, + "required": ["from", "to"] + } + ], + "properties": { + "from": { "enum": ["consumer_local", "shared_experimental", "stable"] }, + "to": { "enum": ["shared_experimental", "stable"] } + }, + "required": ["from", "to"], + "type": "object" + } + }, + "required": [ + "attestations", + "compatibility", + "decision", + "deprecation", + "eligibility_basis", + "evidence", + "example_only", + "exceptions", + "id", + "known_issues", + "migration", + "normative_bypass", + "owner", + "provenance", + "review_independence", + "rollback", + "schema_version", + "scope", + "stable_scope_decision", + "support", + "transition" + ], + "type": "object" +} diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index b617539..b5ea4d3 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -30,6 +30,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Governance, lifecycle, generated-file, ownership, and stale-content policy remain discoverable and CI-enforced. | `scripts/validate-governance.mjs` and `scripts/test-validate-governance.mjs` | `node scripts/validate-governance.mjs --json`; `node scripts/test-validate-governance.mjs --json` | `GOVERNANCE.md`, `.github/CODEOWNERS`, generated warnings, generated metadata, root link targets, lifecycle states, `scheduled_stale_audit` metadata, and CI wiring are present. | Missing governance file, generated warning, generated metadata, CODEOWNERS coverage, or stale policy fixtures must fail. Preferred governance prose and link labels emit warnings. | Proves governance policy is present and linked, not that CODEOWNERS users have verified repository write access. | | Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity, source ownership, or local quality. | | Consumer-reference handoffs, schema/runtime parity, repository-local record paths, lifecycle separation, ownership truth, and dependency direction remain enforced. | `scripts/validate-consumer-reference.mjs` and `scripts/test-validate-consumer-reference.mjs` | `node scripts/validate-consumer-reference.mjs --json`; `node scripts/test-validate-consumer-reference.mjs --json` | Declared and reasoned-not-applicable fixtures exit zero, every shipped schema rule has accepted/rejected parity coverage, all repository handoffs declare applicability, fixtures remain related, and ownership discloses placeholder single-account review. | Unknown top-level or nested properties, invalid schema fields, missing handoff or reason, stable with ended support, boolean independence, unresolved/absolute/scheme/network/parent/item-or-record-symlink/non-JSON paths, scaffold success, fifth-domain classification, literal or supported computed reverse imports, and repository handoff omissions must fail with named finding codes and non-zero child exit. | Proves receiver-contract consistency, bounded static dependency detection, and path containment, not general JavaScript data-flow safety, visual quality, independent adoption, verified owner permissions, or consumer implementation conformance. | +| Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract. | `scripts/validate-promotion-rfc.mjs` and `scripts/test-validate-promotion-rfc.mjs` | `node scripts/validate-promotion-rfc.mjs --json`; `node scripts/test-validate-promotion-rfc.mjs --json` | The manifest-declared policy and two canonical examples pass with deferred decisions, zero attestations, exact support commitment, and no promotion claim. | Manifest-declared invalid fixtures plus forged, unregistered, duplicate, path, extension, support, evidence, canonical-drift, and lifecycle mutations exit non-zero with named findings. | Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision; the current canonical inventory supplies no real independent consumers. | | Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | | The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | diff --git a/scripts/governance-test-fixture.mjs b/scripts/governance-test-fixture.mjs new file mode 100644 index 0000000..83b51b7 --- /dev/null +++ b/scripts/governance-test-fixture.mjs @@ -0,0 +1,221 @@ +export const generatedWarning = ""; +export const sentinelProvenanceClauses = [ + "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", + "The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery.", + "The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256.", + "Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked.", + "Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability.", + "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", + "Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance.", +]; + +export const files = { + "AGENTS.md": "# Agent Instructions\n\nSee [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md) and [StyleGallery Domains](DOMAINS.md).\n", + "README.md": "# StyleGallery\n\n- [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md)\n- [StyleGallery Domains](DOMAINS.md)\n", + "index.md": "# StyleGallery\n\n- [Governance, lifecycle, and docs-as-code](GOVERNANCE.md)\n- [StyleGallery Domains](DOMAINS.md)\n", + "DOMAINS.md": "# StyleGallery Domains\n\n### Consumer Reference Promotion\n\nPromotion records are JSON-only.\n", + "consumer-reference/contract.md": `They are not accepted or promoted RFCs.\nReview truth uses one enum; boolean aliases are invalid.\n${sentinelProvenanceClauses.join("\n")}\n`, + "consumer-reference/index.md": "[Promotion RFC schema](schema/promotion-rfc.schema.json)\n[Shared-experimental promotion policy](policies/shared-experimental.json)\n", + "consumer-reference/fixtures/promotion/manifest.json": '{"id": "promotion-fixture-inventory"}\n', + "consumer-reference/policies/shared-experimental.json": '{"review_independence": "single_account", "promotion_occurred": false, "adopter_attestations": 0, "stable_numeric_threshold": null}\n', + "CATALOG.md": `# Layout Pattern Catalog\n\n${generatedWarning}\n`, + "patterns/index.md": `# Pattern Categories\n\n${generatedWarning}\n`, + "patterns/stacking/index.md": `# Stacking\n\n${generatedWarning}\n`, + "patterns/stacking/stack.md": [ + "---", + "lifecycle: generated", + "generated_from: scripts/generate-patterns.mjs, scripts/pattern-data.mjs", + "---", + "", + "# stack", + "", + generatedWarning, + "", + ].join("\n"), + "quality/evidence/executable-evidence.md": [ + "| Claim | Validator or test | CI command | Positive evidence | Negative evidence | Evidence boundary |", + "| Governance, lifecycle, generated-file, ownership, and stale-content policy remain discoverable and CI-enforced. | `scripts/validate-governance.mjs` and `scripts/test-validate-governance.mjs` | `node scripts/validate-governance.mjs --json`; `node scripts/test-validate-governance.mjs --json` | `GOVERNANCE.md`, `.github/CODEOWNERS`, generated warnings, generated metadata, root links, lifecycle states, stale-audit decision, and CI wiring are present. | Missing governance file, generated warning, generated metadata, CODEOWNERS coverage, or stale policy fixtures must fail. | Proves governance policy is present and linked, not that CODEOWNERS users have verified repository write access. |", + "| Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity or local quality. |", + "Consumer-reference handoffs, schema/runtime parity, and containment remain enforced; repository handoff omissions must fail.", + "Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract.", + "Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision.", + "The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", + "Linux/amd64 20-run calibration and `baseline_owner_approval` remain pending.", + ...sentinelProvenanceClauses, + "", + ].join("\n"), + ".github/CODEOWNERS": [ + "* @changeroa", + "/GOVERNANCE.md @changeroa", + "/README.md @changeroa", + "/index.md @changeroa", + "/AGENTS.md @changeroa", + "/DOMAINS.md @changeroa", + "/GUIDE.md @changeroa", + "/guides/ @changeroa", + "/recipes/ @changeroa", + "/quality/ @changeroa", + "/layout/ @changeroa", + "/motion/ @changeroa", + "/design-engineering/ @changeroa", + "/platform-guides/ @changeroa", + "/consumer-reference/ @changeroa", + "/consumer-reference/baselines/ @changeroa", + "/consumer-reference/policies/ @changeroa", + "/consumer-reference/fixtures/promotion/ @changeroa", + "/consumer-reference/schema/promotion-rfc.schema.json @changeroa", + "/tests/ @changeroa", + "/playwright.config.mjs @changeroa", + "/scripts/pattern-data.mjs @changeroa", + "/scripts/generate-patterns.mjs @changeroa", + "/patterns/ @changeroa", + "/CATALOG.md @changeroa", + "/.github/workflows/validate.yml @changeroa", + "/.github/ @changeroa", + "/scripts/ @changeroa", + "/scripts/validate-*.mjs @changeroa", + "/scripts/test-validate-*.mjs @changeroa", + "", + ].join("\n"), + ".github/workflows/validate.yml": [ + "jobs:", + " validate:", + "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", + "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", + "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", + "node -c scripts/component-state-workflow-contract.mjs", + "node -c scripts/promotion-fixture-inventory.mjs", + "node -c scripts/promotion-boundary-test-contract.mjs", + "node -c scripts/validate-governance.mjs", + "node -c scripts/test-validate-governance.mjs", + "node scripts/validate-governance.mjs --json", + "node scripts/test-validate-governance.mjs --json", + "node -c scripts/validate-domains.mjs", + "node -c scripts/test-validate-domains.mjs", + "node scripts/validate-domains.mjs --json", + "node scripts/test-validate-domains.mjs", + "node scripts/validate-webpage-workflow.mjs --json", + "node scripts/test-validate-webpage-workflow.mjs --json", + "node -c scripts/validate-consumer-reference.mjs", + "node -c scripts/test-validate-consumer-reference.mjs", + "node -c scripts/governed-profile-registry.mjs", + "node -c scripts/promotion-attestation-contract.mjs", + "node -c scripts/validate-promotion-rfc.mjs", + "node -c scripts/test-validate-promotion-rfc.mjs", + "node scripts/validate-consumer-reference.mjs --json", + "node scripts/test-validate-consumer-reference.mjs --json", + "node scripts/validate-promotion-rfc.mjs --json", + "node scripts/test-validate-promotion-rfc.mjs --json", + "consumer-reference/schema/promotion-rfc.schema.json", + "consumer-reference/policies/shared-experimental.json", + "node scripts/test-consumer-reference-sentinel.mjs", + " component-state-evidence:", + " env:", + " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", + " container:", + " image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + " steps:", + " - name: Run artifact harness", + " run: node scripts/test-validate-component-state-artifacts.mjs", + " - name: Capture evidence", + " env:", + " SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + " run: |", + " node scripts/create-component-state-session.mjs \\", + " --output \"$STATE_EVIDENCE_ROOT/capture-session.json\" \\", + " --json", + " STATE_ARTIFACT_DIR=\"$STATE_EVIDENCE_ROOT/runtime\" \\", + " STATE_SESSION_RECEIPT=\"$STATE_EVIDENCE_ROOT/capture-session.json\" \\", + " npm run test:component-state:runtime -- --reporter=line", + " node scripts/finalize-component-state-evidence.mjs \\", + " --artifact-root \"$STATE_EVIDENCE_ROOT\" \\", + " --output \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\", + " --json", + " node scripts/validate-component-state.mjs \\", + " --artifact-root \"$STATE_EVIDENCE_ROOT\" \\", + " --runtime-manifest \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\", + " --json", + " - name: Upload evidence", + " with:", + " path: ${{ env.STATE_EVIDENCE_ROOT }}/", + " chromium-sentinel:", + " steps:", + " - run: node scripts/test-consumer-reference-sentinel.mjs", + " chromium-calibration:", + "node scripts/validate-baseline-manifest.mjs --json", + "node scripts/test-validate-baseline-manifest.mjs --json", + "node scripts/test-summarize-sentinel-calibration.mjs", + "node scripts/validate-renderer-purity.mjs --json", + "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\"", + "--repository \"changeroa/StyleGallery\" \\", + "--execution-repository \"$GITHUB_REPOSITORY\" \\", + "\"repository\":\"%s\",\"execution_repository\":\"%s\"", + "\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", + "permissions:", + "contents: read", + "", + ].join("\n"), + "GOVERNANCE.md": [ + "---", + "scheduled_stale_audit: deferred", + "---", + "", + "# Governance, Lifecycle, And Docs-As-Code", + "", + "| Doc family | Source of truth | Generator | Generated artifacts | Lifecycle state | Stale trigger | Validator | Review owner |", + "| Root repository guide | `README.md` | Manual | None | `stable` | Source-of-truth route changes, broken root links, or ownership changes. | `scripts/validate-okf.mjs` | Repository governance owner |", + "| OKF bundle map | `index.md` | Manual | None | `scripts/validate-okf.mjs` | Repository governance owner |", + "| Agent editing rules | `AGENTS.md` | Manual | None | `scripts/validate-links.mjs` | Repository governance owner |", + "| Planning guides | `GUIDE.md`, `guides/*.md` | Manual | None | `scripts/validate-okf.mjs` | Planning-doc owner |", + "| Layout recipes | `recipes/*.md` | Manual | None | `scripts/validate-okf.mjs` | Recipe owner |", + "| Quality gates and evidence | `quality/**/*.md` | Manual | None | `scripts/validate-okf.mjs` | Quality owner |", + "| Consumer reference contract | `consumer-reference/contract.md` | Manual | None | `stable` | Receiver changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", + "| Component-state evidence matrices | Declared component, state, fixture, and evidence records | `scripts/generate-consumer-reference-evidence.mjs` | Six profile matrices | `generated` | Source or generator changes. | `scripts/validate-component-state.mjs` | Validation owner |", + "| Shared promotion policy | Promotion policy | Manual | None | stable | Policy changes. | promotion validator | Validation owner |", + "| Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/calibration-record.schema.json`, `consumer-reference/baselines/*.json` | Playwright | Snapshot and raw calibration evidence | `experimental` | Renderer or baseline changes. | `scripts/validate-baseline-manifest.mjs` | Validation owner |", + "| Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner |", + "| Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner |", + "| Motion domain guidance | `motion/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Motion domain owner |", + "| Design Engineering domain guidance | `design-engineering/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Design Engineering domain owner |", + "| Platform Guides domain guidance | `platform-guides/*.md` | Manual | None | `experimental` | Platform version, upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Platform Guides domain owner |", + "| Pattern data and examples | `scripts/pattern-data.mjs` | Manual data source | `patterns/**/*.md` | `generated` output from `stable` source | Source-lineage URL changes, generated drift, category changes, or pattern count changes. | `scripts/validate-patterns.mjs`, `scripts/validate-catalog.mjs`, `scripts/validate-governance.mjs` | Pattern-data owner |", + "| Pattern generator | `scripts/generate-patterns.mjs` | Manual code source | `patterns/**/*.md` | `stable` generator, `generated` output | Generated structure changes, generated-warning changes, or generated metadata changes. | `node -c scripts/generate-patterns.mjs` | Pattern-data owner |", + "| Validation scripts | `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs` | Manual code source | CI validation output | `node -c` | Validation owner |", + "| CI workflow | `.github/workflows/validate.yml` | Manual | GitHub Actions run | GitHub Actions | Repository governance owner |", + "", + "## Generated Artifact Policy", + "Do not hand-edit generated artifacts.", + "`scripts/pattern-data.mjs`", + "`scripts/generate-patterns.mjs`", + "`scripts/generate-consumer-reference-evidence.mjs`", + "Browser state evidence begins with `scripts/create-component-state-session.mjs`", + "Validation uses the receipt and completed manifest interval, not a wall-clock maximum age", + "design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md", + "design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md", + "design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md", + "design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md", + "design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md", + "design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md", + "", + "## Lifecycle States", + "`draft` `stable` `deprecated` `experimental` `generated`", + "", + "## Review Ownership", + "CODEOWNERS", + "owner.enforcement: \"placeholder\"", + "review_independence: \"single_account\"", + "The StyleGallery-local `>=2` independent-consumer gateway applies only to consumer-local → shared-experimental invariant eligibility.", + "Shared stable has no numeric adoption threshold.", + "Normative correctness may waive adoption count only.", + "Synthetic examples have zero durable adopter attestations and record no promotion.", + "A stable contract must never be silently relabeled shared-experimental.", + "", + "## Staleness Control", + "Decision: no scheduled stale-content workflow yet.", + "Audit trigger:", + "node scripts/validate-links.mjs --json", + "node scripts/test-consumer-reference-sentinel.mjs", + ...sentinelProvenanceClauses, + "", + ].join("\n"), +}; diff --git a/scripts/governed-profile-registry.mjs b/scripts/governed-profile-registry.mjs new file mode 100644 index 0000000..0ca87d9 --- /dev/null +++ b/scripts/governed-profile-registry.mjs @@ -0,0 +1,125 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv2020 from "ajv/dist/2020.js"; +import { isPlainObject, validateItemSchema } from "./consumer-reference-schema.mjs"; +import { resolveProfileRecords } from "./profile-record-contract.mjs"; +import { validateReferenceProfile, validateReferenceProfileSet } from "./reference-profile-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const itemSchema = parseStrictJson(fs.readFileSync(path.join(moduleRoot, "consumer-reference/schema/item.schema.json"), "utf8")); +const validateFullItemSchema = new Ajv2020({ allErrors: true, strict: false }).compile(itemSchema); + +export const canonicalGovernedProfilePaths = Object.freeze([ + "design-engineering/reference-profiles/governed-local/editorial/profile.json", + "design-engineering/reference-profiles/governed-local/terminal/profile.json", +]); + +function finding(code, recordPath, message) { + return { code, message, path: recordPath }; +} + +function isInside(base, target) { + const relative = path.relative(base, target); + return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)); +} + +function resolveRegisteredFile(root, relative, failures) { + if (!canonicalGovernedProfilePaths.includes(relative)) { + failures.push(finding("profile_registry_unregistered", relative, "profile path is not in the canonical governed profile inventory")); + return undefined; + } + const resolvedRoot = path.resolve(root); + const target = path.resolve(resolvedRoot, relative); + if (!isInside(resolvedRoot, target) || target !== path.join(resolvedRoot, relative)) { + failures.push(finding("profile_registry_path_invalid", relative, "registered profile path must remain normalized and repository-local")); + return undefined; + } + let current = resolvedRoot; + for (const segment of relative.split("/")) { + current = path.join(current, segment); + if (!fs.existsSync(current)) { + failures.push(finding("profile_registry_missing", relative, "registered profile path does not exist")); + return undefined; + } + if (fs.lstatSync(current).isSymbolicLink()) { + failures.push(finding("profile_registry_symlink", relative, "registered profile path must not traverse a symlink")); + return undefined; + } + } + if (!fs.lstatSync(target).isFile()) { + failures.push(finding("profile_registry_type_invalid", relative, "registered profile must be a regular JSON file")); + return undefined; + } + const realRoot = fs.realpathSync(resolvedRoot); + const realTarget = fs.realpathSync(target); + if (!isInside(realRoot, realTarget) || realTarget !== path.join(realRoot, relative)) { + failures.push(finding("profile_registry_redirect", relative, "registered profile must not resolve through a filesystem redirect")); + return undefined; + } + return realTarget; +} + +export function validateGovernedProfile({ relative, root }) { + const failures = []; + const file = resolveRegisteredFile(root, relative, failures); + if (!file) return { failures, profile: undefined, summary: undefined }; + let profile; + try { + profile = parseStrictJson(fs.readFileSync(file, "utf8")); + } catch (error) { + failures.push(finding("profile_json_invalid", relative, error instanceof Error ? error.message : String(error))); + return { failures, profile: undefined, summary: undefined }; + } + if (!validateFullItemSchema(profile)) { + for (const error of validateFullItemSchema.errors ?? []) failures.push(finding("item_schema_invalid", relative, `${error.instancePath || "/"} ${error.message}`)); + } + for (const issue of validateItemSchema(profile, itemSchema)) failures.push(finding(issue.code, relative, issue.message)); + const profileValidation = validateReferenceProfile({ item: profile, relative, root }); + failures.push(...profileValidation.failures); + const records = resolveProfileRecords(path.dirname(file), failures); + if (profile.handoff?.status !== "declared" || profile.handoff?.record !== relative) { + failures.push(finding("profile_registry_handoff_mismatch", relative, "registered profile handoff must name its exact inventory path")); + } + if (records?.profile?.id !== profile.id) failures.push(finding("profile_identity_mismatch", relative, "resolved profile identity must match the validated registry item")); + return { failures, profile, summary: profileValidation.summary }; +} + +export function validateGovernedProfileInventory({ root }) { + const records = new Map(); + const failures = []; + const summaries = []; + const resolvedRoot = path.resolve(root); + if (!fs.existsSync(resolvedRoot) || fs.lstatSync(resolvedRoot).isSymbolicLink() || !fs.lstatSync(resolvedRoot).isDirectory() || fs.realpathSync(resolvedRoot) !== resolvedRoot) { + failures.push(finding("profile_registry_root_invalid", resolvedRoot, "governed profile repository root must be a regular non-symlink directory")); + return { failures, records }; + } + for (const relative of canonicalGovernedProfilePaths) { + const result = validateGovernedProfile({ relative, root }); + records.set(relative, result); + failures.push(...result.failures); + if (result.summary) summaries.push(result.summary); + } + const identities = new Map(); + for (const [relative, result] of records) { + if (!isPlainObject(result.profile) || typeof result.profile.id !== "string") continue; + const previous = identities.get(result.profile.id); + if (previous) failures.push(finding("profile_registry_identity_duplicate", relative, `profile identity duplicates ${previous}`)); + else identities.set(result.profile.id, relative); + } + failures.push(...validateReferenceProfileSet(summaries)); + const uniqueFailures = [...new Map(failures.map((issue) => [`${issue.code}:${issue.path}:${issue.message}`, issue])).values()]; + return { failures: uniqueFailures, records }; +} + +export function isIndependentPromotionProfile(profile) { + return isPlainObject(profile) + && profile.profile_kind === "governed_local" + && profile.artifact_mode === "governed_local" + && profile.fixture_independence === "independent" + && !Object.hasOwn(profile, "related_fixture_set_id") + && profile.example_only === false + && profile.maturity !== "deprecated" + && profile.support?.status === "active"; +} diff --git a/scripts/promotion-attestation-contract.mjs b/scripts/promotion-attestation-contract.mjs new file mode 100644 index 0000000..b7efb6f --- /dev/null +++ b/scripts/promotion-attestation-contract.mjs @@ -0,0 +1,91 @@ +import fs from "node:fs"; +import path from "node:path"; +import { isPlainObject } from "./consumer-reference-schema.mjs"; +import { canonicalGovernedProfilePaths, isIndependentPromotionProfile } from "./governed-profile-registry.mjs"; + +function finding(code, message, recordPath) { + return { code, message, path: recordPath }; +} + +function safeProfilePath(relative, profileRoot, recordPath, failures) { + if (typeof relative !== "string" || !/^(?!\/)(?!.*\/\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?![A-Za-z][A-Za-z0-9+.-]*:)[^\\?#]+\.json$/.test(relative)) { + failures.push(finding("promotion_profile_path_invalid", "profile_record must be a normalized repository-relative JSON path", recordPath)); + return undefined; + } + const target = path.resolve(profileRoot, relative); + if (target === profileRoot || !target.startsWith(`${profileRoot}${path.sep}`)) { + failures.push(finding("promotion_profile_path_invalid", "profile_record escapes the repository", recordPath)); + return undefined; + } + let current = profileRoot; + for (const segment of path.relative(profileRoot, target).split(path.sep)) { + current = path.join(current, segment); + if (!fs.existsSync(current)) { + failures.push(finding("promotion_profile_missing", `${relative} does not exist`, recordPath)); + return undefined; + } + if (fs.lstatSync(current).isSymbolicLink()) { + failures.push(finding("promotion_profile_path_symlink", `${relative} traverses a symlink`, recordPath)); + return undefined; + } + } + if (!fs.lstatSync(target).isFile()) { + failures.push(finding("promotion_profile_invalid", `${relative} must be a regular JSON file`, recordPath)); + return undefined; + } + const realRoot = fs.realpathSync(profileRoot); + const real = fs.realpathSync(target); + if (!real.startsWith(`${realRoot}${path.sep}`) || real !== path.join(realRoot, relative)) { + failures.push(finding("promotion_profile_path_invalid", `${relative} resolves outside the repository`, recordPath)); + return undefined; + } + return relative; +} + +export function validatePromotionAttestations({ inventory, profileRoot, recordPath, value }) { + const attestations = Array.isArray(value.attestations) ? value.attestations : []; + const consumers = new Set(); + const countedContexts = new Set(); + const countedOrganizations = new Set(); + const failures = []; + const identities = new Set(); + let eligibleCount = 0; + for (const attestation of attestations) { + if (!isPlainObject(attestation)) continue; + const identity = `${attestation.consumer_id ?? ""}:${attestation.context_id ?? ""}:${attestation.organization_id ?? ""}`; + if (identities.has(identity) || consumers.has(attestation.consumer_id)) failures.push(finding("promotion_attestation_duplicate", `duplicate consumer or attestation identity ${identity}`, recordPath)); + identities.add(identity); + consumers.add(attestation.consumer_id); + const counted = attestation.count_toward_gate === true; + if (attestation.profile_record === undefined) { + failures.push(finding("promotion_attestation_profile_required", `${identity} has no canonical repository-local profile record`, recordPath)); + if (counted) failures.push(finding("promotion_independence_unproven", `${identity} cannot establish counted independence without a profile record`, recordPath)); + continue; + } + const registeredPath = safeProfilePath(attestation.profile_record, profileRoot, recordPath, failures); + if (!registeredPath) continue; + const registered = inventory.records.get(registeredPath); + if (!canonicalGovernedProfilePaths.includes(registeredPath) || !registered || registered.failures.length > 0 || inventory.failures.length > 0) { + failures.push(finding("promotion_independence_unproven", `${attestation.consumer_id} is not a fully validated registered governed profile`, recordPath)); + continue; + } + const profile = registered.profile; + if (!isPlainObject(profile)) continue; + const identityMatches = profile.id === attestation.consumer_id; + if (!identityMatches) failures.push(finding("promotion_profile_identity_mismatch", `${attestation.profile_record} does not match ${attestation.consumer_id}`, recordPath)); + if (!counted) continue; + if (attestation.relationship !== "independent") failures.push(finding("promotion_related_consumer_counted", `${identity} is related and cannot count toward the independent gateway`, recordPath)); + if (countedOrganizations.has(attestation.organization_id) || countedContexts.has(attestation.context_id)) { + failures.push(finding("promotion_independence_unproven", `${identity} does not establish a distinct organization and context`, recordPath)); + } + countedOrganizations.add(attestation.organization_id); + countedContexts.add(attestation.context_id); + const canonicalIndependent = profile.fixture_independence === "independent" && typeof profile.related_fixture_set_id !== "string"; + if (!canonicalIndependent) failures.push(finding("promotion_related_consumer_counted", `${attestation.consumer_id} is a canonical related fixture and cannot count`, recordPath)); + if (identityMatches && attestation.relationship === "independent" && canonicalIndependent && isIndependentPromotionProfile(profile)) eligibleCount += 1; + else if (identityMatches && canonicalIndependent && !isIndependentPromotionProfile(profile)) { + failures.push(finding("promotion_independence_unproven", `${attestation.consumer_id} is not lifecycle- and scope-eligible independent adoption evidence`, recordPath)); + } + } + return { eligibleCount, failures }; +} diff --git a/scripts/promotion-boundary-test-contract.mjs b/scripts/promotion-boundary-test-contract.mjs new file mode 100644 index 0000000..bd17cc4 --- /dev/null +++ b/scripts/promotion-boundary-test-contract.mjs @@ -0,0 +1,92 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function copyTreeNoSymlinks(source, target) { + fs.cpSync(source, target, { recursive: true, filter: (candidate) => { + if (fs.lstatSync(candidate).isSymbolicLink()) throw new Error(`refusing to copy symlink ${candidate}`); + return true; + } }); +} + +function copyFixtureRoot(fixtureRoot, tempRoot, name) { + const root = path.join(tempRoot, name); + copyTreeNoSymlinks(fixtureRoot, root); + return root; +} + +function editJson(file, mutate) { + const value = JSON.parse(fs.readFileSync(file, "utf8")); + mutate(value); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function result(runArguments, name, args, expected) { + const child = runArguments(args); + const codes = child.report.failures.map((failure) => failure.code); + return { actual: { codes, status: child.status }, expected, name, ok: child.status > 0 && codes.includes(expected) }; +} + +export function testPromotionBoundaries({ fixtureRoot, repositoryRoot, runArguments, tempRoot }) { + const results = []; + for (const [name, args] of [["file_option_value", ["--file", "--json"]], ["policy_option_value", ["--policy", "--json"]], ["profile_root_option_value", ["--profile-repository-root", "--json"]]]) { + results.push(result(runArguments, name, args, "argument_value_required")); + } + const yamlRfc = path.join(tempRoot, "promotion.yaml"); + const yamlPolicy = path.join(tempRoot, "policy.yaml"); + fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), yamlRfc); + fs.copyFileSync(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json"), yamlPolicy); + results.push(result(runArguments, "rfc_yaml_extension", ["--file", yamlRfc, "--json"], "promotion_json_path_required")); + results.push(result(runArguments, "policy_yaml_extension", ["--policy", yamlPolicy, "--json"], "promotion_json_path_required")); + + const linkedRfc = path.join(tempRoot, "linked-rfc.json"); + const linkedPolicy = path.join(tempRoot, "linked-policy.json"); + fs.symlinkSync(path.join(fixtureRoot, "valid-deferred-example.json"), linkedRfc); + fs.symlinkSync(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json"), linkedPolicy); + results.push(result(runArguments, "rfc_symlink", ["--file", linkedRfc, "--json"], "promotion_input_path_invalid")); + results.push(result(runArguments, "policy_symlink", ["--policy", linkedPolicy, "--json"], "promotion_input_path_invalid")); + + for (const [name, mutate] of [ + ["inventory_extra_json", (root) => fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), path.join(root, "extra.json"))], + ["inventory_extra_yaml", (root) => fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), path.join(root, "extra.yaml"))], + ["inventory_missing", (root) => fs.rmSync(path.join(root, "invalid-count-only.json"))], + ["inventory_duplicate", (root) => editJson(path.join(root, "manifest.json"), (value) => value.valid_records.push(value.valid_records[0]))], + ]) { + const root = copyFixtureRoot(fixtureRoot, tempRoot, name); + mutate(root); + results.push(result(runArguments, name, ["--promotion-fixture-root", root, "--json"], "promotion_fixture_inventory_invalid")); + } + + const rejectedRoot = copyFixtureRoot(fixtureRoot, tempRoot, "canonical-rejected"); + editJson(path.join(rejectedRoot, "valid-deferred-example.json"), (value) => { value.decision = "rejected"; }); + results.push(result(runArguments, "canonical_rejected", ["--promotion-fixture-root", rejectedRoot, "--json"], "promotion_canonical_example_invalid")); + const attestedRoot = copyFixtureRoot(fixtureRoot, tempRoot, "canonical-attested"); + editJson(path.join(attestedRoot, "valid-deferred-example.json"), (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "test", consumer_id: "fake", count_toward_gate: false, organization_id: "test", relationship: "related" }]; }); + results.push(result(runArguments, "canonical_attested", ["--promotion-fixture-root", attestedRoot, "--json"], "promotion_canonical_example_invalid")); + + const unbound = path.join(tempRoot, "unbound.json"); + const duplicateEvidence = path.join(tempRoot, "duplicate-evidence.json"); + fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), unbound); + editJson(unbound, (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "test", consumer_id: "fake", count_toward_gate: false, organization_id: "test", relationship: "related" }]; }); + fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), duplicateEvidence); + editJson(duplicateEvidence, (value) => { value.evidence.push(structuredClone(value.evidence[0])); }); + results.push(result(runArguments, "unbound_uncounted", ["--file", unbound, "--json"], "promotion_attestation_profile_required")); + results.push(result(runArguments, "duplicate_evidence_id", ["--file", duplicateEvidence, "--json"], "promotion_evidence_duplicate")); + results.push(result(runArguments, "missing_profile_root", ["--file", path.join(fixtureRoot, "valid-deferred-example.json"), "--profile-repository-root", path.join(tempRoot, "missing-root"), "--json"], "promotion_profile_registry_invalid")); + const linkedProfileRoot = path.join(tempRoot, "linked-profile-root"); + fs.symlinkSync(repositoryRoot, linkedProfileRoot); + results.push(result(runArguments, "symlink_profile_root", ["--file", path.join(fixtureRoot, "valid-deferred-example.json"), "--profile-repository-root", linkedProfileRoot, "--json"], "promotion_profile_registry_invalid")); + const linkedManifestRoot = copyFixtureRoot(fixtureRoot, tempRoot, "linked-manifest-root"); + fs.rmSync(path.join(linkedManifestRoot, "manifest.json")); + fs.symlinkSync(path.join(fixtureRoot, "manifest.json"), path.join(linkedManifestRoot, "manifest.json")); + results.push(result(runArguments, "symlink_manifest", ["--promotion-fixture-root", linkedManifestRoot, "--json"], "promotion_fixture_inventory_invalid")); + + const strictRoot = path.join(tempRoot, "strict-profile-root"); + copyTreeNoSymlinks(path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), path.join(strictRoot, "design-engineering/reference-profiles/governed-local")); + const foundations = path.join(strictRoot, "design-engineering/reference-profiles/governed-local/editorial/local-foundations.json"); + fs.writeFileSync(foundations, fs.readFileSync(foundations, "utf8").replace('"identity":', '"identity":"Duplicate",\n "identity":')); + const counted = path.join(tempRoot, "strict-counted.json"); + fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), counted); + editJson(counted, (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "editorial", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "test", profile_record: "design-engineering/reference-profiles/governed-local/editorial/profile.json", relationship: "independent" }]; }); + results.push(result(runArguments, "duplicate_foundation_identity", ["--file", counted, "--profile-repository-root", strictRoot, "--json"], "promotion_independence_unproven")); + return results; +} diff --git a/scripts/promotion-fixture-inventory.mjs b/scripts/promotion-fixture-inventory.mjs new file mode 100644 index 0000000..515469a --- /dev/null +++ b/scripts/promotion-fixture-inventory.mjs @@ -0,0 +1,109 @@ +import fs from "node:fs"; +import path from "node:path"; +import { parseStrictJson } from "./strict-json.mjs"; + +const validRecords = Object.freeze(["valid-deferred-example.json", "valid-normative-bypass.json"]); +const invalidRecords = Object.freeze([ + ["invalid-blocking-evidence.json", "promotion_blocking_evidence_failed"], + ["invalid-count-only.json", "promotion_count_only_forbidden"], + ["invalid-missing-owner-migration.json", "promotion_owner_migration_required"], + ["invalid-normative-bypass-no-regression.json", "promotion_normative_regression_required"], + ["invalid-related-counted-independent.json", "promotion_related_consumer_counted"], + ["invalid-stable-ended-support.json", "stable_support_ended"], +]); +const policyRecord = "consumer-reference/policies/shared-experimental.json"; +const schemaRecord = "consumer-reference/schema/promotion-rfc.schema.json"; + +function finding(code, message, recordPath) { + return { code, message, path: recordPath }; +} + +function isInside(base, target) { + const relative = path.relative(base, target); + return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)); +} + +function resolveContained({ code, expectedType, repositoryRoot, target }, failures) { + const root = path.resolve(repositoryRoot); + const resolved = path.resolve(target); + const recordPath = path.relative(root, resolved) || resolved; + if (!isInside(root, resolved) || !fs.existsSync(resolved)) { + failures.push(finding(code, "promotion input must exist inside the repository", recordPath)); + return undefined; + } + if (fs.realpathSync(root) !== root) { + failures.push(finding(code, "repository root must not be a filesystem redirect", recordPath)); + return undefined; + } + let current = root; + for (const segment of path.relative(root, resolved).split(path.sep).filter(Boolean)) { + current = path.join(current, segment); + if (fs.lstatSync(current).isSymbolicLink()) { + failures.push(finding(code, "promotion input must not traverse a symlink", recordPath)); + return undefined; + } + } + const metadata = fs.lstatSync(resolved); + if ((expectedType === "file" && !metadata.isFile()) || (expectedType === "directory" && !metadata.isDirectory()) || fs.realpathSync(resolved) !== resolved) { + failures.push(finding(code, `promotion input must be a contained regular ${expectedType}`, recordPath)); + return undefined; + } + return resolved; +} + +export function resolvePromotionJsonFile({ file, repositoryRoot }, failures) { + const recordPath = path.relative(repositoryRoot, path.resolve(file)) || file; + if (path.extname(file) !== ".json") { + failures.push(finding("promotion_json_path_required", "promotion records must use the .json extension", recordPath)); + return undefined; + } + return resolveContained({ code: "promotion_input_path_invalid", expectedType: "file", repositoryRoot, target: file }, failures); +} + +function sameSet(actual, expected) { + return Array.isArray(actual) && actual.length === expected.length && new Set(actual).size === expected.length && expected.every((entry) => actual.includes(entry)); +} + +function validManifest(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) return false; + const keys = ["id", "invalid_records", "policy_record", "schema_record", "schema_version", "valid_records"]; + if (!sameSet(Object.keys(value), keys) || value.id !== "promotion-fixture-inventory" || value.schema_version !== "1.0" || value.policy_record !== policyRecord || value.schema_record !== schemaRecord || !sameSet(value.valid_records, validRecords)) return false; + if (!Array.isArray(value.invalid_records) || value.invalid_records.length !== invalidRecords.length) return false; + const encoded = value.invalid_records.map((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && sameSet(Object.keys(entry), ["expected_code", "file"]) ? `${entry.file}:${entry.expected_code}` : ""); + return sameSet(encoded, invalidRecords.map(([file, code]) => `${file}:${code}`)); +} + +export function loadPromotionFixtureInventory({ fixtureRoot, repositoryRoot }) { + const failures = []; + const root = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "directory", repositoryRoot, target: fixtureRoot }, failures); + if (!root) return { failures, invalidRecords: [], policyFile: undefined, schema: undefined, validFiles: [] }; + const manifestFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(root, "manifest.json") }, failures); + let manifest; + try { + if (manifestFile) manifest = parseStrictJson(fs.readFileSync(manifestFile, "utf8")); + } catch (error) { + failures.push(finding("promotion_fixture_inventory_invalid", error instanceof Error ? error.message : String(error), path.relative(repositoryRoot, manifestFile))); + } + if (!validManifest(manifest)) failures.push(finding("promotion_fixture_inventory_invalid", "promotion manifest must match the canonical closed inventory", path.relative(repositoryRoot, manifestFile ?? root))); + const expectedFiles = ["manifest.json", ...validRecords, ...invalidRecords.map(([file]) => file)]; + const entries = fs.readdirSync(root, { withFileTypes: true }); + if (!sameSet(entries.map((entry) => entry.name), expectedFiles) || entries.some((entry) => !entry.isFile() || path.extname(entry.name) !== ".json")) { + failures.push(finding("promotion_fixture_inventory_invalid", "promotion fixture directory must contain only the declared JSON closed set", path.relative(repositoryRoot, root))); + } + for (const file of expectedFiles) resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(root, file) }, failures); + const policyFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(repositoryRoot, policyRecord) }, failures); + const schemaFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(repositoryRoot, schemaRecord) }, failures); + let schema; + try { + if (schemaFile) schema = parseStrictJson(fs.readFileSync(schemaFile, "utf8")); + } catch (error) { + failures.push(finding("promotion_fixture_inventory_invalid", error instanceof Error ? error.message : String(error), schemaRecord)); + } + return { + failures: [...new Map(failures.map((issue) => [`${issue.code}:${issue.path}:${issue.message}`, issue])).values()], + invalidRecords: invalidRecords.map(([file, expectedCode]) => ({ expectedCode, file: path.join(root, file) })), + policyFile, + schema, + validFiles: validRecords.map((file) => path.join(root, file)), + }; +} diff --git a/scripts/promotion-governance-contract.mjs b/scripts/promotion-governance-contract.mjs new file mode 100644 index 0000000..7e38705 --- /dev/null +++ b/scripts/promotion-governance-contract.mjs @@ -0,0 +1,28 @@ +const requirements = [ + ["GOVERNANCE.md", "The StyleGallery-local `>=2` independent-consumer gateway applies only to consumer-local → shared-experimental invariant eligibility."], + ["GOVERNANCE.md", "Shared stable has no numeric adoption threshold"], + ["GOVERNANCE.md", "may waive adoption count only"], + ["GOVERNANCE.md", "zero durable adopter attestations and record no promotion"], + ["GOVERNANCE.md", "never be silently relabeled shared-experimental"], + ["DOMAINS.md", "### Consumer Reference Promotion"], + ["DOMAINS.md", "Promotion records are JSON-only"], + ["consumer-reference/contract.md", "They are not accepted or promoted RFCs."], + ["consumer-reference/contract.md", "boolean aliases are invalid"], + ["consumer-reference/index.md", "[Promotion RFC schema](schema/promotion-rfc.schema.json)"], + ["consumer-reference/index.md", "[Shared-experimental promotion policy](policies/shared-experimental.json)"], + ["consumer-reference/fixtures/promotion/manifest.json", '"id": "promotion-fixture-inventory"'], + ["quality/evidence/executable-evidence.md", "Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract."], + ["quality/evidence/executable-evidence.md", "Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision"], + [".github/workflows/validate.yml", "node -c scripts/promotion-fixture-inventory.mjs"], + [".github/workflows/validate.yml", "node -c scripts/promotion-boundary-test-contract.mjs"], + ["consumer-reference/policies/shared-experimental.json", '"review_independence": "single_account"'], + ["consumer-reference/policies/shared-experimental.json", '"promotion_occurred": false'], + ["consumer-reference/policies/shared-experimental.json", '"adopter_attestations": 0'], + ["consumer-reference/policies/shared-experimental.json", '"stable_numeric_threshold": null'], +]; + +export function promotionGovernanceFailures(read) { + return requirements + .filter(([relative, text]) => !read(relative).includes(text)) + .map(([relative, text]) => `${relative}: missing ${text}`); +} diff --git a/scripts/reference-profile-contract.mjs b/scripts/reference-profile-contract.mjs index da8f28d..78da955 100644 --- a/scripts/reference-profile-contract.mjs +++ b/scripts/reference-profile-contract.mjs @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { isPlainObject } from "./consumer-reference-schema.mjs"; import { validatePortableTokens } from "./reference-token-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; const layoutSourceSha = "775430bbaf4ee208a642220f440f6926d79c90a3"; const requiredResetFields = ["body_margin", "box_sizing", "figure_margin"]; @@ -42,7 +43,7 @@ function readJson({ failures, kind, relative, root }) { } const bytes = fs.readFileSync(realTarget); try { - return { bytes, value: JSON.parse(bytes.toString("utf8")) }; + return { bytes, value: parseStrictJson(bytes.toString("utf8")) }; } catch (error) { failures.push(finding(`${kind}_json_invalid`, relative, error instanceof Error ? error.message : String(error))); return null; diff --git a/scripts/test-validate-domains.mjs b/scripts/test-validate-domains.mjs index 9ea73f5..b8d72f2 100644 --- a/scripts/test-validate-domains.mjs +++ b/scripts/test-validate-domains.mjs @@ -115,6 +115,15 @@ const baseFiles = { "", "[Consumer Reference](consumer-reference/index.md) is shared infrastructure and not a fifth domain.", "", + "### Consumer Reference Promotion", + "", + "The gateway applies only to consumer-local → shared-experimental invariant eligibility.", + "Editorial and terminal are related examples in one fixture set.", + "Shared stable has no numeric adoption threshold.", + "Normative correctness may waive adoption count only.", + "A stable contract is never silently relabeled experimental.", + "Promotion records are JSON-only and examples have zero adopter attestations.", + "", ].join("\n"), "layout/index.md": indexPage("Layout", [["Catalog", "../CATALOG.md"]]), "motion/index.md": indexPage("Motion", [["Motion Vocabulary", "vocabulary.md"], ["Motion Review Workflow", "review-workflow.md"], ["Motion Practice Reference", "practice-reference.md"]]), @@ -136,6 +145,9 @@ const cases = [ { name: "empty_manifest", mutate: ["DOMAINS.md", baseFiles["DOMAINS.md"], "# Empty manifest\n"], expect: "DOMAINS.md: missing canonical domain contract" }, { name: "manifest_extra_domain", mutate: ["DOMAINS.md", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |\n| Other | [Other](other/index.md) | `experimental` |"], expect: "DOMAINS.md: missing canonical domain contract" }, { name: "consumer_reference_fifth_domain", mutate: ["DOMAINS.md", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |", "| Platform Guides | [Platform Guides](platform-guides/index.md) | `experimental` |\n| Consumer Reference | [Consumer Reference](consumer-reference/index.md) | `stable` |"], expect: "DOMAINS.md: missing canonical domain contract" }, + { name: "promotion_stable_by_count", mutate: ["DOMAINS.md", "Shared stable has no numeric adoption threshold.", "Shared stable uses a numeric adoption threshold."], expect: "DOMAINS.md: missing promotion boundary Shared stable has no numeric adoption threshold" }, + { name: "promotion_related_as_independent", mutate: ["DOMAINS.md", "Editorial and terminal are related examples in one fixture set.", "Editorial and terminal are independent consumers."], expect: "DOMAINS.md: missing promotion boundary Editorial and terminal are related examples in one fixture set" }, + { name: "promotion_yaml_route", mutate: ["DOMAINS.md", "Promotion records are JSON-only", "Promotion records may use YAML"], expect: "DOMAINS.md: missing promotion boundary Promotion records are JSON-only" }, { name: "reference_profiles_fifth_domain", mutate: ["design-engineering/reference-profiles/index.md", "Domain classification: design-engineering.", "Domain classification: reference-profiles."], expect: "design-engineering/reference-profiles/index.md: reference profiles must remain in the Design Engineering domain" }, { name: "manifest_extra_leaf", mutate: ["DOMAINS.md", "`motion/vocabulary.md`,", "`motion/vocabulary.md`, `motion/ghost.md`,"], expect: "DOMAINS.md: missing canonical domain contract" }, { name: "manifest_wrong_lifecycle", mutate: ["DOMAINS.md", "| Motion | [Motion](motion/index.md) | `experimental` |", "| Motion | [Motion](motion/index.md) | `stable` |"], expect: "DOMAINS.md: missing canonical domain contract" }, @@ -203,4 +215,4 @@ function runCase(testCase) { const results = cases.map(runCase); const report = { ok: results.every((result) => result.ok), results }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index d20d7fa..a93279d 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -4,206 +4,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { files, generatedWarning, sentinelProvenanceClauses } from "./governance-test-fixture.mjs"; const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const validator = path.join(root, "scripts", "validate-governance.mjs"); -const generatedWarning = ""; -const sentinelProvenanceClauses = [ - "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", - "The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery.", - "The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256.", - "Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked.", - "Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability.", - "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", - "Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance.", -]; - -const files = { - "AGENTS.md": "# Agent Instructions\n\nSee [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md) and [StyleGallery Domains](DOMAINS.md).\n", - "README.md": "# StyleGallery\n\n- [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md)\n- [StyleGallery Domains](DOMAINS.md)\n", - "index.md": "# StyleGallery\n\n- [Governance, lifecycle, and docs-as-code](GOVERNANCE.md)\n- [StyleGallery Domains](DOMAINS.md)\n", - "DOMAINS.md": "# StyleGallery Domains\n", - "consumer-reference/contract.md": `${sentinelProvenanceClauses.join("\n")}\n`, - "CATALOG.md": `# Layout Pattern Catalog\n\n${generatedWarning}\n`, - "patterns/index.md": `# Pattern Categories\n\n${generatedWarning}\n`, - "patterns/stacking/index.md": `# Stacking\n\n${generatedWarning}\n`, - "patterns/stacking/stack.md": [ - "---", - "lifecycle: generated", - "generated_from: scripts/generate-patterns.mjs, scripts/pattern-data.mjs", - "---", - "", - "# stack", - "", - generatedWarning, - "", - ].join("\n"), - "quality/evidence/executable-evidence.md": [ - "| Claim | Validator or test | CI command | Positive evidence | Negative evidence | Evidence boundary |", - "| Governance, lifecycle, generated-file, ownership, and stale-content policy remain discoverable and CI-enforced. | `scripts/validate-governance.mjs` and `scripts/test-validate-governance.mjs` | `node scripts/validate-governance.mjs --json`; `node scripts/test-validate-governance.mjs --json` | `GOVERNANCE.md`, `.github/CODEOWNERS`, generated warnings, generated metadata, root links, lifecycle states, stale-audit decision, and CI wiring are present. | Missing governance file, generated warning, generated metadata, CODEOWNERS coverage, or stale policy fixtures must fail. | Proves governance policy is present and linked, not that CODEOWNERS users have verified repository write access. |", - "| Domain topology, metadata, provenance, scope boundaries, and root routes remain enforced. | `scripts/validate-domains.mjs` and `scripts/test-validate-domains.mjs` | `node scripts/validate-domains.mjs --json`; `node scripts/test-validate-domains.mjs` | Four governed domains and their declared leaves are reachable and attributed. | Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail. | A full SHA proves content identity syntax, not publisher authenticity or local quality. |", - "Consumer-reference handoffs, schema/runtime parity, and containment remain enforced; repository handoff omissions must fail.", - "The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", - "Linux/amd64 20-run calibration and `baseline_owner_approval` remain pending.", - ...sentinelProvenanceClauses, - "", - ].join("\n"), - ".github/CODEOWNERS": [ - "* @changeroa", - "/GOVERNANCE.md @changeroa", - "/README.md @changeroa", - "/index.md @changeroa", - "/AGENTS.md @changeroa", - "/DOMAINS.md @changeroa", - "/GUIDE.md @changeroa", - "/guides/ @changeroa", - "/recipes/ @changeroa", - "/quality/ @changeroa", - "/layout/ @changeroa", - "/motion/ @changeroa", - "/design-engineering/ @changeroa", - "/platform-guides/ @changeroa", - "/consumer-reference/ @changeroa", - "/consumer-reference/baselines/ @changeroa", - "/tests/ @changeroa", - "/playwright.config.mjs @changeroa", - "/scripts/pattern-data.mjs @changeroa", - "/scripts/generate-patterns.mjs @changeroa", - "/patterns/ @changeroa", - "/CATALOG.md @changeroa", - "/.github/workflows/validate.yml @changeroa", - "/.github/ @changeroa", - "/scripts/ @changeroa", - "/scripts/validate-*.mjs @changeroa", - "/scripts/test-validate-*.mjs @changeroa", - "", - ].join("\n"), - ".github/workflows/validate.yml": [ - "jobs:", - " validate:", - "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", - "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", - "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", - "node -c scripts/component-state-workflow-contract.mjs", - "node -c scripts/validate-governance.mjs", - "node -c scripts/test-validate-governance.mjs", - "node scripts/validate-governance.mjs --json", - "node scripts/test-validate-governance.mjs --json", - "node -c scripts/validate-domains.mjs", - "node -c scripts/test-validate-domains.mjs", - "node scripts/validate-domains.mjs --json", - "node scripts/test-validate-domains.mjs", - "node scripts/validate-webpage-workflow.mjs --json", - "node scripts/test-validate-webpage-workflow.mjs --json", - "node -c scripts/validate-consumer-reference.mjs", - "node -c scripts/test-validate-consumer-reference.mjs", - "node scripts/validate-consumer-reference.mjs --json", - "node scripts/test-validate-consumer-reference.mjs --json", - "node scripts/test-consumer-reference-sentinel.mjs", - " component-state-evidence:", - " env:", - " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", - " container:", - " image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", - " steps:", - " - name: Run artifact harness", - " run: node scripts/test-validate-component-state-artifacts.mjs", - " - name: Capture evidence", - " env:", - " SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", - " run: |", - " node scripts/create-component-state-session.mjs \\", - " --output \"$STATE_EVIDENCE_ROOT/capture-session.json\" \\", - " --json", - " STATE_ARTIFACT_DIR=\"$STATE_EVIDENCE_ROOT/runtime\" \\", - " STATE_SESSION_RECEIPT=\"$STATE_EVIDENCE_ROOT/capture-session.json\" \\", - " npm run test:component-state:runtime -- --reporter=line", - " node scripts/finalize-component-state-evidence.mjs \\", - " --artifact-root \"$STATE_EVIDENCE_ROOT\" \\", - " --output \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\", - " --json", - " node scripts/validate-component-state.mjs \\", - " --artifact-root \"$STATE_EVIDENCE_ROOT\" \\", - " --runtime-manifest \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\", - " --json", - " - name: Upload evidence", - " with:", - " path: ${{ env.STATE_EVIDENCE_ROOT }}/", - " chromium-sentinel:", - " steps:", - " - run: node scripts/test-consumer-reference-sentinel.mjs", - " chromium-calibration:", - "node scripts/validate-baseline-manifest.mjs --json", - "node scripts/test-validate-baseline-manifest.mjs --json", - "node scripts/test-summarize-sentinel-calibration.mjs", - "node scripts/validate-renderer-purity.mjs --json", - "checkout_sha=\"$(git -c safe.directory=\"$GITHUB_WORKSPACE\" rev-parse HEAD)\"", - "--repository \"changeroa/StyleGallery\" \\", - "--execution-repository \"$GITHUB_REPOSITORY\" \\", - "\"repository\":\"%s\",\"execution_repository\":\"%s\"", - "\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", - "permissions:", - "contents: read", - "", - ].join("\n"), - "GOVERNANCE.md": [ - "---", - "scheduled_stale_audit: deferred", - "---", - "", - "# Governance, Lifecycle, And Docs-As-Code", - "", - "| Doc family | Source of truth | Generator | Generated artifacts | Lifecycle state | Stale trigger | Validator | Review owner |", - "| Root repository guide | `README.md` | Manual | None | `stable` | Source-of-truth route changes, broken root links, or ownership changes. | `scripts/validate-okf.mjs` | Repository governance owner |", - "| OKF bundle map | `index.md` | Manual | None | `scripts/validate-okf.mjs` | Repository governance owner |", - "| Agent editing rules | `AGENTS.md` | Manual | None | `scripts/validate-links.mjs` | Repository governance owner |", - "| Planning guides | `GUIDE.md`, `guides/*.md` | Manual | None | `scripts/validate-okf.mjs` | Planning-doc owner |", - "| Layout recipes | `recipes/*.md` | Manual | None | `scripts/validate-okf.mjs` | Recipe owner |", - "| Quality gates and evidence | `quality/**/*.md` | Manual | None | `scripts/validate-okf.mjs` | Quality owner |", - "| Consumer reference contract | `consumer-reference/contract.md` | Manual | None | `stable` | Receiver changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", - "| Component-state evidence matrices | Declared component, state, fixture, and evidence records | `scripts/generate-consumer-reference-evidence.mjs` | Six profile matrices | `generated` | Source or generator changes. | `scripts/validate-component-state.mjs` | Validation owner |", - "| Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/calibration-record.schema.json`, `consumer-reference/baselines/*.json` | Playwright | Snapshot and raw calibration evidence | `experimental` | Renderer or baseline changes. | `scripts/validate-baseline-manifest.mjs` | Validation owner |", - "| Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner |", - "| Layout domain hub | `layout/index.md` | Manual | None | `stable` | Layout route or ownership changes. | `scripts/validate-domains.mjs`, `scripts/validate-ia.mjs` | Pattern-data owner |", - "| Motion domain guidance | `motion/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Motion domain owner |", - "| Design Engineering domain guidance | `design-engineering/*.md` | Manual | None | `experimental` | Upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Design Engineering domain owner |", - "| Platform Guides domain guidance | `platform-guides/*.md` | Manual | None | `experimental` | Platform version, upstream revision, evidence boundary, or guidance changes. | `scripts/validate-domains.mjs` | Platform Guides domain owner |", - "| Pattern data and examples | `scripts/pattern-data.mjs` | Manual data source | `patterns/**/*.md` | `generated` output from `stable` source | Source-lineage URL changes, generated drift, category changes, or pattern count changes. | `scripts/validate-patterns.mjs`, `scripts/validate-catalog.mjs`, `scripts/validate-governance.mjs` | Pattern-data owner |", - "| Pattern generator | `scripts/generate-patterns.mjs` | Manual code source | `patterns/**/*.md` | `stable` generator, `generated` output | Generated structure changes, generated-warning changes, or generated metadata changes. | `node -c scripts/generate-patterns.mjs` | Pattern-data owner |", - "| Validation scripts | `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs` | Manual code source | CI validation output | `node -c` | Validation owner |", - "| CI workflow | `.github/workflows/validate.yml` | Manual | GitHub Actions run | GitHub Actions | Repository governance owner |", - "", - "## Generated Artifact Policy", - "Do not hand-edit generated artifacts.", - "`scripts/pattern-data.mjs`", - "`scripts/generate-patterns.mjs`", - "`scripts/generate-consumer-reference-evidence.mjs`", - "Browser state evidence begins with `scripts/create-component-state-session.mjs`", - "Validation uses the receipt and completed manifest interval, not a wall-clock maximum age", - "design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md", - "design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md", - "design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md", - "design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md", - "design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md", - "design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md", - "", - "## Lifecycle States", - "`draft` `stable` `deprecated` `experimental` `generated`", - "", - "## Review Ownership", - "CODEOWNERS", - "owner.enforcement: \"placeholder\"", - "review_independence: \"single_account\"", - "", - "## Staleness Control", - "Decision: no scheduled stale-content workflow yet.", - "Audit trigger:", - "node scripts/validate-links.mjs --json", - "node scripts/test-consumer-reference-sentinel.mjs", - ...sentinelProvenanceClauses, - "", - ].join("\n"), -}; const cases = [ { name: "missing_governance", omit: ["GOVERNANCE.md"], expect: "GOVERNANCE.md: missing file" }, @@ -266,6 +70,10 @@ const cases = [ { name: "missing_motion_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/motion/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /motion/ @changeroa" }, { name: "missing_consumer_reference_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/consumer-reference/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /consumer-reference/ @changeroa" }, { name: "missing_baseline_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/consumer-reference/baselines/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /consumer-reference/baselines/ @changeroa" }, + { name: "missing_promotion_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/consumer-reference/policies/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /consumer-reference/policies/ @changeroa" }, + { name: "missing_promotion_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/test-validate-promotion-rfc.mjs --json\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-validate-promotion-rfc.mjs --json" }, + { name: "missing_stable_count_boundary", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("Shared stable has no numeric adoption threshold.", "Stable uses an adoption threshold.") }, expect: "GOVERNANCE.md: missing Shared stable has no numeric adoption threshold" }, + { name: "fake_promotion_policy_claim", mutate: { "consumer-reference/policies/shared-experimental.json": files["consumer-reference/policies/shared-experimental.json"].replace('"promotion_occurred": false', '"promotion_occurred": true') }, expect: 'consumer-reference/policies/shared-experimental.json: missing "promotion_occurred": false' }, { name: "missing_domain_family", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("| Motion domain guidance |", "| Motion reference notes |") }, expect: "GOVERNANCE.md: missing Motion domain guidance" }, { name: "missing_domain_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/validate-domains.mjs --json\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/validate-domains.mjs --json" }, { name: "missing_domain_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail.", "Domain fixtures must fail.") }, expect: "quality/evidence/executable-evidence.md: missing Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail." }, @@ -419,4 +227,4 @@ const report = { }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-promotion-rfc.mjs b/scripts/test-validate-promotion-rfc.mjs new file mode 100644 index 0000000..a843f99 --- /dev/null +++ b/scripts/test-validate-promotion-rfc.mjs @@ -0,0 +1,250 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { copyTreeNoSymlinks, testPromotionBoundaries } from "./promotion-boundary-test-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const fixtureRoot = path.join(repositoryRoot, "consumer-reference/fixtures/promotion"); +const validator = path.join(repositoryRoot, "scripts/validate-promotion-rfc.mjs"); + +function readJson(relative) { + return JSON.parse(fs.readFileSync(path.join(fixtureRoot, relative), "utf8")); +} + +function runArguments(args) { + const child = spawnSync(process.execPath, [validator, ...args], { cwd: repositoryRoot, encoding: "utf8" }); + let report = { failures: [], ok: false, parse_error: true }; + try { + report = JSON.parse(child.stdout); + } catch (error) { + if (!(error instanceof SyntaxError)) throw error; + report = { failures: [], ok: false, parse_error: true }; + } + return { report, status: child.status }; +} + +function run(file, profileRepositoryRoot) { + const args = ["--file", file, "--json"]; + if (profileRepositoryRoot) args.push("--profile-repository-root", profileRepositoryRoot); + return runArguments(args); +} + +function runPolicy(file) { + return runArguments(["--policy", file, "--json"]); +} + +function writeMutation(name, source, mutate) { + const value = structuredClone(readJson(source)); + mutate(value); + const file = path.join(tempRoot, `${name}.json`); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); + return file; +} + +function writePolicyMutation(root, name, mutate) { + const value = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json"), "utf8")); + mutate(value); + const file = path.join(root, `${name}.json`); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); + return file; +} + +function copyProfileRepository(name) { + const root = path.join(tempRoot, name); + const source = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); + const target = path.join(root, "design-engineering/reference-profiles/governed-local"); + copyTreeNoSymlinks(source, target); + return root; +} + +function editProfileJson(root, relative, mutate) { + const file = path.join(root, relative); + const value = JSON.parse(fs.readFileSync(file, "utf8")); + mutate(value); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +const fixtureManifest = JSON.parse(fs.readFileSync(path.join(fixtureRoot, "manifest.json"), "utf8")); +const invalidCases = fixtureManifest.invalid_records.map((entry) => [entry.file.replace(/\.json$/, ""), entry.file, entry.expected_code]); + +const tempRoot = fs.mkdtempSync(path.join(repositoryRoot, ".tmp-pr6-promotion-")); +const repositoryTempRoot = fs.mkdtempSync(path.join(repositoryRoot, ".tmp-pr6-profile-")); +const results = []; +try { + for (const [name, relative, expected] of invalidCases) { + const child = run(path.join(fixtureRoot, relative)); + const codes = child.report.failures.map((failure) => failure.code); + results.push({ actual: { codes, scaffold: child.report.scaffold === true, status: child.status }, expected, name, ok: child.status > 0 && codes.includes(expected) && child.report.scaffold !== true }); + } + + const editorial = writeMutation("editorial_counted", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "editorial", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "stylegallery", profile_record: "design-engineering/reference-profiles/governed-local/editorial/profile.json", relationship: "independent" }]; + }); + const terminal = writeMutation("terminal_counted", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "terminal", consumer_id: "terminal-reference-profile", count_toward_gate: true, organization_id: "stylegallery", profile_record: "design-engineering/reference-profiles/governed-local/terminal/profile.json", relationship: "independent" }]; + }); + const booleanReview = writeMutation("boolean_review", "valid-deferred-example.json", (value) => { value.review_independence = false; }); + const fakeIndependence = writeMutation("fake_independence", "valid-deferred-example.json", (value) => { + value.attestations = [ + { attested_by: "Synthetic owner", context_id: "one", consumer_id: "consumer-one", count_toward_gate: true, organization_id: "same-org", relationship: "independent" }, + { attested_by: "Synthetic owner", context_id: "two", consumer_id: "consumer-two", count_toward_gate: true, organization_id: "same-org", relationship: "independent" } + ]; + }); + const duplicateIdentity = writeMutation("duplicate_identity", "valid-deferred-example.json", (value) => { + const attestation = { attested_by: "Synthetic owner", context_id: "same", consumer_id: "same-consumer", count_toward_gate: true, organization_id: "same-org", relationship: "independent" }; + value.attestations = [attestation, structuredClone(attestation)]; + }); + const duplicateConsumer = writeMutation("duplicate_consumer", "valid-deferred-example.json", (value) => { + value.attestations = [ + { attested_by: "Synthetic owner A", context_id: "one", consumer_id: "same-consumer", count_toward_gate: true, organization_id: "organization-a", relationship: "independent" }, + { attested_by: "Synthetic owner B", context_id: "two", consumer_id: "same-consumer", count_toward_gate: true, organization_id: "organization-b", relationship: "independent" } + ]; + }); + const unknownConsumers = writeMutation("unknown_consumers", "valid-deferred-example.json", (value) => { + value.attestations = [ + { attested_by: "Synthetic owner A", context_id: "one", consumer_id: "unknown-one", count_toward_gate: true, organization_id: "organization-a", relationship: "independent" }, + { attested_by: "Synthetic owner B", context_id: "two", consumer_id: "unknown-two", count_toward_gate: true, organization_id: "organization-b", relationship: "independent" } + ]; + }); + const forgedOne = path.join(repositoryTempRoot, "forged-one.json"); + const forgedTwo = path.join(repositoryTempRoot, "forged-two.json"); + fs.writeFileSync(forgedOne, '{"fixture_independence":"independent","id":"forged-one"}\n'); + fs.writeFileSync(forgedTwo, '{"fixture_independence":"independent","id":"forged-two"}\n'); + const forgedProfiles = writeMutation("forged_profiles", "valid-deferred-example.json", (value) => { + value.attestations = [ + { attested_by: "Synthetic owner A", context_id: "one", consumer_id: "forged-one", count_toward_gate: true, organization_id: "organization-a", profile_record: path.relative(repositoryRoot, forgedOne), relationship: "independent" }, + { attested_by: "Synthetic owner B", context_id: "two", consumer_id: "forged-two", count_toward_gate: true, organization_id: "organization-b", profile_record: path.relative(repositoryRoot, forgedTwo), relationship: "independent" } + ]; + }); + const copiedProfileRoot = path.join(repositoryTempRoot, "copied"); + copyTreeNoSymlinks(path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local/editorial"), copiedProfileRoot); + const copiedUnregistered = writeMutation("copied_unregistered", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "copied", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "copied-org", profile_record: path.relative(repositoryRoot, path.join(copiedProfileRoot, "profile.json")), relationship: "independent" }]; + }); + const invalidRegisteredRoot = copyProfileRepository("invalid-registered-root"); + editProfileJson(invalidRegisteredRoot, "design-engineering/reference-profiles/governed-local/editorial/profile.json", (value) => { delete value.owner; }); + const invalidRegistered = writeMutation("invalid_registered", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "invalid", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "invalid-org", profile_record: "design-engineering/reference-profiles/governed-local/editorial/profile.json", relationship: "independent" }]; + }); + const duplicateRegistryRoot = copyProfileRepository("duplicate-registry-root"); + editProfileJson(duplicateRegistryRoot, "design-engineering/reference-profiles/governed-local/terminal/profile.json", (value) => { value.id = "editorial-reference-profile"; }); + const duplicateRegistryIdentity = writeMutation("duplicate_registry_identity", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "duplicate", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "duplicate-org", profile_record: "design-engineering/reference-profiles/governed-local/editorial/profile.json", relationship: "independent" }]; + }); + const silentRelabel = writeMutation("silent_relabel", "valid-deferred-example.json", (value) => { + value.transition = { from: "stable", to: "shared_experimental" }; + }); + const pathEscape = writeMutation("path_escape", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "escape", consumer_id: "escape", count_toward_gate: true, organization_id: "escape", profile_record: "../outside.json", relationship: "independent" }]; + }); + const profileMismatch = writeMutation("profile_mismatch", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "editorial", consumer_id: "different-profile", count_toward_gate: true, organization_id: "stylegallery", profile_record: "design-engineering/reference-profiles/governed-local/editorial/profile.json", relationship: "independent" }]; + }); + const profileLink = path.join(repositoryTempRoot, "profile.json"); + fs.symlinkSync(path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local/editorial/profile.json"), profileLink); + const profileSymlink = writeMutation("profile_symlink", "valid-deferred-example.json", (value) => { + value.attestations = [{ attested_by: "Synthetic owner", context_id: "editorial", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "stylegallery", profile_record: path.relative(repositoryRoot, profileLink), relationship: "independent" }]; + }); + const schemaExtra = writeMutation("schema_extra", "valid-deferred-example.json", (value) => { value.unexpected_promotion = true; }); + const acceptedDecision = writeMutation("accepted_decision", "valid-deferred-example.json", (value) => { value.decision = "accepted"; }); + const aestheticPromotion = writeMutation("aesthetic_promotion", "valid-deferred-example.json", (value) => { value.scope.aesthetic = true; }); + const breakingWithoutMigration = writeMutation("breaking_without_migration", "valid-deferred-example.json", (value) => { value.compatibility.breaking = true; value.migration.required = false; }); + const normativeWithoutRollback = writeMutation("normative_without_rollback", "valid-normative-bypass.json", (value) => { delete value.rollback; }); + const normativeEndedSupport = writeMutation("normative_ended_support", "valid-normative-bypass.json", (value) => { + value.support = { capacity: "No active support capacity", status: "ended" }; + }); + const normativeMissingSupport = writeMutation("normative_missing_support", "valid-normative-bypass.json", (value) => { delete value.support; }); + const normativeMissingCapacity = writeMutation("normative_missing_capacity", "valid-normative-bypass.json", (value) => { delete value.support.capacity; }); + const weakCapacityCases = ["0", "Not available", "Absent", "Lapsed", "Negative capacity"].map((capacity, index) => [ + `weak_capacity_${index + 1}`, + writeMutation(`weak_capacity_${index + 1}`, "valid-deferred-example.json", (value) => { value.support.capacity = capacity; }), + "promotion_support_required", + ]); + const stableWithoutCount = writeMutation("stable_without_count", "valid-deferred-example.json", (value) => { + value.transition = { from: "shared_experimental", to: "stable" }; + value.stable_scope_decision = true; + value.provenance.debt_resolved = true; + }); + const stableWithoutEvidence = writeMutation("stable_without_evidence", "valid-deferred-example.json", (value) => { + value.transition = { from: "shared_experimental", to: "stable" }; + value.stable_scope_decision = true; + value.provenance.debt_resolved = true; + value.evidence = [{ id: "regression", kind: "regression", reference: "synthetic-fixture:regression", status: "failed" }]; + }); + const duplicateKey = path.join(tempRoot, "duplicate_key.json"); + fs.writeFileSync(duplicateKey, fs.readFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), "utf8").replace('"decision": "deferred",', '"decision": "deferred",\n "decision": "rejected",')); + for (const [name, file, expected, profileRepositoryRoot] of [ + ["editorial_counted", editorial, "promotion_related_consumer_counted"], + ["terminal_counted", terminal, "promotion_related_consumer_counted"], + ["boolean_review", booleanReview, "review_independence_boolean"], + ["fake_independence", fakeIndependence, "promotion_independence_unproven"], + ["duplicate_identity", duplicateIdentity, "promotion_attestation_duplicate"], + ["duplicate_consumer", duplicateConsumer, "promotion_attestation_duplicate"], + ["unknown_consumers", unknownConsumers, "promotion_independence_unproven"], + ["forged_profiles", forgedProfiles, "promotion_independence_unproven"], + ["copied_unregistered", copiedUnregistered, "promotion_independence_unproven"], + ["invalid_registered", invalidRegistered, "promotion_independence_unproven", invalidRegisteredRoot], + ["duplicate_registry_identity", duplicateRegistryIdentity, "promotion_independence_unproven", duplicateRegistryRoot], + ["silent_relabel", silentRelabel, "promotion_stable_relabel_forbidden"], + ["path_escape", pathEscape, "promotion_profile_path_invalid"], + ["profile_mismatch", profileMismatch, "promotion_profile_identity_mismatch"], + ["profile_symlink", profileSymlink, "promotion_profile_path_symlink"], + ["schema_extra", schemaExtra, "promotion_schema_invalid"], + ["accepted_decision", acceptedDecision, "promotion_schema_invalid"], + ["aesthetic_promotion", aestheticPromotion, "promotion_schema_invalid"], + ["breaking_without_migration", breakingWithoutMigration, "promotion_migration_required"], + ["normative_without_rollback", normativeWithoutRollback, "promotion_schema_invalid"], + ["normative_ended_support", normativeEndedSupport, "promotion_support_required"], + ["normative_missing_support", normativeMissingSupport, "promotion_support_required"], + ["normative_missing_capacity", normativeMissingCapacity, "promotion_support_required"], + ["duplicate_key", duplicateKey, "promotion_json_invalid"], + ["stable_without_evidence", stableWithoutEvidence, "promotion_stable_evidence_required"], + ...weakCapacityCases, + ]) { + const child = run(file, profileRepositoryRoot); + const codes = child.report.failures.map((failure) => failure.code); + results.push({ actual: { codes, scaffold: child.report.scaffold === true, status: child.status }, expected, name, ok: child.status > 0 && codes.includes(expected) && child.report.scaffold !== true }); + } + + for (const relative of ["valid-deferred-example.json", "valid-normative-bypass.json"]) { + const child = run(path.join(fixtureRoot, relative)); + results.push({ actual: { scaffold: child.report.scaffold === true, status: child.status }, expected: "ok:true", name: relative, ok: child.status === 0 && child.report.ok === true && child.report.scaffold !== true }); + } + const stable = run(stableWithoutCount); + results.push({ actual: { scaffold: stable.report.scaffold === true, status: stable.status }, expected: "stable_without_numeric_threshold", name: "stable_without_count", ok: stable.status === 0 && stable.report.ok === true && stable.report.scaffold !== true }); + const canonicalPolicy = runPolicy(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json")); + results.push({ actual: { status: canonicalPolicy.status }, expected: "canonical_policy_valid", name: "canonical_policy", ok: canonicalPolicy.status === 0 && canonicalPolicy.report.ok === true }); + results.push(...testPromotionBoundaries({ fixtureRoot, repositoryRoot, runArguments, tempRoot })); + for (const [name, file, expected] of [ + ["stable_numeric_count", writePolicyMutation(tempRoot, "stable_numeric_count", (value) => { value.gateway.stable_numeric_threshold = 2; }), "promotion_stable_numeric_threshold_forbidden"], + ["bypass_overreach", writePolicyMutation(tempRoot, "bypass_overreach", (value) => { value.normative_bypass.waives.push("regression_proof"); }), "promotion_normative_bypass_overreach"], + ["fake_promotion_claim", writePolicyMutation(tempRoot, "fake_promotion_claim", (value) => { value.claims.promotion_occurred = true; }), "promotion_claim_forbidden"], + ["policy_unknown_field", writePolicyMutation(tempRoot, "policy_unknown_field", (value) => { value.accepted = true; }), "promotion_policy_shape_invalid"], + ["policy_nested_unknown", writePolicyMutation(tempRoot, "policy_nested_unknown", (value) => { value.gateway.stable_by_count = true; }), "promotion_policy_shape_invalid"], + ["policy_promotes_palette", writePolicyMutation(tempRoot, "policy_promotes_palette", (value) => { value.boundaries.promoted_content.push("palette"); }), "promotion_aesthetic_boundary_required"], + ["policy_promotes_alias", writePolicyMutation(tempRoot, "policy_promotes_alias", (value) => { value.boundaries.promoted_content.push("brand_identity"); }), "promotion_aesthetic_boundary_required"], + ["policy_promoted_non_string", writePolicyMutation(tempRoot, "policy_promoted_non_string", (value) => { value.boundaries.promoted_content = [42]; }), "promotion_aesthetic_boundary_required"], + ["policy_normative_aesthetic", writePolicyMutation(tempRoot, "policy_normative_aesthetic", (value) => { value.normative_bypass.categories = ["aesthetic"]; }), "promotion_normative_bypass_overreach"], + ["policy_lifecycle_incomplete", writePolicyMutation(tempRoot, "policy_lifecycle_incomplete", (value) => { value.lifecycle.failure_actions = ["deprecate_with_migration"]; }), "promotion_lifecycle_invalid"], + ["policy_excluded_non_string", writePolicyMutation(tempRoot, "policy_excluded_non_string", (value) => { value.boundaries.excluded_content.push(42); }), "promotion_aesthetic_boundary_required"], + ["policy_retains_non_string", writePolicyMutation(tempRoot, "policy_retains_non_string", (value) => { value.normative_bypass.retains.push(42); }), "promotion_normative_bypass_overreach"], + ["policy_verified_owner", writePolicyMutation(tempRoot, "policy_verified_owner", (value) => { value.owner.enforcement = "verified"; }), "promotion_policy_owner_invalid"], + ["policy_promoted_status", writePolicyMutation(tempRoot, "policy_promoted_status", (value) => { value.status = "promoted"; }), "promotion_claim_forbidden"], + ["policy_generated_mode", writePolicyMutation(tempRoot, "policy_generated_mode", (value) => { value.artifact_mode = "generated"; }), "promotion_policy_identity_invalid"], + ]) { + const child = runPolicy(file); + const codes = child.report.failures.map((failure) => failure.code); + results.push({ actual: { codes, status: child.status }, expected, name, ok: child.status > 0 && codes.includes(expected) }); + } +} finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); + fs.rmSync(repositoryTempRoot, { force: true, recursive: true }); +} + +const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); +const report = { failures, ok: failures.length === 0, results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +if (!report.ok) process.exitCode = 1; diff --git a/scripts/validate-consumer-reference.mjs b/scripts/validate-consumer-reference.mjs index 2a3db8c..e0ad0d0 100644 --- a/scripts/validate-consumer-reference.mjs +++ b/scripts/validate-consumer-reference.mjs @@ -4,6 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import Ajv2020 from "ajv/dist/2020.js"; import { isPlainObject, validateItemSchema } from "./consumer-reference-schema.mjs"; +import { canonicalGovernedProfilePaths } from "./governed-profile-registry.mjs"; import { validateReferenceProfile, validateReferenceProfileSet } from "./reference-profile-contract.mjs"; const root = process.cwd(); @@ -12,10 +13,7 @@ const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-re const validateFullItemSchema = new Ajv2020({ allErrors: true, strict: false }).compile(schema); const failures = []; const warnings = []; -const canonicalProfilePaths = [ - "design-engineering/reference-profiles/governed-local/editorial/profile.json", - "design-engineering/reference-profiles/governed-local/terminal/profile.json", -]; +const canonicalProfilePaths = canonicalGovernedProfilePaths; const defaultItems = [ "consumer-reference/fixtures/valid-experimental.json", "consumer-reference/fixtures/valid-deprecated.json", diff --git a/scripts/validate-domains.mjs b/scripts/validate-domains.mjs index e6b3861..58e85de 100644 --- a/scripts/validate-domains.mjs +++ b/scripts/validate-domains.mjs @@ -227,6 +227,22 @@ function checkReferenceDocuments() { } } +function checkPromotionBoundary() { + const relative = "DOMAINS.md"; + const content = stripFencedCodeBlocks(read(relative)); + const required = [ + "### Consumer Reference Promotion", + "applies only to consumer-local → shared-experimental invariant eligibility", + "Editorial and terminal are related examples in one fixture set", + "Shared stable has no numeric adoption threshold", + "Normative correctness may waive adoption count only", + "never silently relabeled experimental", + "Promotion records are JSON-only", + "zero adopter attestations", + ]; + for (const clause of required) if (!content.includes(clause)) failures.push(`${relative}: missing promotion boundary ${clause}`); +} + checkManifest(); read("quality/claim-records/stylegallery-multidomain-scope.md"); requireRootRoutes(); @@ -242,6 +258,7 @@ for (const domain of domains) { rejectUndeclaredDomainDocuments(); rejectOmoDependencies(); checkReferenceDocuments(); +checkPromotionBoundary(); const result = { ok: failures.length === 0, checkedDomains: domains.length, checkedLeaves, failures: [...new Set(failures)] }; if (json) console.log(JSON.stringify(result, null, 2)); diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 18309c5..0ef826f 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { componentStateWorkflowFailures } from "./component-state-workflow-contract.mjs"; +import { promotionGovernanceFailures } from "./promotion-governance-contract.mjs"; const args = new Set(process.argv.slice(2)); const json = args.has("--json"); @@ -31,6 +32,9 @@ const requiredCodeowners = [ "/platform-guides/ @changeroa", "/consumer-reference/ @changeroa", "/consumer-reference/baselines/ @changeroa", + "/consumer-reference/policies/ @changeroa", + "/consumer-reference/fixtures/promotion/ @changeroa", + "/consumer-reference/schema/promotion-rfc.schema.json @changeroa", "/tests/ @changeroa", "/playwright.config.mjs @changeroa", "/GUIDE.md @changeroa", @@ -93,6 +97,7 @@ function requireGovernanceMatrix() { "Quality gates and evidence", "Consumer reference contract", "Component-state evidence matrices", + "Shared promotion policy", "Proposed Chromium sentinel", "Domain manifest and scope decision", "Layout domain hub", @@ -156,8 +161,16 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-webpage-workflow.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/validate-consumer-reference.mjs"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/test-validate-consumer-reference.mjs"); + requireIncludes(".github/workflows/validate.yml", "node -c scripts/governed-profile-registry.mjs"); + requireIncludes(".github/workflows/validate.yml", "node -c scripts/promotion-attestation-contract.mjs"); + requireIncludes(".github/workflows/validate.yml", "node -c scripts/validate-promotion-rfc.mjs"); + requireIncludes(".github/workflows/validate.yml", "node -c scripts/test-validate-promotion-rfc.mjs"); requireIncludes(".github/workflows/validate.yml", "node scripts/validate-consumer-reference.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-consumer-reference.mjs --json"); + requireIncludes(".github/workflows/validate.yml", "node scripts/validate-promotion-rfc.mjs --json"); + requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-promotion-rfc.mjs --json"); + requireIncludes(".github/workflows/validate.yml", "consumer-reference/schema/promotion-rfc.schema.json"); + requireIncludes(".github/workflows/validate.yml", "consumer-reference/policies/shared-experimental.json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-consumer-reference-sentinel.mjs"); requireIncludes(".github/workflows/validate.yml", "node scripts/create-component-state-session.mjs"); requireIncludes(".github/workflows/validate.yml", "STATE_SESSION_RECEIPT="); @@ -235,6 +248,7 @@ requireImmutableActions(); requireRootLinks(); requireEvidenceMap(); requireSentinelProvenanceBoundary(); +failures.push(...promotionGovernanceFailures(read)); requireGeneratedWarning("CATALOG.md"); requireGeneratedWarning("patterns/index.md"); requireGeneratedWarning("patterns/stacking/index.md"); diff --git a/scripts/validate-promotion-rfc.mjs b/scripts/validate-promotion-rfc.mjs new file mode 100644 index 0000000..d075b21 --- /dev/null +++ b/scripts/validate-promotion-rfc.mjs @@ -0,0 +1,224 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv2020 from "ajv/dist/2020.js"; +import { isPlainObject } from "./consumer-reference-schema.mjs"; +import { validateGovernedProfileInventory } from "./governed-profile-registry.mjs"; +import { validatePromotionAttestations } from "./promotion-attestation-contract.mjs"; +import { loadPromotionFixtureInventory, resolvePromotionJsonFile } from "./promotion-fixture-inventory.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const options = { + files: [], + fixtureRoot: path.join(repositoryRoot, "consumer-reference/fixtures/promotion"), + json: false, + policies: [], + profileRepositoryRoot: repositoryRoot, + profileRepositoryRootExplicit: false, +}; +const argumentFailures = []; + +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (argument === "--file") { + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--file requires a JSON path", path: "" }); + else { + options.files.push(path.resolve(process.cwd(), value)); + index += 1; + } + } else if (argument === "--policy") { + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--policy requires a JSON path", path: "" }); + else { + options.policies.push(path.resolve(process.cwd(), value)); + index += 1; + } + } else if (argument === "--profile-repository-root") { + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--profile-repository-root requires a repository path", path: "" }); + else { + options.profileRepositoryRoot = path.resolve(process.cwd(), value); + options.profileRepositoryRootExplicit = true; + index += 1; + } + } else if (argument === "--promotion-fixture-root") { + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--promotion-fixture-root requires a repository path", path: "" }); + else { + options.fixtureRoot = path.resolve(process.cwd(), value); + index += 1; + } + } else argumentFailures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "" }); +} + +const failures = [...argumentFailures]; +const warnings = []; +const fixtureInventory = loadPromotionFixtureInventory({ fixtureRoot: options.fixtureRoot, repositoryRoot }); +failures.push(...fixtureInventory.failures); +let validateSchema = () => false; +try { + if (fixtureInventory.schema) validateSchema = new Ajv2020({ allErrors: true, strict: true }).compile(fixtureInventory.schema); +} catch (error) { + failures.push({ code: "promotion_fixture_inventory_invalid", message: error instanceof Error ? error.message : String(error), path: "consumer-reference/schema/promotion-rfc.schema.json" }); +} +const files = options.files.length > 0 + ? options.files + : options.policies.length > 0 + ? [] + : fixtureInventory.validFiles; +const policies = options.files.length === 0 && options.policies.length === 0 + ? [fixtureInventory.policyFile].filter(Boolean) + : options.policies; + +function finding(code, message, recordPath) { + return { code, message, path: recordPath }; +} + +function sameStringSet(actual, expected) { + return Array.isArray(actual) && actual.length === expected.length && actual.every((item) => typeof item === "string") && new Set(actual).size === expected.length && expected.every((item) => actual.includes(item)); +} + +function validateSemantics({ canonicalExample, value, recordPath, failures, warnings }) { + if (!isPlainObject(value)) return; + if (canonicalExample && (value.decision !== "deferred" || !Array.isArray(value.attestations) || value.attestations.length !== 0)) { + failures.push(finding("promotion_canonical_example_invalid", "canonical examples must remain deferred with zero attestations", recordPath)); + } + if (typeof value.review_independence === "boolean") failures.push(finding("review_independence_boolean", "review_independence must use the canonical single_account enum", recordPath)); + if (!isPlainObject(value.owner) || typeof value.owner.migration !== "string" || !isPlainObject(value.migration)) { + failures.push(finding("promotion_owner_migration_required", "promotion proposals require migration ownership and a migration plan", recordPath)); + } + if (value.compatibility?.breaking === true && value.migration?.required !== true) { + failures.push(finding("promotion_migration_required", "breaking compatibility changes require an enabled migration plan", recordPath)); + } + if (value.eligibility_basis === "consumer_count_only") failures.push(finding("promotion_count_only_forbidden", "consumer count cannot replace evidence and lifecycle duties", recordPath)); + if (isPlainObject(value.support) && value.transition?.to === "stable" && value.support.status === "ended") { + failures.push(finding("stable_support_ended", "stable promotion requires active support", recordPath)); + } + if (value.support?.status !== "active" || value.support?.capacity !== "committed") { + failures.push(finding("promotion_support_required", "promotion eligibility requires active, affirmative support capacity", recordPath)); + } + if (value.transition?.from === "stable" && value.transition?.to === "shared_experimental") { + failures.push(finding("promotion_stable_relabel_forbidden", "a stable contract must be maintained, rolled back, or deprecated with migration", recordPath)); + } + const transition = `${value.transition?.from ?? ""}->${value.transition?.to ?? ""}`; + if (!["consumer_local->shared_experimental", "shared_experimental->stable", "stable->shared_experimental"].includes(transition)) { + failures.push(finding("promotion_transition_invalid", `${transition} is not a governed promotion transition`, recordPath)); + } + const evidence = Array.isArray(value.evidence) ? value.evidence.filter(isPlainObject) : []; + const evidenceIds = evidence.map((entry) => entry.id).filter((id) => typeof id === "string"); + if (new Set(evidenceIds).size !== evidenceIds.length) failures.push(finding("promotion_evidence_duplicate", "evidence IDs must be unique", recordPath)); + if (evidence.some((entry) => entry.kind === "blocking" && entry.status === "failed")) { + failures.push(finding("promotion_blocking_evidence_failed", "failed blocking evidence prevents eligibility", recordPath)); + } + const regressionPassed = evidence.some((entry) => entry.kind === "regression" && entry.status === "passed"); + const claimScopedPassed = evidence.some((entry) => entry.kind === "claim_scoped" && entry.status === "passed"); + if (value.normative_bypass === "normative_correctness" && !regressionPassed) { + failures.push(finding("promotion_normative_regression_required", "normative bypass waives adoption count only and requires passing regression evidence", recordPath)); + } + if (value.normative_bypass === "normative_correctness" && value.scope?.kind !== "normative_correctness") { + failures.push(finding("promotion_normative_scope_required", "normative bypass requires a normative correctness scope", recordPath)); + } + if (value.transition?.to === "stable" && (!claimScopedPassed || !regressionPassed)) { + failures.push(finding("promotion_stable_evidence_required", "stable requires passing claim-scoped and regression evidence", recordPath)); + } + const attestationResult = validatePromotionAttestations({ inventory: governedProfileInventory, profileRoot: options.profileRepositoryRoot, recordPath, value }); + failures.push(...attestationResult.failures); + const counted = attestationResult.eligibleCount; + if (value.transition?.from === "consumer_local" && value.transition?.to === "shared_experimental" && value.normative_bypass === "none" && counted < 2) { + warnings.push(finding("promotion_consumer_count_below_gateway", "deferred proposal has fewer than two independent consumers", recordPath)); + } + if (value.transition?.to === "stable" && (value.stable_scope_decision !== true || value.provenance?.debt_resolved !== true)) { + failures.push(finding("promotion_stable_readiness_required", "stable requires explicit scope decision and resolved provenance debt; no numeric count substitutes", recordPath)); + } +} + +function validatePolicy(value, recordPath, failures) { + if (!isPlainObject(value)) { + failures.push(finding("promotion_policy_shape_invalid", "promotion policy must be an object", recordPath)); + return; + } + const allowed = ["artifact_mode", "boundaries", "claims", "gateway", "id", "lifecycle", "normative_bypass", "owner", "review_independence", "schema_version", "status"]; + if (Object.keys(value).some((key) => !allowed.includes(key))) failures.push(finding("promotion_policy_shape_invalid", "promotion policy contains an unknown field", recordPath)); + const nestedShapes = [ + [value.boundaries, ["excluded_content", "promoted_content"], "boundaries"], + [value.claims, ["adopter_attestations", "promotion_occurred"], "claims"], + [value.gateway, ["applies_to", "minimum_independent_consumers", "stable_numeric_threshold", "transition"], "gateway"], + [value.gateway?.transition, ["from", "to"], "gateway.transition"], + [value.lifecycle, ["failure_actions", "stable_relabel"], "lifecycle"], + [value.normative_bypass, ["categories", "retains", "waives"], "normative_bypass"], + [value.owner, ["enforcement", "name"], "owner"], + ]; + for (const [candidate, keys, label] of nestedShapes) { + if (!isPlainObject(candidate) || Object.keys(candidate).some((key) => !keys.includes(key))) failures.push(finding("promotion_policy_shape_invalid", `promotion policy ${label} shape is invalid`, recordPath)); + } + if (value.review_independence !== "single_account") failures.push(finding(typeof value.review_independence === "boolean" ? "review_independence_boolean" : "promotion_policy_shape_invalid", "policy review_independence must be single_account", recordPath)); + if (value.gateway?.transition?.from !== "consumer_local" || value.gateway?.transition?.to !== "shared_experimental" || value.gateway?.applies_to !== "invariant_eligibility_only" || value.gateway?.minimum_independent_consumers !== 2) { + failures.push(finding("promotion_gateway_invalid", "the >=2 gateway applies only to consumer-local invariant eligibility for shared-experimental", recordPath)); + } + if (value.gateway?.stable_numeric_threshold !== null) failures.push(finding("promotion_stable_numeric_threshold_forbidden", "stable has no numeric adoption threshold", recordPath)); + const categories = ["accessibility", "correctness", "security", "standards"]; + const retained = ["compatibility", "deprecation", "migration", "owner_acceptance", "regression_proof", "rollback", "scope_control", "support_capacity"]; + if (!sameStringSet(value.normative_bypass?.categories, categories) || !sameStringSet(value.normative_bypass?.waives, ["adoption_count"]) || !sameStringSet(value.normative_bypass?.retains, retained)) { + failures.push(finding("promotion_normative_bypass_overreach", "normative bypass may waive adoption count only", recordPath)); + } + if (value.claims?.promotion_occurred !== false || value.claims?.adopter_attestations !== 0) failures.push(finding("promotion_claim_forbidden", "the policy records no promotion and zero adopter attestations", recordPath)); + if (value.status !== "active") failures.push(finding("promotion_claim_forbidden", "the policy status must remain active governance, not a promoted decision", recordPath)); + if (value.schema_version !== "1.0" || value.id !== "shared-experimental-policy" || value.artifact_mode !== "schema_only") failures.push(finding("promotion_policy_identity_invalid", "promotion policy identity and artifact mode are canonical", recordPath)); + if (value.owner?.enforcement !== "placeholder" || value.owner?.name !== "Repository governance owner") failures.push(finding("promotion_policy_owner_invalid", "promotion policy must preserve placeholder owner truth", recordPath)); + const excluded = ["component_skin", "imagery", "motion_character", "palette", "typography"]; + const promoted = ["claim_scoped_evidence", "invariant_behavior", "invariant_semantics"]; + if (!sameStringSet(value.boundaries?.excluded_content, excluded)) { + failures.push(finding("promotion_aesthetic_boundary_required", "aesthetic identity must remain excluded from promotion", recordPath)); + } + if (!sameStringSet(value.boundaries?.promoted_content, promoted)) failures.push(finding("promotion_aesthetic_boundary_required", "promoted content must be the invariant-only canonical set", recordPath)); + if (value.lifecycle?.stable_relabel !== "forbidden" || !sameStringSet(value.lifecycle?.failure_actions, ["deprecate_with_migration", "maintain", "restrict", "rollback"])) { + failures.push(finding("promotion_lifecycle_invalid", "failed stable contracts require explicit lifecycle action and migration", recordPath)); + } +} + +const governedProfileInventory = validateGovernedProfileInventory({ root: options.profileRepositoryRoot }); +if (options.profileRepositoryRootExplicit && governedProfileInventory.failures.length > 0) { + failures.push(finding("promotion_profile_registry_invalid", "explicit governed profile repository must fully validate", "")); +} +let checked = 0; +for (const file of files) { + const recordPath = path.relative(repositoryRoot, file) || file; + const resolvedFile = resolvePromotionJsonFile({ file, repositoryRoot }, failures); + if (!resolvedFile) continue; + let value; + try { + value = parseStrictJson(fs.readFileSync(resolvedFile, "utf8")); + checked += 1; + } catch (error) { + failures.push(finding("promotion_json_invalid", error instanceof Error ? error.message : "promotion JSON is invalid", recordPath)); + continue; + } + validateSemantics({ canonicalExample: fixtureInventory.validFiles.includes(resolvedFile), failures, recordPath, value, warnings }); + if (!validateSchema(value)) { + for (const error of validateSchema.errors ?? []) failures.push(finding("promotion_schema_invalid", `${error.instancePath || "/"} ${error.message}`, recordPath)); + } +} +for (const file of policies) { + const recordPath = path.relative(repositoryRoot, file) || file; + const resolvedFile = resolvePromotionJsonFile({ file, repositoryRoot }, failures); + if (!resolvedFile) continue; + try { + const value = parseStrictJson(fs.readFileSync(resolvedFile, "utf8")); + checked += 1; + validatePolicy(value, recordPath, failures); + } catch (error) { + failures.push(finding("promotion_json_invalid", error instanceof Error ? error.message : "promotion policy JSON is invalid", recordPath)); + } +} + +const uniqueFailures = [...new Map(failures.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()]; +const result = { checked, failures: uniqueFailures, ok: uniqueFailures.length === 0, warnings }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`ok: ${checked} promotion proposal records\n`); +else process.stderr.write(`${result.failures.map((entry) => `${entry.code}: ${entry.path}: ${entry.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; From 36539dfd03f002a9460400908e19cbafdd555924 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 06:22:38 +0900 Subject: [PATCH 10/44] Refactor validator fixture harnesses --- scripts/baseline-schema-parity.mjs | 40 +++++++++ scripts/consumer-reference-case-runner.mjs | 93 ++++++++++++++++++++ scripts/test-validate-baseline-manifest.mjs | 40 +-------- scripts/test-validate-consumer-reference.mjs | 92 +------------------ 4 files changed, 141 insertions(+), 124 deletions(-) create mode 100644 scripts/baseline-schema-parity.mjs create mode 100644 scripts/consumer-reference-case-runner.mjs diff --git a/scripts/baseline-schema-parity.mjs b/scripts/baseline-schema-parity.mjs new file mode 100644 index 0000000..f32d8d9 --- /dev/null +++ b/scripts/baseline-schema-parity.mjs @@ -0,0 +1,40 @@ +import { BASELINE_METADATA_SHA256, BASELINE_REFERENCE } from "./baseline-contract.mjs"; + +const executionRepositories = ["ark-jo/StyleGallery", "changeroa/StyleGallery"]; + +export function baselineSchemaParity(schema, canonical, completedEvidence) { + const committedSchema = schema.properties.committed_ci.oneOf.find((entry) => entry.type === "object"); + const externalSchema = committedSchema.properties.external_verification?.oneOf?.find((entry) => entry.type === "object") ?? {}; + const artifactSchema = externalSchema.properties?.artifact ?? {}; + const relationshipSchema = externalSchema.properties?.repository_relationship ?? {}; + const runSchema = schema.properties.runs.items; + const runContains = schema.allOf[0].else.properties.runs.allOf; + return schema.additionalProperties === false + && committedSchema.additionalProperties === false + && schema.properties.environment.additionalProperties === false + && Object.entries(canonical.environment).every(([key, value]) => key === "viewport" + ? JSON.stringify(schema.properties.environment.properties.viewport.properties) === JSON.stringify({ width: { const: value.width }, height: { const: value.height } }) + : schema.properties.environment.properties[key].const === value) + && schema.properties.reference.additionalProperties === false + && schema.properties.reference.properties.source.additionalProperties === false + && schema.properties.reference.properties.source.properties.sha256.const === BASELINE_REFERENCE.source.sha256 + && schema.properties.reference.properties.baseline.properties.sha256.const === BASELINE_REFERENCE.baseline.sha256 + && runSchema.additionalProperties === false + && runSchema.properties.png_sha256.const === BASELINE_REFERENCE.baseline.sha256 + && runSchema.properties.metadata_sha256.const === BASELINE_METADATA_SHA256 + && committedSchema.properties.repository.const === "changeroa/StyleGallery" + && JSON.stringify(committedSchema.properties.execution_repository?.enum ?? []) === JSON.stringify(executionRepositories) + && externalSchema.additionalProperties === false + && artifactSchema.additionalProperties === false + && relationshipSchema.additionalProperties === false + && artifactSchema.properties?.api_digest?.const === completedEvidence.artifactApiDigest + && artifactSchema.properties?.id?.const === completedEvidence.artifactId + && artifactSchema.properties?.name?.const === completedEvidence.artifactName + && artifactSchema.properties?.size_in_bytes?.const === completedEvidence.artifactSize + && artifactSchema.properties?.expires_at?.const === completedEvidence.artifactExpiresAt + && externalSchema.properties?.source?.const === completedEvidence.source + && externalSchema.properties?.verified_at?.const === completedEvidence.verifiedAt + && JSON.stringify(runContains.map((entry) => entry.contains.properties.run.const).sort((left, right) => left - right)) === JSON.stringify(Array.from({ length: 20 }, (_, index) => index + 1)) + && runContains.every((entry) => entry.minContains === 1 && entry.maxContains === 1) + && JSON.stringify([...committedSchema.required].sort()) === JSON.stringify(["artifact_name", "checkout_sha", "execution_repository", "external_verification", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"].sort()); +} diff --git a/scripts/consumer-reference-case-runner.mjs b/scripts/consumer-reference-case-runner.mjs new file mode 100644 index 0000000..eaac5d8 --- /dev/null +++ b/scripts/consumer-reference-case-runner.mjs @@ -0,0 +1,93 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { validateItemSchema } from "./consumer-reference-schema.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const validator = path.join(repositoryRoot, "scripts", "validate-consumer-reference.mjs"); +const itemPath = "consumer-reference/fixtures/item.json"; + +function findingCodes(output) { + if (!Array.isArray(output.failures)) return []; + return output.failures.flatMap((failure) => { + if (typeof failure === "string") return [failure]; + return typeof failure?.code === "string" ? [failure.code] : []; + }); +} + +function writeFixture(testCase, baseItem) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `stylegallery-consumer-reference-${testCase.name}-`)); + let externalRoot; + const item = Object.hasOwn(testCase, "value") ? structuredClone(testCase.value) : structuredClone(baseItem); + testCase.mutate?.(item); + const files = { + "CATALOG.md": "# Catalog\n", + [itemPath]: `${JSON.stringify(item, null, 2)}\n`, + "layout/index.md": testCase.layout ?? "# Layout\n", + "patterns/index.md": "# Patterns\n", + "quality/handoff.md": "Implementation handoff:\nConsumer reference: not_applicable\nConsumer reference reason: This fixture has no consumer-specific reference record.\n", + "scripts/pattern-data.mjs": testCase.patternData ?? "export const patterns = [];\n", + ...(testCase.extraFiles ?? {}), + }; + for (const [relative, content] of Object.entries(files)) { + const target = path.join(root, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content); + } + if (testCase.link) { + const link = path.join(root, "consumer-reference", "fixtures", "redirect.json"); + if (testCase.link === "inside") { + fs.symlinkSync(path.join(root, itemPath), link); + } else { + externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-reference-outside-")); + const external = path.join(externalRoot, "record.json"); + fs.writeFileSync(external, "{}\n"); + fs.symlinkSync(external, link); + } + } + if (testCase.itemLink) { + const itemTarget = path.join(root, itemPath); + fs.rmSync(itemTarget); + if (testCase.itemLink === "inside") { + const actual = path.join(root, "consumer-reference", "fixtures", "actual-item.json"); + fs.writeFileSync(actual, `${JSON.stringify(item, null, 2)}\n`); + fs.symlinkSync(actual, itemTarget); + } else { + externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-reference-item-outside-")); + const external = path.join(externalRoot, "item.json"); + fs.writeFileSync(external, `${JSON.stringify(item, null, 2)}\n`); + fs.symlinkSync(external, itemTarget); + } + } + return { externalRoot, item, root }; +} + +export function makeConsumerReferenceCaseRunner({ baseItem, schema }) { + return function runCase(testCase) { + const fixture = writeFixture(testCase, baseItem); + try { + const child = spawnSync(process.execPath, [validator, "--item", itemPath, "--json"], { + cwd: fixture.root, + encoding: "utf8", + }); + const output = JSON.parse(child.stdout); + const codes = findingCodes(output); + const accepted = child.status === 0 && output.ok && output.scaffold !== true; + const rejected = child.status !== 0 && !output.ok && codes.includes(testCase.expect); + const schemaCodes = validateItemSchema(fixture.item, schema).map((finding) => finding.code); + const schemaParity = testCase.schemaValid === undefined || (schemaCodes.length === 0) === testCase.schemaValid; + return { + actual: { codes, ok: output.ok, scaffold: output.scaffold === true, schemaCodes, status: child.status }, + expected: testCase.expect ?? "ok:true and exit:0", + name: testCase.name, + ok: (testCase.expect === null ? accepted : rejected) && schemaParity, + rules: testCase.rules ?? [], + }; + } finally { + fs.rmSync(fixture.root, { force: true, recursive: true }); + if (fixture.externalRoot) fs.rmSync(fixture.externalRoot, { force: true, recursive: true }); + } + }; +} diff --git a/scripts/test-validate-baseline-manifest.mjs b/scripts/test-validate-baseline-manifest.mjs index 8cde0a3..082a7f5 100644 --- a/scripts/test-validate-baseline-manifest.mjs +++ b/scripts/test-validate-baseline-manifest.mjs @@ -4,15 +4,16 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { BASELINE_METADATA_SHA256, BASELINE_REFERENCE, sha256 } from "./baseline-contract.mjs"; +import { baselineSchemaParity } from "./baseline-schema-parity.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(repositoryRoot, "scripts", "validate-baseline-manifest.mjs"); const canonical = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "baselines", "calibration.json"), "utf8")); const canonicalManifest = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "baselines", "manifest.json"), "utf8")); const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "calibration-record.schema.json"), "utf8")); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-calibration-")); -const executionRepositories = ["ark-jo/StyleGallery", "changeroa/StyleGallery"]; const completedEvidence = { artifactId: "8283099324", artifactName: "chromium-sentinel-calibration-29260372260-18229be570766d3b42f5600955120bfcba690b76", @@ -116,40 +117,7 @@ const cases = [ const results = []; try { - const committedSchema = schema.properties.committed_ci.oneOf.find((entry) => entry.type === "object"); - const externalSchema = committedSchema.properties.external_verification?.oneOf?.find((entry) => entry.type === "object") ?? {}; - const artifactSchema = externalSchema.properties?.artifact ?? {}; - const relationshipSchema = externalSchema.properties?.repository_relationship ?? {}; - const runSchema = schema.properties.runs.items; - const runContains = schema.allOf[0].else.properties.runs.allOf; - const schemaParity = schema.additionalProperties === false - && committedSchema.additionalProperties === false - && schema.properties.environment.additionalProperties === false - && Object.entries(canonical.environment).every(([key, value]) => key === "viewport" - ? JSON.stringify(schema.properties.environment.properties.viewport.properties) === JSON.stringify({ width: { const: value.width }, height: { const: value.height } }) - : schema.properties.environment.properties[key].const === value) - && schema.properties.reference.additionalProperties === false - && schema.properties.reference.properties.source.additionalProperties === false - && schema.properties.reference.properties.source.properties.sha256.const === BASELINE_REFERENCE.source.sha256 - && schema.properties.reference.properties.baseline.properties.sha256.const === BASELINE_REFERENCE.baseline.sha256 - && runSchema.additionalProperties === false - && runSchema.properties.png_sha256.const === BASELINE_REFERENCE.baseline.sha256 - && runSchema.properties.metadata_sha256.const === BASELINE_METADATA_SHA256 - && committedSchema.properties.repository.const === "changeroa/StyleGallery" - && JSON.stringify(committedSchema.properties.execution_repository?.enum ?? []) === JSON.stringify(executionRepositories) - && externalSchema.additionalProperties === false - && artifactSchema.additionalProperties === false - && relationshipSchema.additionalProperties === false - && artifactSchema.properties?.api_digest?.const === completedEvidence.artifactApiDigest - && artifactSchema.properties?.id?.const === completedEvidence.artifactId - && artifactSchema.properties?.name?.const === completedEvidence.artifactName - && artifactSchema.properties?.size_in_bytes?.const === completedEvidence.artifactSize - && artifactSchema.properties?.expires_at?.const === completedEvidence.artifactExpiresAt - && externalSchema.properties?.source?.const === completedEvidence.source - && externalSchema.properties?.verified_at?.const === completedEvidence.verifiedAt - && JSON.stringify(runContains.map((entry) => entry.contains.properties.run.const).sort((left, right) => left - right)) === JSON.stringify(Array.from({ length: 20 }, (_, index) => index + 1)) - && runContains.every((entry) => entry.minContains === 1 && entry.maxContains === 1) - && JSON.stringify([...committedSchema.required].sort()) === JSON.stringify(["artifact_name", "checkout_sha", "execution_repository", "external_verification", "head_sha", "raw_evidence_sha256", "repository", "run_attempt", "run_id", "sha", "workflow"].sort()); + const schemaParity = baselineSchemaParity(schema, canonical, completedEvidence); results.push({ actual: { schemaParity }, expected: "recursive schema/runtime identity parity", name: "schema_runtime_parity", ok: schemaParity }); const validFixture = path.join(tempRoot, "valid-completed.json"); fs.writeFileSync(validFixture, `${JSON.stringify(completeRecord(), null, 2)}\n`); diff --git a/scripts/test-validate-consumer-reference.mjs b/scripts/test-validate-consumer-reference.mjs index 544a23e..f887bc3 100644 --- a/scripts/test-validate-consumer-reference.mjs +++ b/scripts/test-validate-consumer-reference.mjs @@ -1,12 +1,12 @@ #!/usr/bin/env node import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import { validateItemSchema } from "./consumer-reference-schema.mjs"; +import { fileURLToPath } from "node:url"; +import { makeConsumerReferenceCaseRunner } from "./consumer-reference-case-runner.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(repositoryRoot, "scripts", "validate-consumer-reference.mjs"); const itemPath = "consumer-reference/fixtures/item.json"; const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "item.schema.json"), "utf8")); @@ -161,91 +161,7 @@ const expectedSchemaRuleNames = [ "generated_records.uniqueItems", ]; -function cloneItem() { - return JSON.parse(JSON.stringify(baseItem)); -} - -function findingCodes(output) { - if (!Array.isArray(output.failures)) return []; - return output.failures.flatMap((failure) => { - if (typeof failure === "string") return [failure]; - return typeof failure?.code === "string" ? [failure.code] : []; - }); -} - -function writeFixture(testCase) { - const root = fs.mkdtempSync(path.join(os.tmpdir(), `stylegallery-consumer-reference-${testCase.name}-`)); - let externalRoot; - const item = Object.hasOwn(testCase, "value") ? structuredClone(testCase.value) : cloneItem(); - testCase.mutate?.(item); - const files = { - "CATALOG.md": "# Catalog\n", - [itemPath]: `${JSON.stringify(item, null, 2)}\n`, - "layout/index.md": testCase.layout ?? "# Layout\n", - "patterns/index.md": "# Patterns\n", - "quality/handoff.md": "Implementation handoff:\nConsumer reference: not_applicable\nConsumer reference reason: This fixture has no consumer-specific reference record.\n", - "scripts/pattern-data.mjs": testCase.patternData ?? "export const patterns = [];\n", - ...(testCase.extraFiles ?? {}), - }; - for (const [relative, content] of Object.entries(files)) { - const target = path.join(root, relative); - fs.mkdirSync(path.dirname(target), { recursive: true }); - fs.writeFileSync(target, content); - } - if (testCase.link) { - const link = path.join(root, "consumer-reference", "fixtures", "redirect.json"); - if (testCase.link === "inside") { - fs.symlinkSync(path.join(root, itemPath), link); - } else { - externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-reference-outside-")); - const external = path.join(externalRoot, "record.json"); - fs.writeFileSync(external, "{}\n"); - fs.symlinkSync(external, link); - } - } - if (testCase.itemLink) { - const itemTarget = path.join(root, itemPath); - fs.rmSync(itemTarget); - if (testCase.itemLink === "inside") { - const actual = path.join(root, "consumer-reference", "fixtures", "actual-item.json"); - fs.writeFileSync(actual, `${JSON.stringify(item, null, 2)}\n`); - fs.symlinkSync(actual, itemTarget); - } else { - externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-reference-item-outside-")); - const external = path.join(externalRoot, "item.json"); - fs.writeFileSync(external, `${JSON.stringify(item, null, 2)}\n`); - fs.symlinkSync(external, itemTarget); - } - } - return { externalRoot, item, root }; -} - -function runCase(testCase) { - const fixture = writeFixture(testCase); - try { - const child = spawnSync(process.execPath, [validator, "--item", itemPath, "--json"], { - cwd: fixture.root, - encoding: "utf8", - }); - const output = JSON.parse(child.stdout); - const codes = findingCodes(output); - const accepted = child.status === 0 && output.ok && output.scaffold !== true; - const rejected = child.status !== 0 && !output.ok && codes.includes(testCase.expect); - const schemaCodes = validateItemSchema(fixture.item, schema).map((finding) => finding.code); - const schemaParity = testCase.schemaValid === undefined || (schemaCodes.length === 0) === testCase.schemaValid; - return { - actual: { codes, ok: output.ok, scaffold: output.scaffold === true, schemaCodes, status: child.status }, - expected: testCase.expect ?? "ok:true and exit:0", - name: testCase.name, - ok: (testCase.expect === null ? accepted : rejected) && schemaParity, - rules: testCase.rules ?? [], - }; - } finally { - fs.rmSync(fixture.root, { force: true, recursive: true }); - if (fixture.externalRoot) fs.rmSync(fixture.externalRoot, { force: true, recursive: true }); - } -} - +const runCase = makeConsumerReferenceCaseRunner({ baseItem, schema }); const results = cases.map(runCase); const failures = results .filter((result) => !result.ok) From 78bfb0bd1f1cfb07d8c210b4642c17b0d91f833c Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 06:22:48 +0900 Subject: [PATCH 11/44] Improve reference evidence discoverability --- .../governed-local/index.md | 13 ++++++-- quality/evidence/executable-evidence.md | 1 + scripts/test-validate-ia.mjs | 30 +++++++++++++++++-- scripts/validate-ia.mjs | 13 ++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/design-engineering/reference-profiles/governed-local/index.md b/design-engineering/reference-profiles/governed-local/index.md index a0b6f7f..59b2f32 100644 --- a/design-engineering/reference-profiles/governed-local/index.md +++ b/design-engineering/reference-profiles/governed-local/index.md @@ -6,11 +6,20 @@ Their token and foundation values intentionally differ. Their shared relationshi ## Profile Records -- `editorial/profile.json` describes a warm publication identity. -- `terminal/profile.json` describes a dark operator-console identity. +- [Editorial profile](editorial/profile.json) describes a warm publication identity. +- [Terminal profile](terminal/profile.json) describes a dark operator-console identity. The sibling `tokens.dtcg.json` files use the restricted portable token subset. The sibling `local-foundations.json` files keep identity bindings local to each profile. +## Generated Evidence Routes + +The generated matrices expose the declared component states, keyboard expectations, and evidence coverage without making a profile the default. + +| Profile | State matrix | Keyboard matrix | Evidence coverage | +| --- | --- | --- | --- | +| Editorial | [Editorial state matrix](editorial/generated/state-matrix.md) | [Editorial keyboard matrix](editorial/generated/keyboard-matrix.md) | [Editorial evidence coverage](editorial/generated/evidence-coverage.md) | +| Terminal | [Terminal state matrix](terminal/generated/state-matrix.md) | [Terminal keyboard matrix](terminal/generated/keyboard-matrix.md) | [Terminal evidence coverage](terminal/generated/evidence-coverage.md) | + ## IA Navigation Parent: [Reference Profiles](../index.md). diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index b5ea4d3..8db2787 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -33,6 +33,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract. | `scripts/validate-promotion-rfc.mjs` and `scripts/test-validate-promotion-rfc.mjs` | `node scripts/validate-promotion-rfc.mjs --json`; `node scripts/test-validate-promotion-rfc.mjs --json` | The manifest-declared policy and two canonical examples pass with deferred decisions, zero attestations, exact support commitment, and no promotion claim. | Manifest-declared invalid fixtures plus forged, unregistered, duplicate, path, extension, support, evidence, canonical-drift, and lifecycle mutations exit non-zero with named findings. | Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision; the current canonical inventory supplies no real independent consumers. | | Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | | The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | +| Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles. | `scripts/create-component-state-session.mjs`, `scripts/finalize-component-state-evidence.mjs`, `scripts/validate-component-state.mjs`, and the component-state source, contract, artifact, generator, and sentinel harnesses. | `npm run test:component-state`; `npm run validate:component-state`; run the digest-pinned component-state browser capture and `npm run test:component-state:runtime-negative`. | Editorial and terminal each declare default, hover, focus-visible, active, and disabled scenarios; the generated state, keyboard, and evidence matrices match their records; one clean capture session binds the browser artifacts to the governed source inventory. | Missing or altered source bindings, invalid state/profile records, scenario drift, malformed or replayed runtime artifacts, hash/count mismatch, insufficient focus clearance, and sentinel mutations must exit non-zero with named findings. | Proves the declared examples and captured channels agree for the pinned browser environment. It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval; synthetic negative fixtures are not authenticated provenance. | ## Validator Severity Contract diff --git a/scripts/test-validate-ia.mjs b/scripts/test-validate-ia.mjs index 74352ff..d25c5e8 100644 --- a/scripts/test-validate-ia.mjs +++ b/scripts/test-validate-ia.mjs @@ -4,9 +4,20 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-ia.mjs"); +const governedProfileRoutes = [ + ["Editorial profile", "editorial/profile.json"], + ["Editorial state matrix", "editorial/generated/state-matrix.md"], + ["Editorial keyboard matrix", "editorial/generated/keyboard-matrix.md"], + ["Editorial evidence coverage", "editorial/generated/evidence-coverage.md"], + ["Terminal profile", "terminal/profile.json"], + ["Terminal state matrix", "terminal/generated/state-matrix.md"], + ["Terminal keyboard matrix", "terminal/generated/keyboard-matrix.md"], + ["Terminal evidence coverage", "terminal/generated/evidence-coverage.md"], +]; const baseFiles = { "README.md": [ @@ -76,7 +87,15 @@ const baseFiles = { "design-engineering/index.md": "# Design Engineering\n\n- [Craft](craft.md)\n- [Reference Profiles](reference-profiles/index.md)\n", "design-engineering/craft.md": leaf("Craft", "index.md", "../platform-guides/index.md"), "design-engineering/reference-profiles/index.md": "# Reference Profiles\n\n- [Governed Local Profiles](governed-local/index.md)\n- [External Adaptation](external-adaptation/index.md)\n\nParent: [Design Engineering](../index.md).\nNext: [Governed Local Profiles](governed-local/index.md).\n", - "design-engineering/reference-profiles/governed-local/index.md": "# Governed Local Profiles\n\nParent: [Reference Profiles](../index.md).\nNext: [External Adaptation](../external-adaptation/index.md).\n", + "design-engineering/reference-profiles/governed-local/index.md": [ + "# Governed Local Profiles", + "", + ...governedProfileRoutes.map(([label, target]) => `- [${label}](${target})`), + "", + "Parent: [Reference Profiles](../index.md).", + "Next: [External Adaptation](../external-adaptation/index.md).", + "", + ].join("\n"), "design-engineering/reference-profiles/external-adaptation/index.md": "# External Adaptation\n\nParent: [Reference Profiles](../index.md).\nNext: [Platform Guides](../../../platform-guides/index.md).\n", "platform-guides/index.md": "# Platform Guides\n\n- [Apple](apple.md)\n", "platform-guides/apple.md": leaf("Apple", "index.md", "../layout/index.md"), @@ -141,6 +160,13 @@ const cases = [ }, expect: "design-engineering/index.md: missing [Reference Profiles](reference-profiles/index.md)", }, + ...governedProfileRoutes.map(([label, target]) => ({ + name: `missing_governed_route_${target.replace(/[^a-z]+/g, "_").replace(/^_|_$/g, "")}`, + mutate: { + "design-engineering/reference-profiles/governed-local/index.md": baseFiles["design-engineering/reference-profiles/governed-local/index.md"].replace(`- [${label}](${target})\n`, ""), + }, + expect: `design-engineering/reference-profiles/governed-local/index.md: missing [${label}](${target})`, + })), { name: "missing_external_adaptation_parent", mutate: { diff --git a/scripts/validate-ia.mjs b/scripts/validate-ia.mjs index a031cb4..ac3831e 100644 --- a/scripts/validate-ia.mjs +++ b/scripts/validate-ia.mjs @@ -27,6 +27,16 @@ const nestedIndexes = [ "design-engineering/reference-profiles/governed-local/index.md", "design-engineering/reference-profiles/external-adaptation/index.md", ]; +const governedProfileLinks = [ + "[Editorial profile](editorial/profile.json)", + "[Editorial state matrix](editorial/generated/state-matrix.md)", + "[Editorial keyboard matrix](editorial/generated/keyboard-matrix.md)", + "[Editorial evidence coverage](editorial/generated/evidence-coverage.md)", + "[Terminal profile](terminal/profile.json)", + "[Terminal state matrix](terminal/generated/state-matrix.md)", + "[Terminal keyboard matrix](terminal/generated/keyboard-matrix.md)", + "[Terminal evidence coverage](terminal/generated/evidence-coverage.md)", +]; function read(relative) { const target = path.join(root, relative); @@ -68,6 +78,9 @@ function requireRootRoles() { requireIncludes("design-engineering/index.md", "[Reference Profiles](reference-profiles/index.md)"); requireIncludes("design-engineering/reference-profiles/index.md", "[External Adaptation](external-adaptation/index.md)"); requireIncludes("design-engineering/reference-profiles/index.md", "[Governed Local Profiles](governed-local/index.md)"); + for (const link of governedProfileLinks) { + requireIncludes("design-engineering/reference-profiles/governed-local/index.md", link); + } } function requireTaskRoutes() { From 7d38fb7e1b13b320d02f7ce1fb9cfa6e091cc230 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 06:28:25 +0900 Subject: [PATCH 12/44] Make validator module paths portable --- scripts/test-component-state-sentinel.mjs | 3 ++- scripts/test-consumer-reference-sentinel.mjs | 3 ++- scripts/test-generate-consumer-reference-evidence.mjs | 3 ++- scripts/test-renderer-purity.mjs | 3 ++- scripts/test-summarize-sentinel-calibration.mjs | 3 ++- scripts/test-validate-component-state.mjs | 3 ++- scripts/test-validate-domains.mjs | 3 ++- scripts/test-validate-okf.mjs | 3 ++- scripts/test-validate-patterns.mjs | 3 ++- scripts/test-validate-reference-profiles.mjs | 3 ++- scripts/test-validate-vocabulary.mjs | 3 ++- scripts/test-validate-webpage-workflow.mjs | 3 ++- scripts/validate-baseline-manifest.mjs | 3 ++- scripts/validate-consumer-reference.mjs | 3 ++- scripts/validate-renderer-purity.mjs | 4 ++-- tests/helpers/render-component-state.mjs | 3 ++- 16 files changed, 32 insertions(+), 17 deletions(-) diff --git a/scripts/test-component-state-sentinel.mjs b/scripts/test-component-state-sentinel.mjs index c0ec557..8ef89e6 100644 --- a/scripts/test-component-state-sentinel.mjs +++ b/scripts/test-component-state-sentinel.mjs @@ -2,8 +2,9 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const cases = [ ["key_runtime_mismatch", "runtime-key-mismatch", "state_activation_key_parity", "action-focused"], ["pressed_false", "runtime-pressed-false", "pressed_surface_mismatch", "toggle-focused-pressed"], diff --git a/scripts/test-consumer-reference-sentinel.mjs b/scripts/test-consumer-reference-sentinel.mjs index 483ea34..d151a77 100644 --- a/scripts/test-consumer-reference-sentinel.mjs +++ b/scripts/test-consumer-reference-sentinel.mjs @@ -2,8 +2,9 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const cases = [ ["hidden_layout", "hidden-layout", "computed_layout_visible"], ["long_content_reflow", "long-content-reflow", "computed_long_content_reflow"], diff --git a/scripts/test-generate-consumer-reference-evidence.mjs b/scripts/test-generate-consumer-reference-evidence.mjs index c1673a6..d85c79c 100644 --- a/scripts/test-generate-consumer-reference-evidence.mjs +++ b/scripts/test-generate-consumer-reference-evidence.mjs @@ -5,8 +5,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const sourceRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); const generator = path.join(repositoryRoot, "scripts/generate-consumer-reference-evidence.mjs"); diff --git a/scripts/test-renderer-purity.mjs b/scripts/test-renderer-purity.mjs index dd740cf..9a756e7 100644 --- a/scripts/test-renderer-purity.mjs +++ b/scripts/test-renderer-purity.mjs @@ -4,8 +4,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(repositoryRoot, "scripts", "validate-renderer-purity.mjs"); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-renderer-")); const cases = []; diff --git a/scripts/test-summarize-sentinel-calibration.mjs b/scripts/test-summarize-sentinel-calibration.mjs index 46a98d1..4046ea8 100644 --- a/scripts/test-summarize-sentinel-calibration.mjs +++ b/scripts/test-summarize-sentinel-calibration.mjs @@ -4,9 +4,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { BASELINE_ENVIRONMENT, BASELINE_REFERENCE, sha256 } from "./baseline-contract.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const summarizer = path.join(repositoryRoot, "scripts", "summarize-sentinel-calibration.mjs"); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-pr4-summary-")); const png = fs.readFileSync(path.join(repositoryRoot, BASELINE_REFERENCE.baseline.path)); diff --git a/scripts/test-validate-component-state.mjs b/scripts/test-validate-component-state.mjs index 49857e9..326981b 100644 --- a/scripts/test-validate-component-state.mjs +++ b/scripts/test-validate-component-state.mjs @@ -5,8 +5,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const sourceRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); const validator = path.join(repositoryRoot, "scripts/validate-component-state.mjs"); diff --git a/scripts/test-validate-domains.mjs b/scripts/test-validate-domains.mjs index b8d72f2..19cb3cd 100644 --- a/scripts/test-validate-domains.mjs +++ b/scripts/test-validate-domains.mjs @@ -4,8 +4,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-domains.mjs"); const revision = "220e8607c90b17337d210125777b7b695f26c221"; const repository = "https://github.com/emilkowalski/skills"; diff --git a/scripts/test-validate-okf.mjs b/scripts/test-validate-okf.mjs index d83a2d7..8dacdfd 100644 --- a/scripts/test-validate-okf.mjs +++ b/scripts/test-validate-okf.mjs @@ -4,8 +4,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-okf.mjs"); const cases = [ diff --git a/scripts/test-validate-patterns.mjs b/scripts/test-validate-patterns.mjs index 71c05f7..890476f 100644 --- a/scripts/test-validate-patterns.mjs +++ b/scripts/test-validate-patterns.mjs @@ -4,8 +4,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-patterns.mjs"); const cases = [ { diff --git a/scripts/test-validate-reference-profiles.mjs b/scripts/test-validate-reference-profiles.mjs index 64ac14c..94c46e6 100644 --- a/scripts/test-validate-reference-profiles.mjs +++ b/scripts/test-validate-reference-profiles.mjs @@ -4,8 +4,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(repositoryRoot, "scripts", "validate-consumer-reference.mjs"); const profilePath = "design-engineering/reference-profiles/governed-local/editorial/profile.json"; const terminalProfilePath = "design-engineering/reference-profiles/governed-local/terminal/profile.json"; diff --git a/scripts/test-validate-vocabulary.mjs b/scripts/test-validate-vocabulary.mjs index acfa384..d3ada7b 100644 --- a/scripts/test-validate-vocabulary.mjs +++ b/scripts/test-validate-vocabulary.mjs @@ -4,8 +4,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-vocabulary.mjs"); const glossary = [ diff --git a/scripts/test-validate-webpage-workflow.mjs b/scripts/test-validate-webpage-workflow.mjs index 7cda0fc..c33237f 100644 --- a/scripts/test-validate-webpage-workflow.mjs +++ b/scripts/test-validate-webpage-workflow.mjs @@ -4,8 +4,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-webpage-workflow.mjs"); const files = { diff --git a/scripts/validate-baseline-manifest.mjs b/scripts/validate-baseline-manifest.mjs index 4f5c9bd..8b50900 100644 --- a/scripts/validate-baseline-manifest.mjs +++ b/scripts/validate-baseline-manifest.mjs @@ -1,10 +1,11 @@ #!/usr/bin/env node import path from "node:path"; +import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { readJson, validateCalibration, validateManifest } from "./baseline-contract.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const options = { calibration: "consumer-reference/baselines/calibration.json", json: false, manifest: "consumer-reference/baselines/manifest.json" }; const failures = []; for (let index = 2; index < process.argv.length; index += 1) { diff --git a/scripts/validate-consumer-reference.mjs b/scripts/validate-consumer-reference.mjs index e0ad0d0..185e466 100644 --- a/scripts/validate-consumer-reference.mjs +++ b/scripts/validate-consumer-reference.mjs @@ -2,13 +2,14 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { isPlainObject, validateItemSchema } from "./consumer-reference-schema.mjs"; import { canonicalGovernedProfilePaths } from "./governed-profile-registry.mjs"; import { validateReferenceProfile, validateReferenceProfileSet } from "./reference-profile-contract.mjs"; const root = process.cwd(); -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "item.schema.json"), "utf8")); const validateFullItemSchema = new Ajv2020({ allErrors: true, strict: false }).compile(schema); const failures = []; diff --git a/scripts/validate-renderer-purity.mjs b/scripts/validate-renderer-purity.mjs index 22dd3a2..7e27882 100644 --- a/scripts/validate-renderer-purity.mjs +++ b/scripts/validate-renderer-purity.mjs @@ -3,9 +3,9 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); let renderer = "tests/helpers/render-consumer-reference.mjs"; let json = false; const failures = []; diff --git a/tests/helpers/render-component-state.mjs b/tests/helpers/render-component-state.mjs index 2d66586..df3cf15 100644 --- a/tests/helpers/render-component-state.mjs +++ b/tests/helpers/render-component-state.mjs @@ -1,8 +1,9 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { resolveProfileRecords } from "../../scripts/profile-record-contract.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../.."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const governedRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); function readJson(file) { From d7261db2ff7be067fc3920637d1dd27a97b13a20 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 07:41:21 +0900 Subject: [PATCH 13/44] Harden portable token artifact boundaries --- scripts/build-reference-artifacts.mjs | 24 +++++++--- scripts/reference-artifact-path-contract.mjs | 28 ++++++++++++ scripts/test-reference-adapters.mjs | 48 ++++++++++++++++++-- scripts/validate-reference-artifacts.mjs | 30 ++++++++---- 4 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 scripts/reference-artifact-path-contract.mjs diff --git a/scripts/build-reference-artifacts.mjs b/scripts/build-reference-artifacts.mjs index 65463a3..c3242f4 100644 --- a/scripts/build-reference-artifacts.mjs +++ b/scripts/build-reference-artifacts.mjs @@ -5,7 +5,9 @@ import fs from "node:fs"; import path from "node:path"; import StyleDictionary from "style-dictionary"; import { adapter, createStyleDictionaryConfig } from "../consumer-reference/adapters/style-dictionary.config.mjs"; +import { inspectArtifactPath } from "./reference-artifact-path-contract.mjs"; import { validatePortableTokens } from "./reference-token-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; const root = process.cwd(); const defaults = { @@ -55,14 +57,24 @@ const output = path.resolve(root, options.output); const manifestPath = path.resolve(root, options.manifest); const failures = []; const warnings = []; +for (const [code, target] of [ + ["token_source_untrusted", source], + ["artifact_output_untrusted", output], + ["artifact_manifest_untrusted", manifestPath], +]) { + const inspection = inspectArtifactPath(root, target, code !== "token_source_untrusted"); + if (!inspection.ok) failures.push({ code, message: `${target} must be a contained regular non-symlink file`, path: path.relative(root, target) || "." }); +} let sourceBytes; let document; -try { - sourceBytes = fs.readFileSync(source); - document = JSON.parse(sourceBytes.toString("utf8")); -} catch (error) { - const code = error instanceof SyntaxError ? "token_json_invalid" : "token_source_unresolved"; - failures.push({ code, message: error instanceof Error ? error.message : String(error), path: options.source }); +if (failures.length === 0) { + try { + sourceBytes = fs.readFileSync(source); + document = parseStrictJson(sourceBytes.toString("utf8")); + } catch (error) { + const code = error instanceof SyntaxError ? "token_json_invalid" : "token_source_unresolved"; + failures.push({ code, message: error instanceof Error ? error.message : String(error), path: options.source }); + } } let contract = { failures: [], tokens: [] }; if (document !== undefined) { diff --git a/scripts/reference-artifact-path-contract.mjs b/scripts/reference-artifact-path-contract.mjs new file mode 100644 index 0000000..c78a2f9 --- /dev/null +++ b/scripts/reference-artifact-path-contract.mjs @@ -0,0 +1,28 @@ +import fs from "node:fs"; +import path from "node:path"; + +function isContained(root, target) { + const relative = path.relative(root, target); + return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative); +} + +export function inspectArtifactPath(root, candidate, allowMissing) { + const trustedRoot = fs.realpathSync(root); + const target = path.resolve(root, candidate); + if (!isContained(trustedRoot, target)) return { ok: false, path: target, reason: "outside_trust_root" }; + + const segments = path.relative(trustedRoot, target).split(path.sep); + let current = trustedRoot; + for (const [index, segment] of segments.entries()) { + current = path.join(current, segment); + if (!fs.existsSync(current)) { + return allowMissing ? { ok: true, path: target } : { ok: false, path: target, reason: "missing" }; + } + const metadata = fs.lstatSync(current); + if (metadata.isSymbolicLink()) return { ok: false, path: target, reason: "symbolic_link" }; + const finalSegment = index === segments.length - 1; + if (finalSegment && !metadata.isFile()) return { ok: false, path: target, reason: "not_regular_file" }; + if (!finalSegment && !metadata.isDirectory()) return { ok: false, path: target, reason: "parent_not_directory" }; + } + return { ok: true, path: target }; +} diff --git a/scripts/test-reference-adapters.mjs b/scripts/test-reference-adapters.mjs index af132e5..1a012e3 100644 --- a/scripts/test-reference-adapters.mjs +++ b/scripts/test-reference-adapters.mjs @@ -5,8 +5,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const builder = path.join(repositoryRoot, "scripts", "build-reference-artifacts.mjs"); const validator = path.join(repositoryRoot, "scripts", "validate-reference-artifacts.mjs"); const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference/schema/portable-tokens.schema.json"), "utf8")); @@ -25,7 +26,7 @@ function codes(output) { : []; } -const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-reference-adapter-")); +const tempRoot = fs.mkdtempSync(path.join(repositoryRoot, ".tmp-stylegallery-reference-adapter-")); const results = []; function run(command, args) { @@ -109,6 +110,9 @@ try { const invalidTokenDescription = path.join(tempRoot, "invalid-token-description.json"); fs.writeFileSync(invalidTokenDescription, '{"space":{"$type":"dimension","small":{"$description":42,"$value":{"value":1,"unit":"px"}}}}\n'); recordRejected("invalid_token_description", run(builder, ["--source", invalidTokenDescription, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_description_invalid"); + const duplicateSourceProperty = path.join(tempRoot, "invalid-duplicate-source-property.json"); + fs.writeFileSync(duplicateSourceProperty, '{"space":{"$type":"dimension","$type":"dimension","small":{"$value":{"value":1,"unit":"px"}}}}\n'); + recordRejected("duplicate_source_property", run(builder, ["--source", duplicateSourceProperty, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_json_invalid"); for (const [name, sourceText] of [ ["constructor", '{"constructor":{"$type":"dimension","small":{"$value":{"value":1,"unit":"px"}}}}\n'], ["prototype", '{"prototype":{"$type":"dimension","small":{"$value":{"value":1,"unit":"px"}}}}\n'], @@ -125,6 +129,42 @@ try { recordAccepted("artifact_baseline", baselineBuild); const baselineCss = fs.readFileSync(output, "utf8"); const baselineManifest = fs.readFileSync(manifest, "utf8"); + fs.writeFileSync(manifest, baselineManifest.replace('"schemaVersion": "1.0",', '"schemaVersion": "1.0",\n "schemaVersion": "1.0",')); + recordRejected("duplicate_manifest_property", run(validator, ["--manifest", manifest]), "artifact_manifest_invalid"); + const duplicateValidatorSource = baselineSource.replace('"$type": "color",', '"$type": "color",\n "$type": "color",'); + fs.writeFileSync(path.join(tempRoot, "source.json"), duplicateValidatorSource); + const duplicateSourceManifest = JSON.parse(baselineManifest); + duplicateSourceManifest.inputHash = `sha256:${crypto.createHash("sha256").update(duplicateValidatorSource).digest("hex")}`; + fs.writeFileSync(manifest, `${JSON.stringify(duplicateSourceManifest, null, 2)}\n`); + recordRejected("duplicate_validator_source_property", run(validator, ["--manifest", manifest]), "artifact_source_invalid"); + fs.writeFileSync(path.join(tempRoot, "source.json"), baselineSource); + fs.writeFileSync(manifest, baselineManifest); + const symlinkSource = path.join(tempRoot, "symlink-source.json"); + fs.symlinkSync(path.join(tempRoot, "source.json"), symlinkSource); + recordRejected("symlink_source", run(builder, ["--source", symlinkSource, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_source_untrusted"); + const realOutput = path.join(tempRoot, "real-tokens.css"); + fs.renameSync(output, realOutput); + fs.symlinkSync(realOutput, output); + recordRejected("symlink_validator_output", run(validator, ["--manifest", manifest]), "artifact_output_untrusted"); + recordRejected("symlink_builder_output", run(builder, ["--source", path.join(tempRoot, "source.json"), "--output", output, "--manifest", manifest, "--fail-on-warning"]), "artifact_output_untrusted"); + fs.rmSync(output); + fs.renameSync(realOutput, output); + const realManifest = path.join(tempRoot, "real-manifest.json"); + fs.renameSync(manifest, realManifest); + fs.symlinkSync(realManifest, manifest); + recordRejected("symlink_validator_manifest", run(validator, ["--manifest", manifest]), "artifact_manifest_untrusted"); + recordRejected("symlink_builder_manifest", run(builder, ["--source", path.join(tempRoot, "source.json"), "--output", output, "--manifest", manifest, "--fail-on-warning"]), "artifact_manifest_untrusted"); + fs.rmSync(manifest); + fs.renameSync(realManifest, manifest); + const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-reference-escape-")); + const escapedSource = path.join(outsideRoot, "source.json"); + fs.writeFileSync(escapedSource, baselineSource); + recordRejected("escaped_source", run(builder, ["--source", escapedSource, "--output", output, "--manifest", manifest, "--fail-on-warning"]), "token_source_untrusted"); + const escapedOutput = path.join(outsideRoot, "tokens.css"); + recordRejected("escaped_output", run(builder, ["--source", path.join(tempRoot, "source.json"), "--output", escapedOutput, "--manifest", manifest, "--fail-on-warning"]), "artifact_output_untrusted"); + const escapedManifest = path.join(outsideRoot, "manifest.json"); + recordRejected("escaped_manifest", run(builder, ["--source", path.join(tempRoot, "source.json"), "--output", output, "--manifest", escapedManifest, "--fail-on-warning"]), "artifact_manifest_untrusted"); + fs.rmSync(outsideRoot, { force: true, recursive: true }); const artifactCases = [ ["no_output", "artifact_output_missing", () => fs.rmSync(output)], ["zero_count", "artifact_zero_count", (item) => { item.sourceCount = 0; item.outputCount = 0; item.declarations = []; }], @@ -146,7 +186,7 @@ try { fs.writeFileSync(output, baselineCss); const item = JSON.parse(baselineManifest); mutate(item); - if (fs.existsSync(output)) fs.writeFileSync(manifest, `${JSON.stringify(item, null, 2)}\n`); + fs.writeFileSync(manifest, `${JSON.stringify(item, null, 2)}\n`); recordRejected(name, run(validator, ["--manifest", manifest]), expected); } } finally { @@ -156,4 +196,4 @@ try { const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); const report = { failures, ok: failures.length === 0, results }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/validate-reference-artifacts.mjs b/scripts/validate-reference-artifacts.mjs index 46f23e0..d5cb8ab 100644 --- a/scripts/validate-reference-artifacts.mjs +++ b/scripts/validate-reference-artifacts.mjs @@ -2,11 +2,13 @@ import crypto from "node:crypto"; import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { adapter } from "../consumer-reference/adapters/style-dictionary.config.mjs"; +import { inspectArtifactPath } from "./reference-artifact-path-contract.mjs"; import { validatePortableTokens } from "./reference-token-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; function hash(content) { return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`; @@ -35,14 +37,18 @@ function parseArguments() { const options = parseArguments(); const failures = [...options.failures]; const warnings = []; -const manifestPath = path.resolve(process.cwd(), options.manifest || "missing.json"); +const trustRoot = process.cwd(); +const manifestPath = path.resolve(trustRoot, options.manifest || "missing.json"); let manifest; -if (!fs.existsSync(manifestPath)) { +const manifestInspection = inspectArtifactPath(trustRoot, manifestPath, false); +if (!manifestInspection.ok && manifestInspection.reason === "missing") { failures.push({ code: "artifact_manifest_missing", message: "manifest does not exist", path: options.manifest }); failures.push({ code: "artifact_output_missing", message: "output cannot be located without a manifest", path: options.manifest }); +} else if (!manifestInspection.ok) { + failures.push({ code: "artifact_manifest_untrusted", message: "manifest must be a contained regular non-symlink file", path: options.manifest }); } else { try { - manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + manifest = parseStrictJson(fs.readFileSync(manifestPath, "utf8")); } catch (error) { failures.push({ code: "artifact_manifest_invalid", message: error instanceof Error ? error.message : String(error), path: options.manifest }); } @@ -63,12 +69,15 @@ if (manifest !== undefined) { if (new Set(declarationNames).size !== declarationNames.length) failures.push({ code: "artifact_declaration_duplicate", message: "manifest contains duplicate declarations", path: options.manifest }); const sourcePath = typeof manifest.sourceFile === "string" ? path.resolve(path.dirname(manifestPath), manifest.sourceFile) : ""; const outputPath = typeof manifest.outputFile === "string" ? path.resolve(path.dirname(manifestPath), manifest.outputFile) : ""; - if (!sourcePath || !fs.existsSync(sourcePath)) failures.push({ code: "artifact_source_missing", message: "manifest source file does not exist", path: options.manifest }); + const sourceInspection = sourcePath ? inspectArtifactPath(trustRoot, sourcePath, false) : { ok: false, reason: "missing" }; + const outputInspection = outputPath ? inspectArtifactPath(trustRoot, outputPath, false) : { ok: false, reason: "missing" }; + if (!sourceInspection.ok && sourceInspection.reason === "missing") failures.push({ code: "artifact_source_missing", message: "manifest source file does not exist", path: options.manifest }); + else if (!sourceInspection.ok) failures.push({ code: "token_source_untrusted", message: "manifest source must be a contained regular non-symlink file", path: options.manifest }); else { const sourceBytes = fs.readFileSync(sourcePath); if (manifest.inputHash !== hash(sourceBytes)) failures.push({ code: "artifact_input_hash_mismatch", message: "input hash does not match source", path: options.manifest }); try { - const contract = validatePortableTokens(JSON.parse(sourceBytes.toString("utf8"))); + const contract = validatePortableTokens(parseStrictJson(sourceBytes.toString("utf8"))); failures.push(...contract.failures); if (contract.tokens.length !== manifest.sourceCount) failures.push({ code: "artifact_source_count_mismatch", message: "source count does not match canonical tokens", path: options.manifest }); const expected = contract.tokens.map((token) => `--${token.path.replaceAll(".", "-")}`); @@ -77,7 +86,8 @@ if (manifest !== undefined) { failures.push({ code: "artifact_source_invalid", message: error instanceof Error ? error.message : String(error), path: options.manifest }); } } - if (!outputPath || !fs.existsSync(outputPath)) failures.push({ code: "artifact_output_missing", message: "manifest output file does not exist", path: options.manifest }); + if (!outputInspection.ok && outputInspection.reason === "missing") failures.push({ code: "artifact_output_missing", message: "manifest output file does not exist", path: options.manifest }); + else if (!outputInspection.ok) failures.push({ code: "artifact_output_untrusted", message: "manifest output must be a contained regular non-symlink file", path: options.manifest }); else { const css = fs.readFileSync(outputPath, "utf8"); const emitted = cssDeclarations(css); @@ -93,12 +103,12 @@ if (manifest !== undefined) { if (emitted.length !== manifest.outputCount || emitted.length !== declarationNames.length) failures.push({ code: "artifact_count_mismatch", message: "CSS, manifest count, and declarations differ", path: options.manifest }); if (css.includes("[object Object]")) failures.push({ code: "artifact_object_sentinel", message: "CSS contains [object Object]", path: options.manifest }); if (/\{[a-z0-9.-]+\}|\b(?:null|undefined)\b/.test(css)) failures.push({ code: "artifact_unresolved_value", message: "CSS contains an unresolved value", path: options.manifest }); - if (sourcePath && fs.existsSync(sourcePath)) { - const rebuildRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-reference-validation-")); + if (sourceInspection.ok) { + const rebuildRoot = fs.mkdtempSync(path.join(trustRoot, ".tmp-stylegallery-reference-validation-")); try { const expectedOutput = path.join(rebuildRoot, "tokens.css"); const expectedManifest = path.join(rebuildRoot, "manifest.json"); - const builder = path.join(path.dirname(new URL(import.meta.url).pathname), "build-reference-artifacts.mjs"); + const builder = path.join(path.dirname(fileURLToPath(import.meta.url)), "build-reference-artifacts.mjs"); const child = spawnSync(process.execPath, [builder, "--source", sourcePath, "--output", expectedOutput, "--manifest", expectedManifest, "--adapter", "style-dictionary", "--fail-on-warning", "--json"], { cwd: process.cwd(), encoding: "utf8" }); if (child.status !== 0 || !fs.existsSync(expectedOutput)) failures.push({ code: "artifact_canonical_rebuild_failed", message: "canonical source could not be rebuilt with the pinned adapter", path: options.manifest }); else if (fs.readFileSync(expectedOutput, "utf8") !== css) failures.push({ code: "artifact_source_output_mismatch", message: "CSS differs from the pinned adapter output derived from canonical source", path: options.manifest }); From 1f51cd25a47bcfc5559f6742f0c0b078d07f51e3 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 07:42:11 +0900 Subject: [PATCH 14/44] Prepare source-governed visual evidence --- .../schema/capture-session.schema.json | 28 ++++- .../schema/component-state.schema.json | 37 ++++++- .../schema/visual-evidence.schema.json | 31 ++++++ .../editorial/states/button.states.json | 40 +++++++ .../terminal/states/button.states.json | 40 +++++++ package.json | 2 +- scripts/artifact-metadata.mjs | 20 ++++ scripts/capture-session-contract.mjs | 101 ++++++++++++++++- scripts/component-state-contract.mjs | 1 + scripts/component-state-semantics.mjs | 8 ++ scripts/create-component-state-session.mjs | 23 +++- scripts/evidence-artifact-contract.mjs | 62 +++++++---- scripts/finalize-component-state-evidence.mjs | 19 +++- .../generate-consumer-reference-evidence.mjs | 3 +- .../test-component-state-source-contract.mjs | 57 ++++++++++ ...est-validate-component-state-artifacts.mjs | 102 ++++++++++++++++-- scripts/test-validate-component-state.mjs | 3 + scripts/validate-component-state.mjs | 22 +++- scripts/visual-expectation-contract.mjs | 20 ++++ tests/component-state-evidence.spec.mjs | 26 ++++- tests/helpers/render-component-state.mjs | 1 + 21 files changed, 593 insertions(+), 53 deletions(-) create mode 100644 consumer-reference/schema/visual-evidence.schema.json create mode 100644 scripts/artifact-metadata.mjs create mode 100644 scripts/test-component-state-source-contract.mjs create mode 100644 scripts/visual-expectation-contract.mjs diff --git a/consumer-reference/schema/capture-session.schema.json b/consumer-reference/schema/capture-session.schema.json index 9b6ee44..ce43789 100644 --- a/consumer-reference/schema/capture-session.schema.json +++ b/consumer-reference/schema/capture-session.schema.json @@ -16,6 +16,7 @@ "branch": { "type": "string", "minLength": 1 }, "attempt": { "type": "integer", "minimum": 1 }, "environment": { "$ref": "#/$defs/environment" }, + "source": { "$ref": "#/$defs/source_manifest" }, "intended": { "type": "array", "minItems": 1, @@ -72,7 +73,8 @@ "revision": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, "branch": { "type": "string", "minLength": 1 }, "attempt": { "type": "integer", "minimum": 1 }, - "environment": { "$ref": "#/$defs/environment" } + "environment": { "$ref": "#/$defs/environment" }, + "source": { "$ref": "#/$defs/source_manifest" } } }, "receipt_artifact": { @@ -102,9 +104,33 @@ "branch": { "type": "string", "minLength": 1 }, "attempt": { "type": "integer", "minimum": 1 }, "environment": { "$ref": "#/$defs/environment" }, + "source": { "$ref": "#/$defs/source_manifest" }, "intended": { "$ref": "#/$defs/intended" } } }, + "source_manifest": { + "type": "object", + "additionalProperties": false, + "required": ["sha256", "files"], + "properties": { + "sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "files": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256", "byte_length"], + "properties": { + "path": { "type": "string", "pattern": "^(?:(?:consumer-reference|profiles|scripts|tests)/[^\\\\?#]+|package(?:-lock)?\\.json|playwright\\.config\\.mjs)$" }, + "sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "byte_length": { "type": "integer", "minimum": 1 } + } + } + } + } + }, "intended": { "type": "array", "minItems": 1, diff --git a/consumer-reference/schema/component-state.schema.json b/consumer-reference/schema/component-state.schema.json index 0356af7..e43764e 100644 --- a/consumer-reference/schema/component-state.schema.json +++ b/consumer-reference/schema/component-state.schema.json @@ -64,12 +64,30 @@ "stateSet": { "type": "object", "additionalProperties": false, - "required": ["schema_version", "record_kind", "component_id", "profile_id", "scenarios"], + "required": ["schema_version", "record_kind", "component_id", "profile_id", "visual_environments", "scenarios"], "properties": { "schema_version": { "const": "1.0" }, "record_kind": { "const": "state_set" }, "component_id": { "const": "button" }, "profile_id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "visual_environments": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "browser", "browser_revision", "container_image", "platform", "playwright", "viewport"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "browser": { "type": "string", "minLength": 1 }, + "browser_revision": { "type": "string", "minLength": 1 }, + "container_image": { "type": "string", "minLength": 1 }, + "platform": { "enum": ["darwin/arm64", "linux/amd64"] }, + "playwright": { "const": "1.61.0" }, + "viewport": { "const": "1024x768" } + } + } + }, "scenarios": { "type": "array", "minItems": 1, @@ -95,7 +113,7 @@ "expected": { "type": "object", "additionalProperties": false, - "required": ["visual", "dom", "ax", "activation"], + "required": ["visual", "visual_image", "dom", "ax", "activation"], "properties": { "visual": { "type": "array", @@ -103,6 +121,21 @@ "uniqueItems": true, "items": { "enum": ["ready", "focus", "pressed", "expanded", "loading", "busy", "disabled"] } }, + "visual_image": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["environment_id", "sha256", "width", "height"], + "properties": { + "environment_id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "width": { "type": "integer", "minimum": 44 }, + "height": { "type": "integer", "minimum": 44 } + } + } + }, "dom": { "type": "object", "additionalProperties": { "type": "string" } }, "ax": { "type": "object", "additionalProperties": { "type": ["string", "boolean"] } }, "activation": { "enum": ["allowed", "suppressed"] } diff --git a/consumer-reference/schema/visual-evidence.schema.json b/consumer-reference/schema/visual-evidence.schema.json new file mode 100644 index 0000000..e4d08fd --- /dev/null +++ b/consumer-reference/schema/visual-evidence.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stylegallery.local/consumer-reference/schema/visual-evidence.schema.json", + "title": "Browser-authored visual capture metadata", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "channel", "capture_session", "captured_at", "profile_id", "scenario_id", "semantic_mode", "source_sha256", "image"], + "properties": { + "schema_version": { "const": "1.0" }, + "channel": { "const": "visual" }, + "capture_session": { "$ref": "https://stylegallery.local/consumer-reference/schema/capture-session.schema.json#/$defs/session_link" }, + "captured_at": { "type": "string", "format": "date-time" }, + "profile_id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "scenario_id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "semantic_mode": { "enum": ["action", "toggle", "disclosure", "switch"] }, + "source_sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "image": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256", "byte_length", "media_type", "width", "height"], + "properties": { + "path": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*\\.png$" }, + "sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "byte_length": { "type": "integer", "minimum": 1 }, + "media_type": { "const": "image/png" }, + "width": { "type": "integer", "minimum": 44 }, + "height": { "type": "integer", "minimum": 44 } + } + } + } +} diff --git a/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json b/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json index 6d0ee1e..b3d3c45 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +++ b/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json @@ -3,6 +3,26 @@ "record_kind": "state_set", "component_id": "button", "profile_id": "editorial-reference-profile", + "visual_environments": [ + { + "id": "darwin-arm64-chromium-1228", + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + { + "id": "linux-amd64-chromium-1228", + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + "platform": "linux/amd64", + "playwright": "1.61.0", + "viewport": "1024x768" + } + ], "scenarios": [ { "id": "action-disabled-busy", @@ -20,6 +40,10 @@ "busy", "disabled" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:4b3df5ceab968c2808b21319b8b7694a723db61dd11cf6c3173664604f4e3776", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:24bad38e0be21ce73d9450ce0b47aa156550ec3772e3f7842f00be6f3543f727", "width": 640, "height": 56 } + ], "dom": { "active": "false", "aria-busy": "true", @@ -47,6 +71,10 @@ "focus", "ready" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:ad888e0f6eb4f57cb020672af3965e33245330af2c6547247d60bb9a4080afeb", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:a4a954932d60a1602c2c7f8842e69280c94171abd4103cf66ae48fddc8893c96", "width": 640, "height": 56 } + ], "dom": { "active": "true", "role": "button" @@ -75,6 +103,10 @@ "focus", "loading" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:90379d9605eb1f544f3f19192a2299b4369487aff3e42bd904be2c498adcf18d", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:44e729b3cf5a2673edbb89d73b36ebe752ca0b465002745949964fa6e00f9d25", "width": 640, "height": 56 } + ], "dom": { "active": "true", "aria-busy": "true", @@ -108,6 +140,10 @@ "focus", "loading" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:6b94bf18d52b28a0ac616d6180fd004c5f9dbb5d72bae944900e2b1978d7ac9f", "width": 640, "height": 89 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:a8e581adf5de79ab6436c8f9deb46d8a9165698b6bfb4e9ae37928fddf681ee6", "width": 640, "height": 88 } + ], "dom": { "active": "true", "aria-busy": "true", @@ -138,6 +174,10 @@ "focus", "pressed" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:51404ac48079f5925ec2e674852f7fb38fb4c9fddfda0e74e3618a843edc21a3", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:11c6182ae9258f8593aa0a440470b4bc55959e1d723ab2e399bb729b6ed5fdcc", "width": 640, "height": 56 } + ], "dom": { "active": "true", "aria-pressed": "true", diff --git a/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json b/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json index 1209e8d..830f6d4 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +++ b/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json @@ -3,6 +3,26 @@ "record_kind": "state_set", "component_id": "button", "profile_id": "terminal-reference-profile", + "visual_environments": [ + { + "id": "darwin-arm64-chromium-1228", + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "local-host:25.5.0", + "platform": "darwin/arm64", + "playwright": "1.61.0", + "viewport": "1024x768" + }, + { + "id": "linux-amd64-chromium-1228", + "browser": "Google Chrome for Testing 149.0.7827.55", + "browser_revision": "chromium-1228", + "container_image": "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + "platform": "linux/amd64", + "playwright": "1.61.0", + "viewport": "1024x768" + } + ], "scenarios": [ { "id": "action-disabled-busy", @@ -20,6 +40,10 @@ "busy", "disabled" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:d346be9e96750f636f29078d17d9871a04c2f4442b6849b029fa754c2795674e", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:f28ac192821773d5d692ad978075da5b1480971e3b3bc9be8983e3eb2cd43484", "width": 640, "height": 56 } + ], "dom": { "active": "false", "aria-busy": "true", @@ -47,6 +71,10 @@ "focus", "ready" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:39b96bc2459e2f0f442ce7b709f22e46b44f49b17c52d61940dff0246108ec48", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:7497f0542e2a4b77749ee47cfa7f575b640bad737c652a71610e9f86f8e5f2b8", "width": 640, "height": 56 } + ], "dom": { "active": "true", "role": "button" @@ -75,6 +103,10 @@ "focus", "loading" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:05cbe173a8274cfa7e6aab05713d1cb925820a7412bffceadb94d83498c17747", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:1521023b829210f899d2f56750e362cbfc30e8d6045f6f78a6c553a43bf610c4", "width": 640, "height": 56 } + ], "dom": { "active": "true", "aria-busy": "true", @@ -108,6 +140,10 @@ "focus", "loading" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:d8c7ba056dd2a61ce6c85d8ec6bf56733cdc34da07678dd9b4175c7de9ab926a", "width": 640, "height": 88 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:7cef4e68a5a3df50c1705c7b88936bf0988317e2f6bed92ae5c04992388126a3", "width": 640, "height": 91 } + ], "dom": { "active": "true", "aria-busy": "true", @@ -138,6 +174,10 @@ "focus", "pressed" ], + "visual_image": [ + { "environment_id": "darwin-arm64-chromium-1228", "sha256": "sha256:838229be984b8bda1ebfe6095564f1093df18fdd2ca1e6c3d5b5c086162d4692", "width": 640, "height": 56 }, + { "environment_id": "linux-amd64-chromium-1228", "sha256": "sha256:1c105821f16a028e974bdf00590e3d9aee8042016ee341f4efe56b4a525b867d", "width": 640, "height": 56 } + ], "dom": { "active": "true", "aria-pressed": "true", diff --git a/package.json b/package.json index 1771c17..7e766b1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json", "create:component-state-session": "node scripts/create-component-state-session.mjs", "test": "node scripts/test-reference-adapters.mjs --json", - "test:component-state": "node scripts/test-validate-component-state.mjs --json && node scripts/test-validate-component-state-artifacts.mjs && node scripts/test-generate-consumer-reference-evidence.mjs --json", + "test:component-state": "node scripts/test-component-state-source-contract.mjs && node scripts/test-validate-component-state.mjs --json && node scripts/test-validate-component-state-artifacts.mjs && node scripts/test-generate-consumer-reference-evidence.mjs --json", "test:component-state:runtime": "playwright test tests/component-state-evidence.spec.mjs --project=chromium", "test:component-state:runtime-negative": "node scripts/test-component-state-sentinel.mjs", "test:sentinel": "playwright test tests/consumer-reference-sentinels.spec.mjs --project=chromium", diff --git a/scripts/artifact-metadata.mjs b/scripts/artifact-metadata.mjs new file mode 100644 index 0000000..2e5cab6 --- /dev/null +++ b/scripts/artifact-metadata.mjs @@ -0,0 +1,20 @@ +import crypto from "node:crypto"; + +const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); + +function pngDimensions(bytes) { + if (bytes.length < 24 || !bytes.subarray(0, 8).equals(pngSignature) || bytes.toString("ascii", 12, 16) !== "IHDR") return undefined; + const width = bytes.readUInt32BE(16); + const height = bytes.readUInt32BE(20); + return width > 0 && height > 0 ? { height, width } : undefined; +} + +export function artifactMetadata(bytes, mediaType) { + const metadata = { + byte_length: bytes.length, + media_type: mediaType, + sha256: `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`, + }; + if (mediaType === "image/png") Object.assign(metadata, pngDimensions(bytes)); + return metadata; +} diff --git a/scripts/capture-session-contract.mjs b/scripts/capture-session-contract.mjs index a07c0e1..68dc982 100644 --- a/scripts/capture-session-contract.mjs +++ b/scripts/capture-session-contract.mjs @@ -1,9 +1,34 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { execFileSync } from "node:child_process"; import { resolveProfileRecords } from "./profile-record-contract.mjs"; import { parseStrictJson } from "./strict-json.mjs"; +const captureSourcePaths = Object.freeze([ + "package-lock.json", + "package.json", + "playwright.config.mjs", + "consumer-reference/schema/ax-evidence.schema.json", + "consumer-reference/schema/capture-session.schema.json", + "consumer-reference/schema/component-state.schema.json", + "consumer-reference/schema/dom-evidence.schema.json", + "consumer-reference/schema/evidence-record.schema.json", + "consumer-reference/schema/fixture-manifest.schema.json", + "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "consumer-reference/schema/visual-evidence.schema.json", + "scripts/artifact-metadata.mjs", + "scripts/capture-session-contract.mjs", + "scripts/component-state-contract.mjs", + "scripts/component-state-semantics.mjs", + "scripts/create-component-state-session.mjs", + "scripts/profile-record-contract.mjs", + "scripts/strict-json.mjs", + "scripts/visual-expectation-contract.mjs", + "tests/component-state-evidence.spec.mjs", + "tests/helpers/render-component-state.mjs", +]); + function finding(code, file, message) { return { code, message, path: file }; } @@ -12,6 +37,77 @@ export function sha256(bytes) { return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`; } +function compare(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function profileSourceFiles(profileRoot) { + if (!fs.existsSync(profileRoot)) return []; + const sources = []; + const profiles = fs.readdirSync(profileRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(profileRoot, entry.name, "profile.json"))) + .sort((left, right) => compare(left.name, right.name)); + for (const profile of profiles) { + const profileFile = path.join(profileRoot, profile.name, "profile.json"); + const record = parseStrictJson(fs.readFileSync(profileFile, "utf8")); + const references = [ + "profile.json", + record.local_foundations, + record.tokens, + ...(record.component_records ?? []), + ...(record.fixture_records ?? []), + ...(record.state_records ?? []), + ]; + for (const reference of references) { + if (typeof reference !== "string") continue; + if (path.posix.isAbsolute(reference) || path.win32.isAbsolute(reference) || reference.includes("\\") || path.posix.normalize(reference) !== reference || reference.split("/").some((segment) => segment === "." || segment === "..")) { + throw new Error(`unsafe profile source reference ${profile.name}/${reference}`); + } + sources.push({ + file: path.join(profileRoot, profile.name, reference), + logicalPath: `profiles/${profile.name}/${reference.split(path.sep).join("/")}`, + }); + } + } + return sources; +} + +export function relevantSourceFiles(repositoryRoot, profileRoot) { + return [ + ...captureSourcePaths.map((reference) => ({ file: path.join(repositoryRoot, reference), logicalPath: reference })), + ...profileSourceFiles(profileRoot), + ].sort((left, right) => compare(left.logicalPath, right.logicalPath)); +} + +export function canonicalSourceManifest(repositoryRoot, profileRoot) { + const files = relevantSourceFiles(repositoryRoot, profileRoot).map(({ file, logicalPath }) => { + const stat = fs.lstatSync(file); + if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`capture source must be a regular non-symlink file: ${logicalPath}`); + const bytes = fs.readFileSync(file); + return { byte_length: bytes.length, path: logicalPath, sha256: sha256(bytes) }; + }); + return { files, sha256: sha256(Buffer.from(JSON.stringify(files))) }; +} + +export function sourceManifestMatches(source, repositoryRoot, profileRoot) { + try { + return sameJson(source, canonicalSourceManifest(repositoryRoot, profileRoot)); + } catch { + return false; + } +} + +export function dirtyRelevantSources(repositoryRoot, profileRoot) { + const repositoryPrefix = `${path.resolve(repositoryRoot)}${path.sep}`; + const tracked = relevantSourceFiles(repositoryRoot, profileRoot) + .map(({ file }) => path.resolve(file)) + .filter((file) => file.startsWith(repositoryPrefix)) + .map((file) => path.relative(repositoryRoot, file)); + if (tracked.length === 0) return []; + const output = execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all", "--", ...tracked], { cwd: repositoryRoot, encoding: "utf8" }); + return output.split("\n").filter(Boolean).sort(compare); +} + export function sessionLink(receipt, receiptSha256) { return { attempt: receipt.attempt, @@ -21,6 +117,7 @@ export function sessionLink(receipt, receiptSha256) { receipt_sha256: receiptSha256, revision: receipt.revision, session_id: receipt.session_id, + source: receipt.source, started_at: receipt.started_at, }; } @@ -56,10 +153,6 @@ export function readCaptureSession(file, validate, failures) { }; } -function compare(left, right) { - return left < right ? -1 : left > right ? 1 : 0; -} - export function canonicalIntended(profileRoot, failures) { if (!fs.existsSync(profileRoot)) return []; const intended = []; diff --git a/scripts/component-state-contract.mjs b/scripts/component-state-contract.mjs index 62d49b5..d7b9c94 100644 --- a/scripts/component-state-contract.mjs +++ b/scripts/component-state-contract.mjs @@ -39,6 +39,7 @@ export function compileSchemas(schemaRoot) { fixture: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "fixture-manifest.schema.json"), "utf8"))), item: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "item.schema.json"), "utf8"))), runtime: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "runtime-evidence-manifest.schema.json"), "utf8"))), + visual: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "visual-evidence.schema.json"), "utf8"))), }; } diff --git a/scripts/component-state-semantics.mjs b/scripts/component-state-semantics.mjs index 49461d5..0e0c1f3 100644 --- a/scripts/component-state-semantics.mjs +++ b/scripts/component-state-semantics.mjs @@ -55,10 +55,18 @@ export function validateComponentSemantics(component, file, failures) { export function validateStateSemantics(states, component, file, failures) { const scenarios = states.scenarios ?? []; + const visualEnvironments = states.visual_environments ?? []; + const visualEnvironmentIds = new Set(visualEnvironments.map((environment) => environment.id)); + for (const id of duplicateValues(visualEnvironments.map((environment) => environment.id))) failures.push(finding("visual_environment_duplicate", file, `visual environment ${id} is duplicated`)); + for (const selector of duplicateValues(visualEnvironments, (environment) => JSON.stringify(Object.entries(environment).filter(([key]) => key !== "id").sort()))) failures.push(finding("visual_environment_selector_duplicate", file, `visual environment selector ${selector} is duplicated`)); for (const id of duplicateValues(scenarios.map((scenario) => scenario.id))) failures.push(finding("scenario_duplicate", file, `scenario ${id} is duplicated`)); for (const key of duplicateValues(scenarios, (scenario) => [...(scenario.states ?? [])].sort().join("\u0000"))) failures.push(finding("state_set_duplicate", file, `state set ${key} is duplicated`)); const modes = new Map((component.semantic_modes ?? []).map((mode) => [mode.id, mode])); for (const scenario of scenarios) { + const visualImages = scenario.expected?.visual_image ?? []; + for (const id of duplicateValues(visualImages.map((image) => image.environment_id))) failures.push(finding("visual_expectation_duplicate", file, `${scenario.id} repeats visual expectation ${id}`)); + for (const image of visualImages) if (!visualEnvironmentIds.has(image.environment_id)) failures.push(finding("visual_environment_unknown", file, `${scenario.id} references unknown visual environment ${image.environment_id}`)); + for (const id of visualEnvironmentIds) if (!visualImages.some((image) => image.environment_id === id)) failures.push(finding("visual_expectation_missing", file, `${scenario.id} lacks visual expectation ${id}`)); const stateSet = new Set(scenario.states ?? []); const mode = modes.get(scenario.semantic_mode); if (!mode) failures.push(finding("scenario_mode_unknown", file, `${scenario.id} references an unknown semantic mode`)); diff --git a/scripts/create-component-state-session.mjs b/scripts/create-component-state-session.mjs index 6d4e3b3..921fcbe 100644 --- a/scripts/create-component-state-session.mjs +++ b/scripts/create-component-state-session.mjs @@ -5,11 +5,17 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { chromium } from "playwright"; -import { canonicalIntended, sha256 } from "./capture-session-contract.mjs"; +import { + canonicalIntended, + canonicalSourceManifest, + dirtyRelevantSources, + sha256, +} from "./capture-session-contract.mjs"; import { compileSchemas } from "./component-state-contract.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const options = { json: false, output: undefined, @@ -43,6 +49,16 @@ const executable = chromium.executablePath(); const revisionSegment = executable.split(path.sep).find((segment) => /^chromium(?:_headless_shell)?-\d+$/.test(segment)); const packageJson = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "package.json"), "utf8")); const intended = canonicalIntended(options.profileRoot, failures); +const revision = git("rev-parse", "HEAD"); +if (process.env.GITHUB_SHA && process.env.GITHUB_SHA !== revision) failures.push({ code: "capture_revision_mismatch", message: `GITHUB_SHA ${process.env.GITHUB_SHA} does not match checked-out HEAD ${revision}`, path: repositoryRoot }); +let source; +try { + source = canonicalSourceManifest(repositoryRoot, options.profileRoot); + const dirty = dirtyRelevantSources(repositoryRoot, options.profileRoot); + if (dirty.length > 0) failures.push({ code: "capture_source_dirty", message: `relevant capture sources must be committed: ${dirty.join(", ")}`, path: repositoryRoot }); +} catch (error) { + failures.push({ code: "capture_source_unreadable", message: error instanceof Error ? error.message : String(error), path: options.profileRoot }); +} const receipt = { attempt: Number(process.env.GITHUB_RUN_ATTEMPT ?? 1), branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || git("branch", "--show-current") || "detached", @@ -61,9 +77,10 @@ const receipt = { nonce: crypto.randomBytes(32).toString("hex"), record_kind: "component_state_capture_session", repository: process.env.GITHUB_REPOSITORY ?? "changeroa/StyleGallery", - revision: process.env.GITHUB_SHA ?? git("rev-parse", "HEAD"), + revision, schema_version: "1.0", session_id: crypto.randomUUID(), + source, started_at: new Date().toISOString(), }; diff --git a/scripts/evidence-artifact-contract.mjs b/scripts/evidence-artifact-contract.mjs index 85c0e8d..3fe2f66 100644 --- a/scripts/evidence-artifact-contract.mjs +++ b/scripts/evidence-artifact-contract.mjs @@ -1,11 +1,9 @@ -import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { artifactMetadata } from "./artifact-metadata.mjs"; import { sameJson, withinSession } from "./capture-session-contract.mjs"; import { parseStrictJson } from "./strict-json.mjs"; -const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); - function finding(code, file, message) { return { code, message, path: file }; } @@ -28,23 +26,6 @@ function isSafeArtifactPath(reference) { && !reference.split("/").some((segment) => segment === "." || segment === ".."); } -function pngDimensions(bytes) { - if (bytes.length < 24 || !bytes.subarray(0, 8).equals(pngSignature) || bytes.toString("ascii", 12, 16) !== "IHDR") return undefined; - const width = bytes.readUInt32BE(16); - const height = bytes.readUInt32BE(20); - return width > 0 && height > 0 ? { height, width } : undefined; -} - -export function artifactMetadata(bytes, mediaType) { - const metadata = { - byte_length: bytes.length, - media_type: mediaType, - sha256: `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`, - }; - if (mediaType === "image/png") Object.assign(metadata, pngDimensions(bytes)); - return metadata; -} - function validateIntegrity(pass, bytes, evidenceFile, failures) { const actual = artifactMetadata(bytes, pass.artifact.media_type); if (actual.sha256 !== pass.artifact.sha256 || actual.byte_length !== pass.artifact.byte_length) failures.push(finding("evidence_artifact_integrity", evidenceFile, `${pass.id} hash or byte length does not match`)); @@ -84,6 +65,15 @@ function validateAx(document, pass, scenario, fixture, profileId, evidenceFile, } } +function validateVisual(document, pass, scenario, profileId, actual, evidenceFile, failures) { + if (document.schema_version !== "1.0" || document.channel !== "visual" || document.profile_id !== profileId || document.scenario_id !== pass.scenario_id || document.semantic_mode !== scenario.semantic_mode) { + failures.push(finding("evidence_visual_identity_mismatch", evidenceFile, `${pass.id} visual identity does not match its pass`)); + } + if (document.source_sha256 !== pass.session?.source?.sha256) failures.push(finding("capture_source_mismatch", evidenceFile, `${pass.id} visual source digest differs from its capture session`)); + const expectedImage = { path: path.posix.basename(pass.artifact.path), ...actual }; + if (!sameJson(document.image, expectedImage)) failures.push(finding("evidence_visual_image_mismatch", evidenceFile, `${pass.id} visual metadata does not match its PNG`)); +} + function parseArtifactJson(bytes, pass, scenario, fixture, profileId, evidenceFile, failures, schemas, session) { let document; try { @@ -137,6 +127,28 @@ function readArtifact(pass, artifactRoot, evidenceFile, failures) { return fs.readFileSync(realTarget); } +function parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, evidenceFile, failures, schemas, session) { + const reference = pass.artifact.path.replace(/\.png$/, ".visual.json"); + const bytes = readArtifact({ artifact: { path: reference }, id: `${pass.id}-metadata` }, artifactRoot, evidenceFile, failures); + if (!bytes) return undefined; + let document; + try { + document = parseStrictJson(bytes.toString("utf8")); + } catch (error) { + failures.push(finding("evidence_visual_json_invalid", evidenceFile, `${pass.id} visual metadata is not structured JSON: ${error instanceof Error ? error.message : String(error)}`)); + return undefined; + } + if (!schemas.visual(document)) { + for (const error of schemas.visual.errors ?? []) failures.push(finding("evidence_visual_schema_invalid", evidenceFile, `${pass.id} ${error.instancePath || "/"} ${error.message}`)); + return undefined; + } + if (!sameJson(document.capture_session, pass.session)) failures.push(finding("capture_session_mismatch", evidenceFile, `${pass.id} visual metadata session differs from its pass`)); + if (document.captured_at !== pass.recorded_at) failures.push(finding("evidence_recorded_at_mismatch", evidenceFile, `${pass.id} recorded_at is not derived from visual metadata`)); + if (session && !withinSession(document.captured_at, session.started_at, session.completed_at)) failures.push(finding("capture_session_time_outside", evidenceFile, `${pass.id} visual metadata was not captured within its session`)); + validateVisual(document, pass, scenario, profileId, actual, evidenceFile, failures); + return document; +} + export function validateEvidenceArtifacts({ artifactRoot, evidence, evidenceFile, fixture, profileId, schemas, session, states }) { const failures = []; const scenarios = new Map(states.scenarios.map((scenario) => [scenario.id, scenario])); @@ -148,17 +160,23 @@ export function validateEvidenceArtifacts({ artifactRoot, evidence, evidenceFile if (!scenario || !runtimeFixture || !pass.artifact?.media_type) continue; const bytes = readArtifact(pass, artifactRoot, evidenceFile, failures); if (!bytes) continue; - validateIntegrity(pass, bytes, evidenceFile, failures); + const actual = validateIntegrity(pass, bytes, evidenceFile, failures); if (pass.artifact.media_type === "application/json") { const document = parseArtifactJson(bytes, pass, scenario, runtimeFixture, profileId, evidenceFile, failures, schemas, session); if (document?.captured_at) { if (!capturedTimes.has(pass.scenario_id)) capturedTimes.set(pass.scenario_id, new Set()); capturedTimes.get(pass.scenario_id).add(document.captured_at); } + } else if (pass.channel === "visual") { + const document = parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, evidenceFile, failures, schemas, session); + if (document?.captured_at) { + if (!capturedTimes.has(pass.scenario_id)) capturedTimes.set(pass.scenario_id, new Set()); + capturedTimes.get(pass.scenario_id).add(document.captured_at); + } } } for (const [scenarioId, times] of capturedTimes) { - if (times.size !== 1) failures.push(finding("capture_session_time_mismatch", evidenceFile, `${scenarioId} DOM and AX captured_at values differ`)); + if (times.size !== 1) failures.push(finding("capture_session_time_mismatch", evidenceFile, `${scenarioId} visual, DOM, and AX captured_at values differ`)); const capturedAt = [...times][0]; for (const pass of evidence.passes.filter((entry) => entry.scenario_id === scenarioId)) if (pass.recorded_at !== capturedAt) failures.push(finding("evidence_recorded_at_mismatch", evidenceFile, `${pass.id} recorded_at is not derived from its scenario artifacts`)); } diff --git a/scripts/finalize-component-state-evidence.mjs b/scripts/finalize-component-state-evidence.mjs index b57ed03..07051ca 100644 --- a/scripts/finalize-component-state-evidence.mjs +++ b/scripts/finalize-component-state-evidence.mjs @@ -2,13 +2,19 @@ import fs from "node:fs"; import path from "node:path"; -import { artifactMetadata } from "./evidence-artifact-contract.mjs"; -import { canonicalIntended, readCaptureSession, sameJson } from "./capture-session-contract.mjs"; +import { fileURLToPath } from "node:url"; +import { artifactMetadata } from "./artifact-metadata.mjs"; +import { + canonicalIntended, + readCaptureSession, + sameJson, + sourceManifestMatches, +} from "./capture-session-contract.mjs"; import { compileSchemas, validateEvidenceArtifacts } from "./component-state-contract.mjs"; import { resolveProfileRecords } from "./profile-record-contract.mjs"; import { parseStrictJson } from "./strict-json.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const options = { artifactRoot: undefined, json: false, @@ -45,6 +51,7 @@ const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/sch const capture = receiptFile ? readCaptureSession(receiptFile, schemas.capture, failures) : undefined; const intended = canonicalIntended(options.profileRoot, failures); if (capture && !sameJson(capture.receipt.intended, intended)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from canonical profile scenarios", path: receiptFile }); +if (capture && !sourceManifestMatches(capture.receipt.source, repositoryRoot, options.profileRoot)) failures.push({ code: "capture_source_drift", message: "capture sources differ from the receipt source manifest", path: receiptFile }); const completedAt = new Date().toISOString(); const completedSession = capture ? { @@ -59,6 +66,7 @@ const completedSession = capture ? { repository: capture.receipt.repository, revision: capture.receipt.revision, session_id: capture.receipt.session_id, + source: capture.receipt.source, started_at: capture.receipt.started_at, } : undefined; const run = capture ? { @@ -86,13 +94,14 @@ for (const profileIntent of intended) { for (const scenario of fixture.scenarios) { const canonical = stateById.get(scenario.id); const documents = {}; - for (const channel of ["dom", "ax"]) { + for (const channel of ["dom", "ax", "visual"]) { const file = path.join(options.artifactRoot, "runtime", `${profileIntent.profile_name}-${scenario.id}.${channel}.json`); try { documents[channel] = parseStrictJson(fs.readFileSync(file, "utf8")); } catch (error) { failures.push({ code: "evidence_json_invalid", message: error instanceof Error ? error.message : String(error), path: file }); } + if (channel === "visual") expectedFiles.add(path.basename(file)); } const capturedTimes = new Set(Object.values(documents).map((document) => document?.captured_at).filter(Boolean)); - if (capturedTimes.size !== 1) failures.push({ code: "capture_session_time_mismatch", message: `${scenario.id} DOM and AX must share one captured_at`, path: profileRoot }); + if (capturedTimes.size !== 1) failures.push({ code: "capture_session_time_mismatch", message: `${scenario.id} visual, DOM, and AX must share one captured_at`, path: profileRoot }); const recordedAt = [...capturedTimes][0] ?? ""; for (const channel of scenario.required_channels) { const suffix = channelFiles[channel]; diff --git a/scripts/generate-consumer-reference-evidence.mjs b/scripts/generate-consumer-reference-evidence.mjs index 49766f4..5eb437b 100644 --- a/scripts/generate-consumer-reference-evidence.mjs +++ b/scripts/generate-consumer-reference-evidence.mjs @@ -2,9 +2,10 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { resolveProfileRecords } from "./profile-record-contract.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const options = { check: false, json: false, root: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local") }; const failures = []; for (let index = 2; index < process.argv.length; index += 1) { diff --git a/scripts/test-component-state-source-contract.mjs b/scripts/test-component-state-source-contract.mjs new file mode 100644 index 0000000..594f208 --- /dev/null +++ b/scripts/test-component-state-source-contract.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + canonicalSourceManifest, + sourceManifestMatches, +} from "./capture-session-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const profileRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); +const first = canonicalSourceManifest(repositoryRoot, profileRoot); +const second = canonicalSourceManifest(repositoryRoot, profileRoot); + +assert.equal(first.sha256, second.sha256, "source manifest digest must be deterministic"); +assert.deepEqual(first.files, second.files, "source manifest files must be stable"); +assert(first.files.some((entry) => entry.path === "tests/component-state-evidence.spec.mjs"), "capture browser code must be bound"); +assert(first.files.some((entry) => entry.path === "playwright.config.mjs"), "Playwright capture configuration must be bound"); +assert(first.files.some((entry) => entry.path === "package.json"), "capture dependency declarations must be bound"); +assert(first.files.some((entry) => entry.path === "package-lock.json"), "exact capture dependencies must be bound"); +assert(first.files.some((entry) => entry.path === "scripts/artifact-metadata.mjs"), "capture artifact metadata code must be bound"); +assert(first.files.some((entry) => entry.path === "scripts/visual-expectation-contract.mjs"), "visual expectation selection must be bound"); +assert(!first.files.some((entry) => entry.path === "scripts/finalize-component-state-evidence.mjs"), "post-capture finalization must not invalidate captured source identity"); +assert(!first.files.some((entry) => entry.path === "scripts/validate-component-state.mjs"), "post-capture enforcement must not invalidate captured source identity"); +assert(first.files.some((entry) => entry.path.endsWith("/profile.json")), "profile declarations must be bound"); +assert(first.files.some((entry) => entry.path.endsWith("/tokens.dtcg.json")), "profile token inputs must be bound"); +assert.equal(sourceManifestMatches(first, repositoryRoot, profileRoot), true, "canonical source manifest must match itself"); + +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-source-contract-")); +try { + const copiedProfiles = path.join(tempRoot, "profiles"); + const copiedRepository = path.join(tempRoot, "repository"); + fs.cpSync(profileRoot, copiedProfiles, { recursive: true }); + for (const entry of first.files.filter((candidate) => !candidate.path.startsWith("profiles/"))) { + const source = path.join(repositoryRoot, entry.path); + const target = path.join(copiedRepository, entry.path); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(source, target); + } + const copied = canonicalSourceManifest(copiedRepository, copiedProfiles); + assert.equal(copied.sha256, first.sha256, "profile-root location must not change the source digest"); + const tokenFile = path.join(copiedProfiles, "editorial/tokens.dtcg.json"); + fs.appendFileSync(tokenFile, "\n"); + assert.equal(sourceManifestMatches(copied, copiedRepository, copiedProfiles), false, "profile source drift must invalidate the manifest"); + fs.copyFileSync(path.join(profileRoot, "editorial/tokens.dtcg.json"), tokenFile); + const configFile = path.join(copiedRepository, "playwright.config.mjs"); + const config = fs.readFileSync(configFile, "utf8").replace("viewport: { height: 768, width: 1024 },", "deviceScaleFactor: 2,\n viewport: { height: 768, width: 1024 },"); + fs.writeFileSync(configFile, config); + assert.equal(sourceManifestMatches(copied, copiedRepository, copiedProfiles), false, "deviceScaleFactor drift must invalidate the source manifest before it can double screenshot pixels"); +} finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); +} + +process.stdout.write(`${JSON.stringify({ files: first.files.length, ok: true, sha256: first.sha256 }, null, 2)}\n`); diff --git a/scripts/test-validate-component-state-artifacts.mjs b/scripts/test-validate-component-state-artifacts.mjs index 7f2f946..603a714 100644 --- a/scripts/test-validate-component-state-artifacts.mjs +++ b/scripts/test-validate-component-state-artifacts.mjs @@ -4,16 +4,56 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import { artifactMetadata } from "./evidence-artifact-contract.mjs"; +import { fileURLToPath } from "node:url"; +import zlib from "node:zlib"; +import { artifactMetadata } from "./artifact-metadata.mjs"; import { sessionLink, sha256 } from "./capture-session-contract.mjs"; import { resolveProfileRecords } from "./profile-record-contract.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const sourceProfiles = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); const validator = path.join(repositoryRoot, "scripts/validate-component-state.mjs"); const finalizer = path.join(repositoryRoot, "scripts/finalize-component-state-evidence.mjs"); const creator = path.join(repositoryRoot, "scripts/create-component-state-session.mjs"); -const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64"); + +function crc32(bytes) { + let crc = 0xffffffff; + for (const byte of bytes) { + crc ^= byte; + for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); + } + return (crc ^ 0xffffffff) >>> 0; +} + +function pngChunk(type, data) { + const name = Buffer.from(type, "ascii"); + const output = Buffer.alloc(12 + data.length); + output.writeUInt32BE(data.length, 0); + name.copy(output, 4); + data.copy(output, 8); + output.writeUInt32BE(crc32(Buffer.concat([name, data])), 8 + data.length); + return output; +} + +function makePng(red) { + const width = 64; + const height = 64; + const header = Buffer.alloc(13); + header.writeUInt32BE(width, 0); + header.writeUInt32BE(height, 4); + header[8] = 8; + header[9] = 6; + const row = Buffer.alloc(1 + (width * 4)); + for (let pixel = 0; pixel < width; pixel += 1) row.set([red, 64, 128, 255], 1 + (pixel * 4)); + return Buffer.concat([ + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), + pngChunk("IHDR", header), + pngChunk("IDAT", zlib.deflateSync(Buffer.concat(Array.from({ length: height }, () => row)))), + pngChunk("IEND", Buffer.alloc(0)), + ]); +} + +const png = makePng(32); function writeJson(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); @@ -68,12 +108,28 @@ function prepareCanonical() { semantic_mode: scenario.semantic_mode, }); fs.mkdirSync(path.dirname(`${prefix}.png`), { recursive: true }); - fs.writeFileSync(`${prefix}.png`, Buffer.concat([png, Buffer.from(`${entry.name}:${fixture.id}`)])); + const image = Buffer.concat([png, Buffer.from(`${entry.name}:${fixture.id}`)]); + fs.writeFileSync(`${prefix}.png`, image); + writeJson(`${prefix}.visual.json`, { + capture_session: captureLink, + captured_at: capturedAt, + channel: "visual", + image: { path: `${entry.name}-${fixture.id}.png`, ...artifactMetadata(image, "image/png") }, + profile_id: resolved.profile.id, + scenario_id: fixture.id, + schema_version: "1.0", + semantic_mode: scenario.semantic_mode, + source_sha256: receipt.source.sha256, + }); } } const manifest = path.join(artifactRoot, "runtime-manifest.json"); - const child = spawnSync(process.execPath, [finalizer, "--root", profileRoot, "--artifact-root", artifactRoot, "--output", manifest, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + const child = spawnSync(process.execPath, [finalizer, "--root", profileRoot, "--artifact-root", artifactRoot, "--output", manifest, "--write-canonical", "--json"], { cwd: repositoryRoot, encoding: "utf8" }); if (child.status !== 0) throw new Error(`canonical artifact finalization failed: ${child.stdout}${child.stderr}`); + const finalization = JSON.parse(child.stdout); + if (finalization.artifactCount !== 40) throw new Error(`canonical finalization must close 40 runtime files, found ${finalization.artifactCount}`); + const passCount = JSON.parse(fs.readFileSync(manifest, "utf8")).records.flatMap((record) => record.passes).length; + if (passCount !== 30) throw new Error(`canonical finalization must retain 30 channel passes, found ${passCount}`); return { artifactRoot, manifest, profileRoot, root }; } @@ -106,6 +162,7 @@ const cases = [ { name: "canonical", valid: true }, { expect: "evidence_channel_duplicate", name: "channel_substitution", mutate: (m) => { findPass(m, "action-focused", "ax").channel = "dom"; } }, { expect: "evidence_png_invalid", name: "dummy_visual_bytes", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); fs.writeFileSync(path.join(a, p.artifact.path), "not a png"); refresh(p, a, true); } }, + { expect: "evidence_visual_image_mismatch", name: "wrong_valid_png", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); fs.writeFileSync(path.join(a, p.artifact.path), makePng(224)); refresh(p, a); } }, { expect: "evidence_json_invalid", name: "png_as_dom", mutate: (m, a) => { const p = findPass(m, "action-focused", "dom"); fs.writeFileSync(path.join(a, p.artifact.path), png); refresh(p, a); } }, { expect: "evidence_artifact_symlink", name: "artifact_symlink", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); const f = path.join(a, p.artifact.path); fs.rmSync(f); fs.symlinkSync("/etc/hosts", f); } }, { expect: "evidence_artifact_reused", name: "artifact_path_reuse", mutate: (m) => { const p = findPass(m, "action-loading-busy", "visual"); p.artifact = structuredClone(findPass(m, "action-focused", "visual").artifact); } }, @@ -128,8 +185,10 @@ const cases = [ { expect: "evidence_ax_schema_invalid", name: "ax_null_type", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { d.properties.focused = null; }); } }, { expect: "evidence_ax_schema_invalid", name: "ax_missing_key", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { delete d.role; }); } }, { expect: "capture_session_receipt_mismatch", name: "receipt_tamper", mutate: (m, a) => { const f = path.join(a, "capture-session.json"); const r = JSON.parse(fs.readFileSync(f)); r.nonce = "0".repeat(64); writeJson(f, r); } }, + { expect: "capture_session_receipt_mismatch", name: "isolated_session_replay", mutate: (_m, a) => { fs.copyFileSync(path.join(alternate.artifactRoot, "capture-session.json"), path.join(a, "capture-session.json")); } }, { expect: "capture_session_missing", name: "receipt_missing", mutate: (m, a) => { fs.rmSync(path.join(a, "capture-session.json")); } }, { expect: "capture_session_mismatch", name: "cross_session_artifact_mix", mutate: (m, a) => { const target = findPass(m, "action-focused", "dom"); const sourceManifest = JSON.parse(fs.readFileSync(alternate.manifest)); const source = findPass(sourceManifest, "action-focused", "dom"); fs.copyFileSync(path.join(alternate.artifactRoot, source.artifact.path), path.join(a, target.artifact.path)); refresh(target, a); } }, + { expect: "capture_session_mismatch", name: "visual_cross_session_replay", mutate: (m, a) => { const target = findPass(m, "action-focused", "visual"); const sourceManifest = JSON.parse(fs.readFileSync(alternate.manifest)); const source = findPass(sourceManifest, "action-focused", "visual"); fs.copyFileSync(path.join(alternate.artifactRoot, source.artifact.path), path.join(a, target.artifact.path)); fs.copyFileSync(path.join(alternate.artifactRoot, source.artifact.path.replace(/\.png$/, ".visual.json")), path.join(a, target.artifact.path.replace(/\.png$/, ".visual.json"))); refresh(target, a); } }, { expect: "capture_session_mismatch", name: "artifact_revision_drift", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.capture_session.revision = "0".repeat(40); }); } }, { expect: "capture_session_mismatch", name: "artifact_environment_drift", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.capture_session.environment.browser = "Different browser"; }); } }, { expect: "capture_session_mismatch", name: "artifact_attempt_drift", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { d.capture_session.attempt += 1; }); } }, @@ -137,6 +196,7 @@ const cases = [ { expect: "capture_session_time_outside", name: "completed_before_started", mutate: (m) => { m.session.completed_at = "2000-01-01T00:00:00.000Z"; m.recorded_at = m.session.completed_at; } }, { expect: "at_evidence_unverified", name: "fake_at", mutate: (m) => { findPass(m, "action-focused", "ax").channel = "at"; } }, { expect: "runtime_manifest_identity_mismatch", name: "run_identity_mismatch", mutate: (m) => { findPass(m, "action-focused", "dom").run.id = "different-run"; } }, + { expect: "capture_source_drift", name: "source_drift", mutate: (_m, _a, root) => { fs.appendFileSync(path.join(root, "profiles/editorial/tokens.dtcg.json"), "\n"); } }, ]; const base = prepareCanonical(); @@ -148,13 +208,22 @@ const results = cases.map((testCase) => { const artifactRoot = path.join(root, "artifacts"); const manifestFile = path.join(artifactRoot, "runtime-manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestFile)); - testCase.mutate?.(manifest, artifactRoot); + testCase.mutate?.(manifest, artifactRoot, root); writeJson(manifestFile, manifest); const child = spawnSync(process.execPath, [validator, "--root", path.join(root, "profiles"), "--artifact-root", artifactRoot, "--runtime-manifest", manifestFile, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); const output = JSON.parse(child.stdout); const actual = codes(output); const ok = testCase.valid ? child.status === 0 && output.ok === true : child.status !== 0 && output.ok === false && actual.includes(testCase.expect); - return { actual: { codes: actual, status: child.status }, expected: testCase.expect ?? "valid", name: testCase.name, ok }; + return { + actual: { + codes: actual, + ...(testCase.valid && !ok ? { findings: output.failures.slice(0, 10) } : {}), + status: child.status, + }, + expected: testCase.expect ?? "valid", + name: testCase.name, + ok, + }; } finally { fs.rmSync(root, { force: true, recursive: true }); } @@ -168,6 +237,25 @@ const closedOutput = JSON.parse(closed.stdout); results.push({ actual: { codes: codes(closedOutput), status: closed.status }, expected: "runtime_artifact_unmanifested", name: "unmanifested_closed_set", ok: closed.status !== 0 && codes(closedOutput).includes("runtime_artifact_unmanifested") }); results.push({ actual: { codes: codes(closedOutput), status: closed.status }, expected: "capture_session_replay", name: "session_replay", ok: closed.status !== 0 && codes(closedOutput).includes("capture_session_replay") }); fs.rmSync(closedRoot, { force: true, recursive: true }); + +const revisionRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-state-revision-")); +try { + const receiptFile = path.join(revisionRoot, "capture-session.json"); + const child = spawnSync(process.execPath, [creator, "--root", sourceProfiles, "--output", receiptFile, "--json"], { + cwd: repositoryRoot, + encoding: "utf8", + env: { ...process.env, GITHUB_SHA: "0".repeat(40) }, + }); + const output = JSON.parse(child.stdout); + results.push({ + actual: { codes: codes(output), receiptWritten: fs.existsSync(receiptFile), status: child.status }, + expected: "capture_revision_mismatch", + name: "github_sha_mismatch", + ok: child.status !== 0 && codes(output).includes("capture_revision_mismatch") && !fs.existsSync(receiptFile), + }); +} finally { + fs.rmSync(revisionRoot, { force: true, recursive: true }); +} fs.rmSync(base.root, { force: true, recursive: true }); fs.rmSync(alternate.root, { force: true, recursive: true }); diff --git a/scripts/test-validate-component-state.mjs b/scripts/test-validate-component-state.mjs index 326981b..37c988a 100644 --- a/scripts/test-validate-component-state.mjs +++ b/scripts/test-validate-component-state.mjs @@ -59,6 +59,9 @@ const cases = [ ["duplicate_mode", "semantic_mode_duplicate", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.semantic_modes.push(structuredClone(value.semantic_modes[0])); })], ["duplicate_pass", "evidence_pass_duplicate", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { value.passes.push(structuredClone(value.passes[0])); })], ["duplicate_state_set", "state_set_duplicate", (root) => edit(root, "editorial", "states/button.states.json", (value) => { const copy = structuredClone(value.scenarios[0]); copy.id = "duplicate-disabled-busy"; copy.states.reverse(); value.scenarios.push(copy); })], + ["duplicate_visual_environment", "visual_environment_duplicate", (root) => edit(root, "editorial", "states/button.states.json", (value) => { value.visual_environments.push(structuredClone(value.visual_environments[0])); })], + ["missing_visual_expectation", "visual_expectation_missing", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-focused").expected.visual_image.pop(); })], + ["unknown_visual_environment", "visual_environment_unknown", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-focused").expected.visual_image[0].environment_id = "unknown-environment"; })], ["pressed_surface", "pressed_surface_mismatch", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "toggle-focused-pressed").expected.dom["aria-pressed"] = "false"; })], ["busy_surface", "busy_surface_mismatch", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-loading-busy").expected.ax.busy = false; })], ["disabled_surface", "disabled_surface_mismatch", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-disabled-busy").expected.dom.disabled = "false"; })], diff --git a/scripts/validate-component-state.mjs b/scripts/validate-component-state.mjs index 20d9e7d..4952d32 100644 --- a/scripts/validate-component-state.mjs +++ b/scripts/validate-component-state.mjs @@ -2,10 +2,17 @@ import fs from "node:fs"; import path from "node:path"; -import { canonicalIntended, readCaptureSession, sameJson, withinSession } from "./capture-session-contract.mjs"; +import { fileURLToPath } from "node:url"; +import { + canonicalIntended, + readCaptureSession, + sameJson, + sourceManifestMatches, + withinSession, +} from "./capture-session-contract.mjs"; import { compileSchemas, readRecord, validateEvidenceArtifacts, validateProfile } from "./component-state-contract.mjs"; -const repositoryRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const options = { artifactRoot: undefined, json: false, root: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), runtimeManifest: undefined }; const failures = []; for (let index = 2; index < process.argv.length; index += 1) { @@ -35,12 +42,13 @@ if (options.runtimeManifest) { const session = runtimeManifest.session; if (!session || !sameJson(session.receipt, capture.artifact) || session.receipt_sha256 !== capture.digest) failures.push({ code: "capture_session_receipt_mismatch", message: "manifest receipt metadata does not match capture-session.json", path: options.runtimeManifest }); const expectedLink = capture.link; - for (const key of ["session_id", "nonce", "started_at", "revision", "branch", "attempt", "environment"]) if (!sameJson(session?.[key], expectedLink[key])) failures.push({ code: "capture_session_mismatch", message: `manifest ${key} differs from receipt`, path: options.runtimeManifest }); + for (const key of ["session_id", "nonce", "started_at", "revision", "branch", "attempt", "environment", "source"]) if (!sameJson(session?.[key], expectedLink[key])) failures.push({ code: "capture_session_mismatch", message: `manifest ${key} differs from receipt`, path: options.runtimeManifest }); for (const key of ["repository", "intended"]) if (!sameJson(session?.[key], capture.receipt[key])) failures.push({ code: "capture_session_mismatch", message: `manifest ${key} differs from receipt`, path: options.runtimeManifest }); if (runtimeManifest.recorded_at !== session?.completed_at) failures.push({ code: "evidence_recorded_at_mismatch", message: "manifest recorded_at must equal session completed_at", path: options.runtimeManifest }); if (!withinSession(session?.completed_at, capture.receipt.started_at, session?.completed_at)) failures.push({ code: "capture_session_time_outside", message: "completed_at precedes session start", path: options.runtimeManifest }); const canonical = canonicalIntended(options.root, failures); if (!sameJson(capture.receipt.intended, canonical)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from current canonical records", path: options.runtimeManifest }); + if (!sourceManifestMatches(capture.receipt.source, repositoryRoot, options.root)) failures.push({ code: "capture_source_drift", message: "current capture sources differ from the receipt source manifest", path: options.runtimeManifest }); const run = runtimeManifest.run; if (run?.id !== capture.receipt.session_id || run?.repository !== capture.receipt.repository || run?.revision !== capture.receipt.revision || run?.attempt !== capture.receipt.attempt) failures.push({ code: "capture_session_mismatch", message: "manifest run differs from receipt", path: options.runtimeManifest }); for (const record of runtimeManifest.records ?? []) { @@ -69,8 +77,12 @@ if (runtimeManifest) { for (const record of runtimeManifest.records ?? []) if (!known.has(record.profile_id)) failures.push({ code: "runtime_manifest_profile_unknown", message: `${record.profile_id} is not a governed profile`, path: options.runtimeManifest }); } if (runtimeManifest && options.artifactRoot) { - const expected = new Set((runtimeManifest.records ?? []).flatMap((record) => (record.passes ?? []).map((pass) => pass.artifact?.path)).filter(Boolean)); - if (expected.size !== 30) failures.push({ code: "runtime_artifact_count_mismatch", message: `manifest requires exactly 30 unique artifacts, found ${expected.size}`, path: options.runtimeManifest }); + const passes = (runtimeManifest.records ?? []).flatMap((record) => record.passes ?? []); + const passArtifacts = new Set(passes.map((pass) => pass.artifact?.path).filter(Boolean)); + if (passArtifacts.size !== 30) failures.push({ code: "runtime_channel_count_mismatch", message: `manifest requires exactly 30 unique channel artifacts, found ${passArtifacts.size}`, path: options.runtimeManifest }); + const expected = new Set(passArtifacts); + for (const pass of passes) if (pass.channel === "visual" && pass.artifact?.path?.endsWith(".png")) expected.add(pass.artifact.path.replace(/\.png$/, ".visual.json")); + if (expected.size !== 40) failures.push({ code: "runtime_artifact_count_mismatch", message: `manifest requires exactly 40 runtime files, found ${expected.size}`, path: options.runtimeManifest }); const runtimeRoot = path.join(options.artifactRoot, "runtime"); const actual = fs.existsSync(runtimeRoot) ? fs.readdirSync(runtimeRoot, { withFileTypes: true }) : []; for (const entry of actual) { diff --git a/scripts/visual-expectation-contract.mjs b/scripts/visual-expectation-contract.mjs new file mode 100644 index 0000000..f0c649a --- /dev/null +++ b/scripts/visual-expectation-contract.mjs @@ -0,0 +1,20 @@ +const environmentFields = Object.freeze([ + "browser", + "browser_revision", + "container_image", + "platform", + "playwright", + "viewport", +]); + +function matchesEnvironment(approved, environment) { + return environmentFields.every((field) => approved[field] === environment[field]); +} + +export function visualExpectationFor(scenario, environment, approvedEnvironments) { + const environments = approvedEnvironments.filter((approved) => matchesEnvironment(approved, environment)); + if (environments.length !== 1) throw new Error(`capture environment requires exactly one approved visual environment, found ${environments.length}`); + const matches = scenario.expected.visual_image.filter((expectation) => expectation.environment_id === environments[0].id); + if (matches.length !== 1) throw new Error(`scenario ${scenario.id} requires exactly one approved visual expectation for ${environments[0].id}, found ${matches.length}`); + return matches[0]; +} diff --git a/tests/component-state-evidence.spec.mjs b/tests/component-state-evidence.spec.mjs index 9b3e57c..57bd864 100644 --- a/tests/component-state-evidence.spec.mjs +++ b/tests/component-state-evidence.spec.mjs @@ -1,16 +1,20 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { expect, test } from "@playwright/test"; +import { artifactMetadata } from "../scripts/artifact-metadata.mjs"; import { readCaptureSession } from "../scripts/capture-session-contract.mjs"; import { compileSchemas } from "../scripts/component-state-contract.mjs"; +import { visualExpectationFor } from "../scripts/visual-expectation-contract.mjs"; import { componentStateFixture, componentStateProfiles, renderComponentState } from "./helpers/render-component-state.mjs"; const mutation = process.env.STATE_MUTATION ?? "none"; const artifactRoot = process.env.STATE_ARTIFACT_DIR; const sessionFile = process.env.STATE_SESSION_RECEIPT; -const playwrightVersion = JSON.parse(fs.readFileSync(path.resolve(path.dirname(new URL(import.meta.url).pathname), "../package.json"), "utf8")).devDependencies["@playwright/test"]; +const testRoot = path.dirname(fileURLToPath(import.meta.url)); +const playwrightVersion = JSON.parse(fs.readFileSync(path.resolve(testRoot, "../package.json"), "utf8")).devDependencies["@playwright/test"]; const sessionFailures = []; -const schemas = compileSchemas(path.resolve(path.dirname(new URL(import.meta.url).pathname), "../consumer-reference/schema")); +const schemas = compileSchemas(path.resolve(testRoot, "../consumer-reference/schema")); const captureSession = sessionFile ? readCaptureSession(path.resolve(sessionFile), schemas.capture, sessionFailures) : undefined; if (artifactRoot && (!captureSession || sessionFailures.length > 0)) throw new Error(`valid STATE_SESSION_RECEIPT is required for capture: ${JSON.stringify(sessionFailures)}`); @@ -67,9 +71,27 @@ async function captureEvidence(page, fixture, scenario, session) { const control = page.locator("#control"); const stem = `${fixture.profileName}-${scenario.id}`; const png = await page.locator(".capture").screenshot({ animations: "disabled", caret: "hide" }); + const image = { path: `${stem}.png`, ...artifactMetadata(png, "image/png") }; + const expectation = visualExpectationFor(scenario, captureSession.receipt.environment, fixture.visualEnvironments); + expect(image.sha256, "visual_image_hash_mismatch").toBe(expectation.sha256); + expect(image.width, "visual_image_width_mismatch").toBe(expectation.width); + expect(image.height, "visual_image_height_mismatch").toBe(expectation.height); + const visual = { + capture_session: captureSession.link, + captured_at: capturedAt, + channel: "visual", + image, + profile_id: fixture.profileId, + scenario_id: scenario.id, + schema_version: "1.0", + semantic_mode: scenario.semantic_mode, + source_sha256: captureSession.receipt.source.sha256, + }; + expect(schemas.visual(visual), JSON.stringify(schemas.visual.errors)).toBe(true); const dom = await domObservation(control, fixture, scenario, capturedAt, captureSession.link); const ax = await axObservation(session, fixture, scenario, capturedAt, captureSession.link); fs.writeFileSync(path.join(artifactRoot, `${stem}.png`), png); + fs.writeFileSync(path.join(artifactRoot, `${stem}.visual.json`), `${JSON.stringify(visual, null, 2)}\n`); fs.writeFileSync(path.join(artifactRoot, `${stem}.dom.json`), `${JSON.stringify(dom, null, 2)}\n`); fs.writeFileSync(path.join(artifactRoot, `${stem}.ax.json`), `${JSON.stringify(ax, null, 2)}\n`); } diff --git a/tests/helpers/render-component-state.mjs b/tests/helpers/render-component-state.mjs index df3cf15..01a759b 100644 --- a/tests/helpers/render-component-state.mjs +++ b/tests/helpers/render-component-state.mjs @@ -69,6 +69,7 @@ export function componentStateFixture(profileName) { page: cssDimension(tokens.space.page.$value), rhythm: cssDimension(tokenValue(tokens, foundations.bindings.content_rhythm)), }, + visualEnvironments: states.visual_environments, }; } From 0e4603bddb187c9d61ad29177cf239f6aceead66 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 07:42:30 +0900 Subject: [PATCH 15/44] Enforce hardened reference workflows --- .github/workflows/validate.yml | 15 ++++++ GOVERNANCE.md | 2 +- quality/evidence/executable-evidence.md | 2 +- scripts/governance-policy-contract.mjs | 48 +++++++++++++++++++ scripts/governance-test-fixture.mjs | 25 ++++++---- scripts/test-validate-governance.mjs | 29 ++++++++++- scripts/validate-governance.mjs | 64 ++++++------------------- scripts/workflow-action-contract.mjs | 17 +++++++ 8 files changed, 140 insertions(+), 62 deletions(-) create mode 100644 scripts/governance-policy-contract.mjs create mode 100644 scripts/workflow-action-contract.mjs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 748f44e..0c2ecbf 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -51,6 +53,7 @@ jobs: node -c scripts/governed-profile-registry.mjs node -c scripts/promotion-attestation-contract.mjs node -c scripts/governance-test-fixture.mjs + node -c scripts/workflow-action-contract.mjs node -c scripts/validate-governance.mjs node -c scripts/test-validate-governance.mjs node -c scripts/validate-domains.mjs @@ -59,10 +62,12 @@ jobs: node -c scripts/test-validate-webpage-workflow.mjs node -c scripts/validate-consumer-reference.mjs node -c scripts/test-validate-consumer-reference.mjs + node -c scripts/consumer-reference-case-runner.mjs node -c scripts/validate-promotion-rfc.mjs node -c scripts/test-validate-promotion-rfc.mjs node -c consumer-reference/adapters/style-dictionary.config.mjs node -c scripts/reference-token-contract.mjs + node -c scripts/reference-artifact-path-contract.mjs node -c scripts/build-reference-artifacts.mjs node -c scripts/validate-reference-artifacts.mjs node -c scripts/test-reference-adapters.mjs @@ -74,6 +79,7 @@ jobs: node -c scripts/calibration-raw-contract.mjs node -c scripts/validate-baseline-manifest.mjs node -c scripts/test-validate-baseline-manifest.mjs + node -c scripts/baseline-schema-parity.mjs node -c scripts/summarize-sentinel-calibration.mjs node -c scripts/test-summarize-sentinel-calibration.mjs node -c scripts/validate-renderer-purity.mjs @@ -89,6 +95,7 @@ jobs: node -c scripts/validate-component-state.mjs node -c scripts/test-validate-component-state.mjs node -c scripts/test-validate-component-state-artifacts.mjs + node -c scripts/test-component-state-source-contract.mjs node -c scripts/test-component-state-sentinel.mjs node -c scripts/generate-consumer-reference-evidence.mjs node -c scripts/test-generate-consumer-reference-evidence.mjs @@ -100,6 +107,7 @@ jobs: node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/capture-session.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/dom-evidence.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/ax-evidence.schema.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/visual-evidence.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/fixture-manifest.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/evidence-record.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/runtime-evidence-manifest.schema.json"))' @@ -164,6 +172,7 @@ jobs: - name: Compound component state and evidence integrity run: | + node scripts/test-component-state-source-contract.mjs node scripts/validate-component-state.mjs --json node scripts/test-validate-component-state.mjs --json node scripts/generate-consumer-reference-evidence.mjs --check --json @@ -198,6 +207,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Setup Node 22 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -225,6 +236,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Setup Node 22 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -281,6 +294,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Setup Node 22 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 46bee28..58d7344 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -67,7 +67,7 @@ Portable token artifacts are regenerated only from the restricted fixture throug Component-state evidence matrices are regenerated only from the records declared by each `profile.json`. Run `node scripts/generate-consumer-reference-evidence.mjs --json`; never hand-edit any of the six matrices or substitute undeclared record paths. -Browser state evidence begins with `scripts/create-component-state-session.mjs`. Its closed receipt binds a random nonce and session ID to the checked-out revision, branch, attempt, exact runtime pins, viewport, and intended profile/scenario/channel set before capture. DOM and AX artifacts embed the receipt digest and session identity; the finalizer derives pass timestamps from those artifacts and binds the visual artifact hash to the same scenario triple. Validation uses the receipt and completed manifest interval, not a wall-clock maximum age, so an unchanged downloaded session remains verifiable later. +Browser state evidence begins with `scripts/create-component-state-session.mjs`. Its closed receipt binds a random nonce and session ID to the checked-out revision, branch, attempt, exact runtime pins, viewport, intended profile/scenario/channel set, and a deterministic digest of the relevant capture sources before capture. Receipt creation rejects dirty relevant sources. DOM and AX artifacts embed the receipt digest and session identity; browser-authored visual sidecars bind the same session, scenario, capture time, source digest, PNG bytes, dimensions, and hash. Finalization accepts exactly 30 channel passes over the closed 40-file runtime set. Validation uses the receipt and completed manifest interval, not a wall-clock maximum age, so an unchanged downloaded session remains verifiable later. ## Lifecycle States diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index 8db2787..ece7b33 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -33,7 +33,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract. | `scripts/validate-promotion-rfc.mjs` and `scripts/test-validate-promotion-rfc.mjs` | `node scripts/validate-promotion-rfc.mjs --json`; `node scripts/test-validate-promotion-rfc.mjs --json` | The manifest-declared policy and two canonical examples pass with deferred decisions, zero attestations, exact support commitment, and no promotion claim. | Manifest-declared invalid fixtures plus forged, unregistered, duplicate, path, extension, support, evidence, canonical-drift, and lifecycle mutations exit non-zero with named findings. | Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision; the current canonical inventory supplies no real independent consumers. | | Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | | The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | -| Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles. | `scripts/create-component-state-session.mjs`, `scripts/finalize-component-state-evidence.mjs`, `scripts/validate-component-state.mjs`, and the component-state source, contract, artifact, generator, and sentinel harnesses. | `npm run test:component-state`; `npm run validate:component-state`; run the digest-pinned component-state browser capture and `npm run test:component-state:runtime-negative`. | Editorial and terminal each declare default, hover, focus-visible, active, and disabled scenarios; the generated state, keyboard, and evidence matrices match their records; one clean capture session binds the browser artifacts to the governed source inventory. | Missing or altered source bindings, invalid state/profile records, scenario drift, malformed or replayed runtime artifacts, hash/count mismatch, insufficient focus clearance, and sentinel mutations must exit non-zero with named findings. | Proves the declared examples and captured channels agree for the pinned browser environment. It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval; synthetic negative fixtures are not authenticated provenance. | +| Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles. | `scripts/create-component-state-session.mjs`, `scripts/finalize-component-state-evidence.mjs`, `scripts/validate-component-state.mjs`, and the component-state source, contract, artifact, generator, and sentinel harnesses. | `npm run test:component-state`; `npm run validate:component-state`; run the digest-pinned component-state browser capture and `npm run test:component-state:runtime-negative`. | Editorial and terminal each declare the five compound scenarios `action-disabled-busy`, `action-focused`, `action-loading-busy`, `disclosure-expanded-loading`, and `toggle-focused-pressed`; their environment-scoped approved PNG hashes and dimensions are canonical pre-capture inputs; the generated state, keyboard, and evidence matrices match their records; one clean capture session binds the browser artifacts to the governed source inventory. | Missing or altered source bindings, missing executable source manifests, invalid state/profile records, scenario drift, coordinated PNG/manifest/sidecar substitution, unapproved environment-specific raster identity, malformed or replayed runtime artifacts, hash/count mismatch, insufficient focus clearance, and sentinel mutations must exit non-zero with named findings. | Proves the declared examples and captured channels agree for an explicitly approved browser environment. The capture-session schema remains parse-compatible with legacy receipts, while executable finalization and validation require source manifests. Environment-scoped PNG identity does not imply cross-platform pixel equivalence. It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval; synthetic negative fixtures are not authenticated provenance. | ## Validator Severity Contract diff --git a/scripts/governance-policy-contract.mjs b/scripts/governance-policy-contract.mjs new file mode 100644 index 0000000..1744c60 --- /dev/null +++ b/scripts/governance-policy-contract.mjs @@ -0,0 +1,48 @@ +export const sentinelProvenanceClauses = [ + "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", + "The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery.", + "The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256.", + "Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked.", + "Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability.", + "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", + "Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance.", +]; + +export const requiredCodeowners = [ + "* @changeroa", + "/GOVERNANCE.md @changeroa", + "/README.md @changeroa", + "/index.md @changeroa", + "/AGENTS.md @changeroa", + "/DOMAINS.md @changeroa", + "/layout/ @changeroa", + "/motion/ @changeroa", + "/design-engineering/ @changeroa", + "/platform-guides/ @changeroa", + "/consumer-reference/ @changeroa", + "/consumer-reference/baselines/ @changeroa", + "/consumer-reference/policies/ @changeroa", + "/consumer-reference/fixtures/promotion/ @changeroa", + "/consumer-reference/schema/promotion-rfc.schema.json @changeroa", + "/tests/ @changeroa", + "/playwright.config.mjs @changeroa", + "/GUIDE.md @changeroa", + "/guides/ @changeroa", + "/recipes/ @changeroa", + "/quality/ @changeroa", + "/scripts/pattern-data.mjs @changeroa", + "/scripts/generate-patterns.mjs @changeroa", + "/patterns/ @changeroa", + "/CATALOG.md @changeroa", + "/.github/workflows/validate.yml @changeroa", + "/.github/ @changeroa", + "/scripts/ @changeroa", + "/scripts/validate-*.mjs @changeroa", + "/scripts/test-validate-*.mjs @changeroa", +]; + +export const immutableActionPins = [ + "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", + "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", + "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", +]; diff --git a/scripts/governance-test-fixture.mjs b/scripts/governance-test-fixture.mjs index 83b51b7..331e5a2 100644 --- a/scripts/governance-test-fixture.mjs +++ b/scripts/governance-test-fixture.mjs @@ -1,13 +1,7 @@ +import { sentinelProvenanceClauses } from "./governance-policy-contract.mjs"; + export const generatedWarning = ""; -export const sentinelProvenanceClauses = [ - "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", - "The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery.", - "The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256.", - "Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked.", - "Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability.", - "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", - "Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance.", -]; +export { sentinelProvenanceClauses }; export const files = { "AGENTS.md": "# Agent Instructions\n\nSee [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md) and [StyleGallery Domains](DOMAINS.md).\n", @@ -40,6 +34,9 @@ export const files = { "Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract.", "Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision.", "The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", + "Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles.", + "one clean capture session binds the browser artifacts to the governed source inventory", + "It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval", "Linux/amd64 20-run calibration and `baseline_owner_approval` remain pending.", ...sentinelProvenanceClauses, "", @@ -81,6 +78,8 @@ export const files = { "jobs:", " validate:", "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", + " with:", + " persist-credentials: false", "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", "node -c scripts/component-state-workflow-contract.mjs", @@ -98,6 +97,7 @@ export const files = { "node scripts/test-validate-webpage-workflow.mjs --json", "node -c scripts/validate-consumer-reference.mjs", "node -c scripts/test-validate-consumer-reference.mjs", + "node -c scripts/consumer-reference-case-runner.mjs", "node -c scripts/governed-profile-registry.mjs", "node -c scripts/promotion-attestation-contract.mjs", "node -c scripts/validate-promotion-rfc.mjs", @@ -109,6 +109,10 @@ export const files = { "consumer-reference/schema/promotion-rfc.schema.json", "consumer-reference/policies/shared-experimental.json", "node scripts/test-consumer-reference-sentinel.mjs", + "node -c scripts/baseline-schema-parity.mjs", + "node -c scripts/test-component-state-source-contract.mjs", + "node scripts/test-component-state-source-contract.mjs", + "consumer-reference/schema/visual-evidence.schema.json", " component-state-evidence:", " env:", " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", @@ -189,6 +193,9 @@ export const files = { "`scripts/generate-patterns.mjs`", "`scripts/generate-consumer-reference-evidence.mjs`", "Browser state evidence begins with `scripts/create-component-state-session.mjs`", + "Receipt creation rejects dirty relevant sources.", + "browser-authored visual sidecars bind the same session, scenario, capture time, source digest, PNG bytes, dimensions, and hash", + "exactly 30 channel passes over the closed 40-file runtime set", "Validation uses the receipt and completed manifest interval, not a wall-clock maximum age", "design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md", "design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md", diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index a93279d..0aba880 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -4,10 +4,13 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { files, generatedWarning, sentinelProvenanceClauses } from "./governance-test-fixture.mjs"; -const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-governance.mjs"); +const adapterHarness = path.join(root, "scripts", "test-reference-adapters.mjs"); +const adapterPipeParser = 'let output = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { output += chunk; }); process.stdin.on("end", () => { const report = JSON.parse(output); const complete = report.ok === true && report.failures.length === 0 && report.results.length === 42; process.stdout.write(complete ? "42\\n" : "incomplete\\n"); process.exitCode = complete ? 0 : 1; });'; const cases = [ { name: "missing_governance", omit: ["GOVERNANCE.md"], expect: "GOVERNANCE.md: missing file" }, @@ -78,9 +81,16 @@ const cases = [ { name: "missing_domain_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/validate-domains.mjs --json\n", "") }, expect: ".github/workflows/validate.yml: missing node scripts/validate-domains.mjs --json" }, { name: "missing_domain_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail.", "Domain fixtures must fail.") }, expect: "quality/evidence/executable-evidence.md: missing Domain metadata, immutable provenance, scope boundaries, and root-route fixtures must fail." }, { name: "missing_sentinel_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replaceAll("node scripts/test-consumer-reference-sentinel.mjs", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-consumer-reference-sentinel.mjs" }, + { name: "missing_component_source_contract_ci", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/test-component-state-source-contract.mjs", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-component-state-source-contract.mjs" }, + { name: "missing_visual_schema_ci", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("consumer-reference/schema/visual-evidence.schema.json", "") }, expect: ".github/workflows/validate.yml: missing consumer-reference/schema/visual-evidence.schema.json" }, { name: "browser_artifact_harness_in_static_job", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" component-state-evidence:\n", "node scripts/test-validate-component-state-artifacts.mjs\n component-state-evidence:\n") }, + mutate: { + ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace( + " validate:\n", + " validate:\nnode scripts/test-validate-component-state-artifacts.mjs\n", + ), + }, expect: ".github/workflows/validate.yml: browser-dependent artifact/session harness must not run in validate job", }, { @@ -134,11 +144,17 @@ const cases = [ expect: ".github/workflows/validate.yml: missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", }, { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, + { name: "missing_component_state_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles.", "Component evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles." }, { name: "floating_action_ref", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", "uses: actions/checkout@v4") }, expect: ".github/workflows/validate.yml: floating or unlabeled action ref uses: actions/checkout@v4", }, + { + name: "checkout_credentials_persisted", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" persist-credentials: false\n", "") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, { name: "missing_scoped_checkout_sha_trust", mutate: { @@ -221,6 +237,15 @@ function runCase(testCase) { } const results = cases.map(runCase); +const adapterPipeCheck = spawnSync("sh", ["-c", '"$NODE_BIN" "$ADAPTER_HARNESS" --json | "$NODE_BIN" -e "$ADAPTER_PIPE_PARSER"'], { + cwd: root, encoding: "utf8", env: { ...process.env, ADAPTER_HARNESS: adapterHarness, ADAPTER_PIPE_PARSER: adapterPipeParser, NODE_BIN: process.execPath }, +}); +results.push({ + actual: { status: adapterPipeCheck.status, stdout: adapterPipeCheck.stdout.trim() }, + expected: "complete 42-result JSON report through a pipe", + name: "adapter_json_pipe_complete", + ok: adapterPipeCheck.status === 0 && adapterPipeCheck.stdout.trim() === "42", +}); const report = { ok: results.every((result) => result.ok), results, diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 0ef826f..6272102 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -3,60 +3,14 @@ import fs from "node:fs"; import path from "node:path"; import { componentStateWorkflowFailures } from "./component-state-workflow-contract.mjs"; +import { immutableActionPins, requiredCodeowners, sentinelProvenanceClauses } from "./governance-policy-contract.mjs"; import { promotionGovernanceFailures } from "./promotion-governance-contract.mjs"; +import { checkoutCredentialFailures } from "./workflow-action-contract.mjs"; -const args = new Set(process.argv.slice(2)); -const json = args.has("--json"); +const json = new Set(process.argv.slice(2)).has("--json"); const root = process.cwd(); const failures = []; const warnings = []; -const sentinelProvenanceClauses = [ - "Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation.", - "The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery.", - "The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256.", - "Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked.", - "Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability.", - "Baseline-owner approval remains unclaimed until the named owner explicitly approves it.", - "Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance.", -]; -const requiredCodeowners = [ - "* @changeroa", - "/GOVERNANCE.md @changeroa", - "/README.md @changeroa", - "/index.md @changeroa", - "/AGENTS.md @changeroa", - "/DOMAINS.md @changeroa", - "/layout/ @changeroa", - "/motion/ @changeroa", - "/design-engineering/ @changeroa", - "/platform-guides/ @changeroa", - "/consumer-reference/ @changeroa", - "/consumer-reference/baselines/ @changeroa", - "/consumer-reference/policies/ @changeroa", - "/consumer-reference/fixtures/promotion/ @changeroa", - "/consumer-reference/schema/promotion-rfc.schema.json @changeroa", - "/tests/ @changeroa", - "/playwright.config.mjs @changeroa", - "/GUIDE.md @changeroa", - "/guides/ @changeroa", - "/recipes/ @changeroa", - "/quality/ @changeroa", - "/scripts/pattern-data.mjs @changeroa", - "/scripts/generate-patterns.mjs @changeroa", - "/patterns/ @changeroa", - "/CATALOG.md @changeroa", - "/.github/workflows/validate.yml @changeroa", - "/.github/ @changeroa", - "/scripts/ @changeroa", - "/scripts/validate-*.mjs @changeroa", - "/scripts/test-validate-*.mjs @changeroa", -]; -const immutableActionPins = [ - "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", - "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", - "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", -]; - function read(relative) { const target = path.join(root, relative); if (!fs.existsSync(target)) { @@ -118,6 +72,9 @@ function requireGovernanceMatrix() { requireIncludes("GOVERNANCE.md", "Generated structure changes, generated-warning changes, or generated metadata changes."); requireIncludes("GOVERNANCE.md", "`scripts/generate-consumer-reference-evidence.mjs`"); requireIncludes("GOVERNANCE.md", "Browser state evidence begins with `scripts/create-component-state-session.mjs`"); + requireIncludes("GOVERNANCE.md", "Receipt creation rejects dirty relevant sources."); + requireIncludes("GOVERNANCE.md", "browser-authored visual sidecars bind the same session, scenario, capture time, source digest, PNG bytes, dimensions, and hash"); + requireIncludes("GOVERNANCE.md", "exactly 30 channel passes over the closed 40-file runtime set"); requireIncludes("GOVERNANCE.md", "Validation uses the receipt and completed manifest interval, not a wall-clock maximum age"); for (const profile of ["editorial", "terminal"]) { for (const artifact of ["state-matrix.md", "keyboard-matrix.md", "evidence-coverage.md"]) { @@ -161,6 +118,7 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "node scripts/test-validate-webpage-workflow.mjs --json"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/validate-consumer-reference.mjs"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/test-validate-consumer-reference.mjs"); + requireIncludes(".github/workflows/validate.yml", "node -c scripts/consumer-reference-case-runner.mjs"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/governed-profile-registry.mjs"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/promotion-attestation-contract.mjs"); requireIncludes(".github/workflows/validate.yml", "node -c scripts/validate-promotion-rfc.mjs"); @@ -172,6 +130,10 @@ function requireCiwiring() { requireIncludes(".github/workflows/validate.yml", "consumer-reference/schema/promotion-rfc.schema.json"); requireIncludes(".github/workflows/validate.yml", "consumer-reference/policies/shared-experimental.json"); requireIncludes(".github/workflows/validate.yml", "node scripts/test-consumer-reference-sentinel.mjs"); + requireIncludes(".github/workflows/validate.yml", "node -c scripts/baseline-schema-parity.mjs"); + requireIncludes(".github/workflows/validate.yml", "node -c scripts/test-component-state-source-contract.mjs"); + requireIncludes(".github/workflows/validate.yml", "node scripts/test-component-state-source-contract.mjs"); + requireIncludes(".github/workflows/validate.yml", "consumer-reference/schema/visual-evidence.schema.json"); requireIncludes(".github/workflows/validate.yml", "node scripts/create-component-state-session.mjs"); requireIncludes(".github/workflows/validate.yml", "STATE_SESSION_RECEIPT="); requireIncludes(".github/workflows/validate.yml", "node scripts/finalize-component-state-evidence.mjs"); @@ -204,6 +166,7 @@ function requireImmutableActions() { if (!/^\s*uses:\s+actions\/[a-z0-9-]+@[a-f0-9]{40}\s+#\s+v\d+\s*$/.test(line)) failures.push(`${relative}: floating or unlabeled action ref ${line.trim()}`); } for (const pin of immutableActionPins) if (!workflow.includes(pin)) failures.push(`${relative}: missing immutable action pin ${pin}`); + failures.push(...checkoutCredentialFailures(workflow, relative)); } function requireRootLinks() { @@ -227,6 +190,9 @@ function requireEvidenceMap() { requireIncludes("quality/evidence/executable-evidence.md", "Consumer-reference handoffs, schema/runtime parity"); requireIncludes("quality/evidence/executable-evidence.md", "repository handoff omissions must fail"); requireIncludes("quality/evidence/executable-evidence.md", "The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence."); + requireIncludes("quality/evidence/executable-evidence.md", "Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles."); + requireIncludes("quality/evidence/executable-evidence.md", "one clean capture session binds the browser artifacts to the governed source inventory"); + requireIncludes("quality/evidence/executable-evidence.md", "It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval"); requireIncludes("GOVERNANCE.md", "node scripts/test-consumer-reference-sentinel.mjs"); requireIncludes("GOVERNANCE.md", "owner.enforcement: \"placeholder\""); requireIncludes("GOVERNANCE.md", "review_independence: \"single_account\""); diff --git a/scripts/workflow-action-contract.mjs b/scripts/workflow-action-contract.mjs new file mode 100644 index 0000000..6e20804 --- /dev/null +++ b/scripts/workflow-action-contract.mjs @@ -0,0 +1,17 @@ +export function checkoutCredentialFailures(workflow, relative) { + const failures = []; + const lines = workflow.split("\n"); + for (const [index, line] of lines.entries()) { + if (!line.trim().startsWith("uses: actions/checkout@")) continue; + const indentation = line.length - line.trimStart().length; + let credentialsPersist = true; + for (const following of lines.slice(index + 1)) { + if (following.trim() === "") continue; + const followingIndentation = following.length - following.trimStart().length; + if (followingIndentation < indentation || (followingIndentation === indentation && following.trim().startsWith("uses:"))) break; + if (followingIndentation > indentation && following.trim() === "persist-credentials: false") credentialsPersist = false; + } + if (credentialsPersist) failures.push(`${relative}: every actions/checkout step must set persist-credentials: false`); + } + return failures; +} From 15c8fcfb129749a6461208d90d61f4f6a74388e4 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 07:51:03 +0900 Subject: [PATCH 16/44] Recapture approved component evidence --- .../editorial/evidence/button.evidence.json | 2860 ++++++++++++++++- .../terminal/evidence/button.evidence.json | 2860 ++++++++++++++++- 2 files changed, 5410 insertions(+), 310 deletions(-) diff --git a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json index c33a0b2..fa3cbcc 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json @@ -24,22 +24,22 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-visual", - "recorded_at": "2026-07-13T17:57:27.692Z", + "recorded_at": "2026-07-13T22:47:00.154Z", "result": { "observed": "Recorded visual evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-disabled-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -51,11 +51,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -69,9 +239,9 @@ { "artifact": { "path": "runtime/editorial-action-disabled-busy.dom.json", - "byte_length": 1286, + "byte_length": 8290, "media_type": "application/json", - "sha256": "sha256:bd67134d1f292882bf43891e9f81c6e542d0b977e2c445c5713352915324854a" + "sha256": "sha256:961f33de3cc17c66b9c06cefa709eb09589e70331946e4638f6a48effe8d6279" }, "channel": "dom", "environment": { @@ -86,22 +256,22 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-dom", - "recorded_at": "2026-07-13T17:57:27.692Z", + "recorded_at": "2026-07-13T22:47:00.154Z", "result": { "observed": "Recorded dom evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-disabled-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -113,11 +283,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -131,9 +471,9 @@ { "artifact": { "path": "runtime/editorial-action-disabled-busy.ax.json", - "byte_length": 1205, + "byte_length": 8209, "media_type": "application/json", - "sha256": "sha256:b16fb9faa7cd29423426510c450f01e1e1a24830a29936c4cce3de08812450f7" + "sha256": "sha256:393ed828beb537b6ac91c5830b580eeaf65fcbbc749309ef2583c1a4390ce073" }, "channel": "ax", "environment": { @@ -148,22 +488,22 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-ax", - "recorded_at": "2026-07-13T17:57:27.692Z", + "recorded_at": "2026-07-13T22:47:00.154Z", "result": { "observed": "Recorded ax evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-disabled-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -175,11 +515,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -212,22 +722,22 @@ "viewport": "1024x768" }, "id": "action-focused-visual", - "recorded_at": "2026-07-13T17:57:27.806Z", + "recorded_at": "2026-07-13T22:47:00.309Z", "result": { "observed": "Recorded visual evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-focused", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -239,11 +749,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -256,9 +936,9 @@ { "artifact": { "path": "runtime/editorial-action-focused.dom.json", - "byte_length": 1226, + "byte_length": 8230, "media_type": "application/json", - "sha256": "sha256:a3322569c6304e5d73943ec8f2217d7edeff588d517cb00daed018b9d9bd2add" + "sha256": "sha256:2e327c5a2cad6ff339619023179aa76838d12d99d10901eab532881ba6777d95" }, "channel": "dom", "environment": { @@ -273,22 +953,22 @@ "viewport": "1024x768" }, "id": "action-focused-dom", - "recorded_at": "2026-07-13T17:57:27.806Z", + "recorded_at": "2026-07-13T22:47:00.309Z", "result": { "observed": "Recorded dom evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-focused", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -300,11 +980,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -317,9 +1167,9 @@ { "artifact": { "path": "runtime/editorial-action-focused.ax.json", - "byte_length": 1146, + "byte_length": 8150, "media_type": "application/json", - "sha256": "sha256:b38484844b058e1647c105116feb3086739634516f17a5c620c57c36d4742976" + "sha256": "sha256:3c713beb750d7563b2c1f0f09a9f088acde9d3e0042ab954ed553ceaec118853" }, "channel": "ax", "environment": { @@ -334,22 +1184,22 @@ "viewport": "1024x768" }, "id": "action-focused-ax", - "recorded_at": "2026-07-13T17:57:27.806Z", + "recorded_at": "2026-07-13T22:47:00.309Z", "result": { "observed": "Recorded ax evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-focused", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -361,11 +1211,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -397,22 +1417,22 @@ "viewport": "1024x768" }, "id": "action-loading-busy-visual", - "recorded_at": "2026-07-13T17:57:27.915Z", + "recorded_at": "2026-07-13T22:47:00.476Z", "result": { "observed": "Recorded visual evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-loading-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -424,11 +1444,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -443,9 +1633,9 @@ { "artifact": { "path": "runtime/editorial-action-loading-busy.dom.json", - "byte_length": 1272, + "byte_length": 8276, "media_type": "application/json", - "sha256": "sha256:6f4207736d0b0f7f02b2e320be98fc05d3b067eb90b93c9cd45ac52fc4bc75bd" + "sha256": "sha256:857ee7a1ba66b002ffae9ab3fd741240b3429329215bd0046e0b4ab991df6712" }, "channel": "dom", "environment": { @@ -460,22 +1650,22 @@ "viewport": "1024x768" }, "id": "action-loading-busy-dom", - "recorded_at": "2026-07-13T17:57:27.915Z", + "recorded_at": "2026-07-13T22:47:00.476Z", "result": { "observed": "Recorded dom evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-loading-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -487,11 +1677,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -506,9 +1866,9 @@ { "artifact": { "path": "runtime/editorial-action-loading-busy.ax.json", - "byte_length": 1172, + "byte_length": 8176, "media_type": "application/json", - "sha256": "sha256:b72b51a832c0ff1c176acc2fcc3318f4e42fdc4b73b63ece8e54139eeb4488bb" + "sha256": "sha256:d0ce37683f38a6db749fe55b900358d219c187424d0055ac2f81c04c1bff37ad" }, "channel": "ax", "environment": { @@ -523,22 +1883,22 @@ "viewport": "1024x768" }, "id": "action-loading-busy-ax", - "recorded_at": "2026-07-13T17:57:27.915Z", + "recorded_at": "2026-07-13T22:47:00.476Z", "result": { "observed": "Recorded ax evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-loading-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -550,11 +1910,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -588,22 +2118,22 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-visual", - "recorded_at": "2026-07-13T17:57:28.032Z", + "recorded_at": "2026-07-13T22:47:00.604Z", "result": { "observed": "Recorded visual evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "disclosure-expanded-loading", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -615,11 +2145,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -635,9 +2335,9 @@ { "artifact": { "path": "runtime/editorial-disclosure-expanded-loading.dom.json", - "byte_length": 1329, + "byte_length": 8333, "media_type": "application/json", - "sha256": "sha256:c97aa21052514b75474967d0c8ae74f790cbcce9652d0838c6d7e1c5ccb3ad7f" + "sha256": "sha256:4f0e84c6fa3e32e6194270b94216eba75e1cfe01cc5e41ddd2643a36a310d589" }, "channel": "dom", "environment": { @@ -652,22 +2352,22 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-dom", - "recorded_at": "2026-07-13T17:57:28.032Z", + "recorded_at": "2026-07-13T22:47:00.604Z", "result": { "observed": "Recorded dom evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "disclosure-expanded-loading", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -679,11 +2379,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -699,9 +2569,9 @@ { "artifact": { "path": "runtime/editorial-disclosure-expanded-loading.ax.json", - "byte_length": 1210, + "byte_length": 8214, "media_type": "application/json", - "sha256": "sha256:c8298da99d9f31b406a43141cf532175ca44c4b3251c61c0233d255a9f7b60b7" + "sha256": "sha256:2938a8a30def39da0915a4bdee022b4ca0e01258667a025fb310e823b972741f" }, "channel": "ax", "environment": { @@ -716,22 +2586,22 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-ax", - "recorded_at": "2026-07-13T17:57:28.032Z", + "recorded_at": "2026-07-13T22:47:00.604Z", "result": { "observed": "Recorded ax evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "disclosure-expanded-loading", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -743,11 +2613,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -782,22 +2822,22 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-visual", - "recorded_at": "2026-07-13T17:57:28.149Z", + "recorded_at": "2026-07-13T22:47:00.776Z", "result": { "observed": "Recorded visual evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "toggle-focused-pressed", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -809,11 +2849,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -827,9 +3037,9 @@ { "artifact": { "path": "runtime/editorial-toggle-focused-pressed.dom.json", - "byte_length": 1266, + "byte_length": 8270, "media_type": "application/json", - "sha256": "sha256:1fb40c94b570b84402d27429a3b2e7d9aa282039a1ac6d4fb4b73aab88c65bc5" + "sha256": "sha256:d9a086cc6b5384720106cc958869cf479f7ebe9b8ba2f9a0c41faaebb1fc2873" }, "channel": "dom", "environment": { @@ -844,22 +3054,22 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-dom", - "recorded_at": "2026-07-13T17:57:28.149Z", + "recorded_at": "2026-07-13T22:47:00.776Z", "result": { "observed": "Recorded dom evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "toggle-focused-pressed", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -871,11 +3081,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -889,9 +3269,9 @@ { "artifact": { "path": "runtime/editorial-toggle-focused-pressed.ax.json", - "byte_length": 1180, + "byte_length": 8184, "media_type": "application/json", - "sha256": "sha256:d43555fecb3120cadc23a1d2d056cd63f93907b25ebc4c62ccbe00a5e76011fd" + "sha256": "sha256:b5c7f94341b9e348fc5cfbfe06c490f5b26e2aaf7ccc2a9cee984f4c13cfa12b" }, "channel": "ax", "environment": { @@ -906,22 +3286,22 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-ax", - "recorded_at": "2026-07-13T17:57:28.149Z", + "recorded_at": "2026-07-13T22:47:00.776Z", "result": { "observed": "Recorded ax evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "toggle-focused-pressed", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -933,11 +3313,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", diff --git a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json index 763b7f2..f754565 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json @@ -24,22 +24,22 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-visual", - "recorded_at": "2026-07-13T17:57:28.259Z", + "recorded_at": "2026-07-13T22:47:00.941Z", "result": { "observed": "Recorded visual evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-disabled-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -51,11 +51,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -69,9 +239,9 @@ { "artifact": { "path": "runtime/terminal-action-disabled-busy.dom.json", - "byte_length": 1285, + "byte_length": 8289, "media_type": "application/json", - "sha256": "sha256:b81b10289290e069b73a81481b6b2bc0705a625f0dbd6244c5608e684f58819f" + "sha256": "sha256:71d3e5f60eacc2276bc41e95caf5d25d9e96a003ac915c0236930affa7b4da99" }, "channel": "dom", "environment": { @@ -86,22 +256,22 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-dom", - "recorded_at": "2026-07-13T17:57:28.259Z", + "recorded_at": "2026-07-13T22:47:00.941Z", "result": { "observed": "Recorded dom evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-disabled-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -113,11 +283,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -131,9 +471,9 @@ { "artifact": { "path": "runtime/terminal-action-disabled-busy.ax.json", - "byte_length": 1204, + "byte_length": 8208, "media_type": "application/json", - "sha256": "sha256:282e01575951673bb419bf2064984fe7bb8468d93c296f6fc8f55165347594f1" + "sha256": "sha256:e101912252a44f199e1bf8dd7d6a0b296b38cac85bbefffcff0c222bee5f9b90" }, "channel": "ax", "environment": { @@ -148,22 +488,22 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-ax", - "recorded_at": "2026-07-13T17:57:28.259Z", + "recorded_at": "2026-07-13T22:47:00.941Z", "result": { "observed": "Recorded ax evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-disabled-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -175,11 +515,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -212,22 +722,22 @@ "viewport": "1024x768" }, "id": "action-focused-visual", - "recorded_at": "2026-07-13T17:57:28.366Z", + "recorded_at": "2026-07-13T22:47:01.043Z", "result": { "observed": "Recorded visual evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-focused", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -239,11 +749,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -256,9 +936,9 @@ { "artifact": { "path": "runtime/terminal-action-focused.dom.json", - "byte_length": 1225, + "byte_length": 8229, "media_type": "application/json", - "sha256": "sha256:fcbed06dfa9cfbc4e764a9c4bd3bb0dbc90920d72f8cf9698095eb23d1317e38" + "sha256": "sha256:bcd6a06c58eafdf5aec6f61c272c4c26d2580f2f74d0fe25c2d83b07e211d475" }, "channel": "dom", "environment": { @@ -273,22 +953,22 @@ "viewport": "1024x768" }, "id": "action-focused-dom", - "recorded_at": "2026-07-13T17:57:28.366Z", + "recorded_at": "2026-07-13T22:47:01.043Z", "result": { "observed": "Recorded dom evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-focused", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -300,11 +980,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -317,9 +1167,9 @@ { "artifact": { "path": "runtime/terminal-action-focused.ax.json", - "byte_length": 1149, + "byte_length": 8153, "media_type": "application/json", - "sha256": "sha256:ec9240455b30b17b3d09d7a5a9d3d5488ff8ea10caff2a03524e861210609631" + "sha256": "sha256:e12246b87fbe9f8b055a85ea5285771223057d46c469d0a9ac843c8d1e6dc220" }, "channel": "ax", "environment": { @@ -334,22 +1184,22 @@ "viewport": "1024x768" }, "id": "action-focused-ax", - "recorded_at": "2026-07-13T17:57:28.366Z", + "recorded_at": "2026-07-13T22:47:01.043Z", "result": { "observed": "Recorded ax evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-focused", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -361,11 +1211,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -397,22 +1417,22 @@ "viewport": "1024x768" }, "id": "action-loading-busy-visual", - "recorded_at": "2026-07-13T17:57:28.482Z", + "recorded_at": "2026-07-13T22:47:01.179Z", "result": { "observed": "Recorded visual evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-loading-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -424,11 +1444,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -443,9 +1633,9 @@ { "artifact": { "path": "runtime/terminal-action-loading-busy.dom.json", - "byte_length": 1271, + "byte_length": 8275, "media_type": "application/json", - "sha256": "sha256:fd96628699bc5b1035160e66fff9a35430c1aa38657c9d3b410825151566ba98" + "sha256": "sha256:b85bf78aadc96eb104d6b8a51831ea2cc20681bc9bcf47abecfaf583bfe18df8" }, "channel": "dom", "environment": { @@ -460,22 +1650,22 @@ "viewport": "1024x768" }, "id": "action-loading-busy-dom", - "recorded_at": "2026-07-13T17:57:28.482Z", + "recorded_at": "2026-07-13T22:47:01.179Z", "result": { "observed": "Recorded dom evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-loading-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -487,11 +1677,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -506,9 +1866,9 @@ { "artifact": { "path": "runtime/terminal-action-loading-busy.ax.json", - "byte_length": 1176, + "byte_length": 8180, "media_type": "application/json", - "sha256": "sha256:051996c60a57e0934046e63cc32a0bcad8f7bd0e298c571ab57b0cc2ca0e0375" + "sha256": "sha256:d9e4afb0ff0f5f27d2c2f993b3e9cc3894149738c4f4d7efe1d2c048c4d0d698" }, "channel": "ax", "environment": { @@ -523,22 +1883,22 @@ "viewport": "1024x768" }, "id": "action-loading-busy-ax", - "recorded_at": "2026-07-13T17:57:28.482Z", + "recorded_at": "2026-07-13T22:47:01.179Z", "result": { "observed": "Recorded ax evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "action-loading-busy", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -550,11 +1910,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -588,22 +2118,22 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-visual", - "recorded_at": "2026-07-13T17:57:28.602Z", + "recorded_at": "2026-07-13T22:47:01.371Z", "result": { "observed": "Recorded visual evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "disclosure-expanded-loading", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -615,11 +2145,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -635,9 +2335,9 @@ { "artifact": { "path": "runtime/terminal-disclosure-expanded-loading.dom.json", - "byte_length": 1328, + "byte_length": 8332, "media_type": "application/json", - "sha256": "sha256:d6c84c4b2edeb5306cc3606a3f79589e385c8d46c5fad2f54e4fb5eedd258f8f" + "sha256": "sha256:bc2150a332b593b481e28f3ceeaaf69e26bfdb652bfd24675a6b205c097e14bf" }, "channel": "dom", "environment": { @@ -652,22 +2352,22 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-dom", - "recorded_at": "2026-07-13T17:57:28.602Z", + "recorded_at": "2026-07-13T22:47:01.371Z", "result": { "observed": "Recorded dom evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "disclosure-expanded-loading", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -679,11 +2379,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -699,9 +2569,9 @@ { "artifact": { "path": "runtime/terminal-disclosure-expanded-loading.ax.json", - "byte_length": 1211, + "byte_length": 8215, "media_type": "application/json", - "sha256": "sha256:4db17e7bbe59a29cf409a798d28f2f91b39dde203f6704df5e3e3038d9e96fd4" + "sha256": "sha256:c478c6f24274b363f77dbcfc7dad86f54f664c712eac8a1dc612eab60bd18a95" }, "channel": "ax", "environment": { @@ -716,22 +2586,22 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-ax", - "recorded_at": "2026-07-13T17:57:28.602Z", + "recorded_at": "2026-07-13T22:47:01.371Z", "result": { "observed": "Recorded ax evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "disclosure-expanded-loading", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -743,11 +2613,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -782,22 +2822,22 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-visual", - "recorded_at": "2026-07-13T17:57:28.707Z", + "recorded_at": "2026-07-13T22:47:01.504Z", "result": { "observed": "Recorded visual evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "toggle-focused-pressed", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -809,11 +2849,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -827,9 +3037,9 @@ { "artifact": { "path": "runtime/terminal-toggle-focused-pressed.dom.json", - "byte_length": 1265, + "byte_length": 8269, "media_type": "application/json", - "sha256": "sha256:643b8f352ff6895eff1c44f95d6655b363da6ea269b5f0d560150afa931156bd" + "sha256": "sha256:e77eba7af268dd94831680f56132dd9f1378b0acc767e13c752788040038d36a" }, "channel": "dom", "environment": { @@ -844,22 +3054,22 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-dom", - "recorded_at": "2026-07-13T17:57:28.707Z", + "recorded_at": "2026-07-13T22:47:01.504Z", "result": { "observed": "Recorded dom evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "toggle-focused-pressed", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -871,11 +3081,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", @@ -889,9 +3269,9 @@ { "artifact": { "path": "runtime/terminal-toggle-focused-pressed.ax.json", - "byte_length": 1177, + "byte_length": 8181, "media_type": "application/json", - "sha256": "sha256:d5f2271c832538821cfee9683b873243cc5f51aa1f0fa7912d8fc1878a7fbeec" + "sha256": "sha256:18656b4749689d71ece82fa63ef4e576948b8e619a696e437442d3e190efcce8" }, "channel": "ax", "environment": { @@ -906,22 +3286,22 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-ax", - "recorded_at": "2026-07-13T17:57:28.707Z", + "recorded_at": "2026-07-13T22:47:01.504Z", "result": { "observed": "Recorded ax evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "bcf6a47e-1588-496e-b15c-1c5669539eee", + "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", "repository": "changeroa/StyleGallery", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", "source": "local" }, "scenario_id": "toggle-focused-pressed", "session": { "attempt": 1, - "branch": "codex/roadmap-05-state-evidence", + "branch": "codex/roadmap-07-final-hardening", "environment": { "browser": "Google Chrome for Testing 149.0.7827.55", "browser_revision": "chromium-1228", @@ -933,11 +3313,181 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "a19a4fa9ad541d3d7ddc757435b2718c210733c1d070fea65d8db55ec7195346", - "receipt_sha256": "sha256:2db986d79bed5928fdbc531e6884eca8d6ff37299fcdbfb794ab33cf5db85740", - "revision": "bb772d303c9dafea6e99d2c5713b4bd66e41a3bb", - "session_id": "bcf6a47e-1588-496e-b15c-1c5669539eee", - "started_at": "2026-07-13T17:57:26.704Z" + "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", + "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", + "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "source": { + "files": [ + { + "byte_length": 1375, + "path": "consumer-reference/schema/ax-evidence.schema.json", + "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" + }, + { + "byte_length": 7261, + "path": "consumer-reference/schema/capture-session.schema.json", + "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + }, + { + "byte_length": 7101, + "path": "consumer-reference/schema/component-state.schema.json", + "sha256": "sha256:606ef65acadf81c5b2769b1d79f9c1e870880a8fc2b72cbafa4f1c8c7f5e7e67" + }, + { + "byte_length": 1799, + "path": "consumer-reference/schema/dom-evidence.schema.json", + "sha256": "sha256:5b0e67d44f0592b729642e58580ddc7d3386c77a9cc61dd33e9f0538fcb3b850" + }, + { + "byte_length": 3807, + "path": "consumer-reference/schema/evidence-record.schema.json", + "sha256": "sha256:5f32dce2352a304d19eac923f538ad105edf04f7743128cb30eee07c9c071290" + }, + { + "byte_length": 1537, + "path": "consumer-reference/schema/fixture-manifest.schema.json", + "sha256": "sha256:05332488a36a546b2a1119d420e7cd3695e67f0544177734c46ba515784bc6e1" + }, + { + "byte_length": 1669, + "path": "consumer-reference/schema/runtime-evidence-manifest.schema.json", + "sha256": "sha256:7a273cae646efff8652da92d52448d94dd93046feefd4b6db96b12332bb0ac94" + }, + { + "byte_length": 1620, + "path": "consumer-reference/schema/visual-evidence.schema.json", + "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" + }, + { + "byte_length": 59030, + "path": "package-lock.json", + "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + }, + { + "byte_length": 1404, + "path": "package.json", + "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + }, + { + "byte_length": 532, + "path": "playwright.config.mjs", + "sha256": "sha256:7dcc2ae21602ac89af47d2bbf122f5945098f0e8fda9d47db7d33980592e60ae" + }, + { + "byte_length": 3069, + "path": "profiles/editorial/components/button.component.json", + "sha256": "sha256:fb3888e451783aba0c6ac86abcb7d4f361b6c88904f66f412551afea62f33f67" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/fixtures/button.fixture.json", + "sha256": "sha256:9f4f1971d44b5d3d432658d0e15321536ed2741d71e368bb80cfdd4413ed3779" + }, + { + "byte_length": 340, + "path": "profiles/editorial/local-foundations.json", + "sha256": "sha256:467f948969076ccd1226cafa069741d3a693ff66357f610d9c6eda7554bdcf8f" + }, + { + "byte_length": 1434, + "path": "profiles/editorial/profile.json", + "sha256": "sha256:9f22cfc4306ee1002017762915aa6ff8ce1400868a1e1e00e1353015ba8c4997" + }, + { + "byte_length": 5710, + "path": "profiles/editorial/states/button.states.json", + "sha256": "sha256:7217f3af17af24d7fde05e4e6207b7234e80bb431415ff8095450c33e91b8560" + }, + { + "byte_length": 608, + "path": "profiles/editorial/tokens.dtcg.json", + "sha256": "sha256:807c2f1ed2db489b4ffa3ee86b991f5b34decdc279dfd0c75f29b4b65d86c34f" + }, + { + "byte_length": 3068, + "path": "profiles/terminal/components/button.component.json", + "sha256": "sha256:fd9ce393498649125b7e9490f9f00ab95b14ddd59762076af599d22887daa42a" + }, + { + "byte_length": 1441, + "path": "profiles/terminal/fixtures/button.fixture.json", + "sha256": "sha256:c56f157ed0c297da747c1a352ede820cfe493d4617d52977dbd506e9e05b2332" + }, + { + "byte_length": 343, + "path": "profiles/terminal/local-foundations.json", + "sha256": "sha256:a91a72ae29cab8eeef6d3d2fff6b0917246e0a2d478410eca8eeef73db414b8c" + }, + { + "byte_length": 1432, + "path": "profiles/terminal/profile.json", + "sha256": "sha256:26194b0094eac1f721679cbe43794c9372480cb916a12ba6ede0558e1bd2ff55" + }, + { + "byte_length": 5709, + "path": "profiles/terminal/states/button.states.json", + "sha256": "sha256:b56f534a904bff67a41f1cb015ea22b420318a1a7b2421ad6f403b027d582bbf" + }, + { + "byte_length": 606, + "path": "profiles/terminal/tokens.dtcg.json", + "sha256": "sha256:2fe2ac25708c77efcc662b9bbec9147624cd11f3711d70e96ff604e9602d91c2" + }, + { + "byte_length": 741, + "path": "scripts/artifact-metadata.mjs", + "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" + }, + { + "byte_length": 7527, + "path": "scripts/capture-session-contract.mjs", + "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + }, + { + "byte_length": 5099, + "path": "scripts/component-state-contract.mjs", + "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + }, + { + "byte_length": 12400, + "path": "scripts/component-state-semantics.mjs", + "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" + }, + { + "byte_length": 5367, + "path": "scripts/create-component-state-session.mjs", + "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + }, + { + "byte_length": 6909, + "path": "scripts/profile-record-contract.mjs", + "sha256": "sha256:b216e6a939362198bfc0628d0856842891c8e2e5be02b0b298110424de1d8bf7" + }, + { + "byte_length": 1592, + "path": "scripts/strict-json.mjs", + "sha256": "sha256:d203dab02d4fb74866663bdfd7bc58beb120bd5d0e8e145b3bfe62c1bc5091a3" + }, + { + "byte_length": 953, + "path": "scripts/visual-expectation-contract.mjs", + "sha256": "sha256:4279a66c607ff3eb99fa86054fcc27658f93108d80c0cdadf9ceb40bb79cc62c" + }, + { + "byte_length": 9388, + "path": "tests/component-state-evidence.spec.mjs", + "sha256": "sha256:f10bfb2cf7ea37f07ca1af6c160a33b22cb5bcf31eeb60f338f121f1cdc057fa" + }, + { + "byte_length": 7039, + "path": "tests/helpers/render-component-state.mjs", + "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" + } + ], + "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + }, + "started_at": "2026-07-13T22:46:47.631Z" }, "scope": { "component": "button", From ad602d1b58a48857748a115e9d5746cef2990e49 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 08:07:51 +0900 Subject: [PATCH 17/44] Require strict source-bound session schemas --- .../schema/capture-session.schema.json | 6 ++-- scripts/component-state-contract.mjs | 30 +++++++++++++------ .../test-component-state-source-contract.mjs | 13 ++++++++ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/consumer-reference/schema/capture-session.schema.json b/consumer-reference/schema/capture-session.schema.json index ce43789..47327fa 100644 --- a/consumer-reference/schema/capture-session.schema.json +++ b/consumer-reference/schema/capture-session.schema.json @@ -4,7 +4,7 @@ "title": "Component-state capture session", "type": "object", "additionalProperties": false, - "required": ["schema_version", "record_kind", "session_id", "nonce", "started_at", "repository", "revision", "branch", "attempt", "environment", "intended"], + "required": ["schema_version", "record_kind", "session_id", "nonce", "started_at", "repository", "revision", "branch", "attempt", "environment", "source", "intended"], "properties": { "schema_version": { "const": "1.0" }, "record_kind": { "const": "component_state_capture_session" }, @@ -64,7 +64,7 @@ "session_link": { "type": "object", "additionalProperties": false, - "required": ["session_id", "nonce", "receipt_sha256", "started_at", "revision", "branch", "attempt", "environment"], + "required": ["session_id", "nonce", "receipt_sha256", "started_at", "revision", "branch", "attempt", "environment", "source"], "properties": { "session_id": { "type": "string", "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" }, "nonce": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, @@ -91,7 +91,7 @@ "completed_session": { "type": "object", "additionalProperties": false, - "required": ["session_id", "nonce", "receipt_sha256", "receipt", "started_at", "completed_at", "repository", "revision", "branch", "attempt", "environment", "intended"], + "required": ["session_id", "nonce", "receipt_sha256", "receipt", "started_at", "completed_at", "repository", "revision", "branch", "attempt", "environment", "source", "intended"], "properties": { "session_id": { "type": "string", "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" }, "nonce": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, diff --git a/scripts/component-state-contract.mjs b/scripts/component-state-contract.mjs index d7b9c94..9a4c2f5 100644 --- a/scripts/component-state-contract.mjs +++ b/scripts/component-state-contract.mjs @@ -24,22 +24,34 @@ export function readRecord(file, failures) { } } +function readSchema(schemaRoot, name) { + const file = path.join(schemaRoot, name); + try { + return parseStrictJson(fs.readFileSync(file, "utf8")); + } catch (cause) { + const error = new Error(`component schema is not strict JSON: ${cause instanceof Error ? cause.message : String(cause)}`, { cause }); + error.code = "component_schema_json_invalid"; + error.path = file; + throw error; + } +} + export function compileSchemas(schemaRoot) { const ajv = new Ajv2020({ allErrors: true, formats: { "date-time": true }, strict: false }); - const captureSchema = JSON.parse(fs.readFileSync(path.join(schemaRoot, "capture-session.schema.json"), "utf8")); + const captureSchema = readSchema(schemaRoot, "capture-session.schema.json"); ajv.addSchema(captureSchema); - const evidenceSchema = JSON.parse(fs.readFileSync(path.join(schemaRoot, "evidence-record.schema.json"), "utf8")); + const evidenceSchema = readSchema(schemaRoot, "evidence-record.schema.json"); const validateEvidence = ajv.compile(evidenceSchema); return { - ax: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "ax-evidence.schema.json"), "utf8"))), + ax: ajv.compile(readSchema(schemaRoot, "ax-evidence.schema.json")), capture: ajv.getSchema(captureSchema.$id), - component: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "component-state.schema.json"), "utf8"))), - dom: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "dom-evidence.schema.json"), "utf8"))), + component: ajv.compile(readSchema(schemaRoot, "component-state.schema.json")), + dom: ajv.compile(readSchema(schemaRoot, "dom-evidence.schema.json")), evidence: validateEvidence, - fixture: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "fixture-manifest.schema.json"), "utf8"))), - item: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "item.schema.json"), "utf8"))), - runtime: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "runtime-evidence-manifest.schema.json"), "utf8"))), - visual: ajv.compile(JSON.parse(fs.readFileSync(path.join(schemaRoot, "visual-evidence.schema.json"), "utf8"))), + fixture: ajv.compile(readSchema(schemaRoot, "fixture-manifest.schema.json")), + item: ajv.compile(readSchema(schemaRoot, "item.schema.json")), + runtime: ajv.compile(readSchema(schemaRoot, "runtime-evidence-manifest.schema.json")), + visual: ajv.compile(readSchema(schemaRoot, "visual-evidence.schema.json")), }; } diff --git a/scripts/test-component-state-source-contract.mjs b/scripts/test-component-state-source-contract.mjs index 594f208..ceffc6f 100644 --- a/scripts/test-component-state-source-contract.mjs +++ b/scripts/test-component-state-source-contract.mjs @@ -9,9 +9,11 @@ import { canonicalSourceManifest, sourceManifestMatches, } from "./capture-session-contract.mjs"; +import { compileSchemas } from "./component-state-contract.mjs"; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const profileRoot = path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"); +const schemaRoot = path.join(repositoryRoot, "consumer-reference/schema"); const first = canonicalSourceManifest(repositoryRoot, profileRoot); const second = canonicalSourceManifest(repositoryRoot, profileRoot); @@ -50,6 +52,17 @@ try { const config = fs.readFileSync(configFile, "utf8").replace("viewport: { height: 768, width: 1024 },", "deviceScaleFactor: 2,\n viewport: { height: 768, width: 1024 },"); fs.writeFileSync(configFile, config); assert.equal(sourceManifestMatches(copied, copiedRepository, copiedProfiles), false, "deviceScaleFactor drift must invalidate the source manifest before it can double screenshot pixels"); + + const copiedSchemas = path.join(tempRoot, "schemas"); + fs.cpSync(schemaRoot, copiedSchemas, { recursive: true }); + const captureSchema = path.join(copiedSchemas, "capture-session.schema.json"); + const schemaSource = fs.readFileSync(captureSchema, "utf8"); + fs.writeFileSync(captureSchema, schemaSource.replace('"title":', '"title": "shadowed title",\n "title":')); + assert.throws( + () => compileSchemas(copiedSchemas), + (error) => error?.code === "component_schema_json_invalid" && error?.path === captureSchema, + "duplicate component schema properties must fail strict compilation with a named error", + ); } finally { fs.rmSync(tempRoot, { force: true, recursive: true }); } From f131bde908b37b9accf6c1ab9bccc2f987fe2b54 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 08:17:16 +0900 Subject: [PATCH 18/44] Recapture strict source-bound evidence --- .../editorial/evidence/button.evidence.json | 410 +++++++++--------- .../terminal/evidence/button.evidence.json | 410 +++++++++--------- 2 files changed, 410 insertions(+), 410 deletions(-) diff --git a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json index fa3cbcc..ce108b5 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json @@ -24,16 +24,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-visual", - "recorded_at": "2026-07-13T22:47:00.154Z", + "recorded_at": "2026-07-13T23:12:58.644Z", "result": { "observed": "Recorded visual evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -51,10 +51,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -63,9 +63,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -183,9 +183,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -223,9 +223,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -241,7 +241,7 @@ "path": "runtime/editorial-action-disabled-busy.dom.json", "byte_length": 8290, "media_type": "application/json", - "sha256": "sha256:961f33de3cc17c66b9c06cefa709eb09589e70331946e4638f6a48effe8d6279" + "sha256": "sha256:7f810be204ac0e758db459768436102b6f33ce7fc08bde2864672df769193f18" }, "channel": "dom", "environment": { @@ -256,16 +256,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-dom", - "recorded_at": "2026-07-13T22:47:00.154Z", + "recorded_at": "2026-07-13T23:12:58.644Z", "result": { "observed": "Recorded dom evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -283,10 +283,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -295,9 +295,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -415,9 +415,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -455,9 +455,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -473,7 +473,7 @@ "path": "runtime/editorial-action-disabled-busy.ax.json", "byte_length": 8209, "media_type": "application/json", - "sha256": "sha256:393ed828beb537b6ac91c5830b580eeaf65fcbbc749309ef2583c1a4390ce073" + "sha256": "sha256:e1a8b35ccd1aeeaf67176ef1c54b88d8a52accc97179352c78279037b16a3d94" }, "channel": "ax", "environment": { @@ -488,16 +488,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-ax", - "recorded_at": "2026-07-13T22:47:00.154Z", + "recorded_at": "2026-07-13T23:12:58.644Z", "result": { "observed": "Recorded ax evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -515,10 +515,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -527,9 +527,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -647,9 +647,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -687,9 +687,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -722,16 +722,16 @@ "viewport": "1024x768" }, "id": "action-focused-visual", - "recorded_at": "2026-07-13T22:47:00.309Z", + "recorded_at": "2026-07-13T23:12:58.781Z", "result": { "observed": "Recorded visual evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-focused", @@ -749,10 +749,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -761,9 +761,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -881,9 +881,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -921,9 +921,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -938,7 +938,7 @@ "path": "runtime/editorial-action-focused.dom.json", "byte_length": 8230, "media_type": "application/json", - "sha256": "sha256:2e327c5a2cad6ff339619023179aa76838d12d99d10901eab532881ba6777d95" + "sha256": "sha256:d3e8aebe36e4f88f6936d0d5ea6852d9a92b1121a84c07d40f7b636b25975277" }, "channel": "dom", "environment": { @@ -953,16 +953,16 @@ "viewport": "1024x768" }, "id": "action-focused-dom", - "recorded_at": "2026-07-13T22:47:00.309Z", + "recorded_at": "2026-07-13T23:12:58.781Z", "result": { "observed": "Recorded dom evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-focused", @@ -980,10 +980,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -992,9 +992,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1112,9 +1112,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1152,9 +1152,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1169,7 +1169,7 @@ "path": "runtime/editorial-action-focused.ax.json", "byte_length": 8150, "media_type": "application/json", - "sha256": "sha256:3c713beb750d7563b2c1f0f09a9f088acde9d3e0042ab954ed553ceaec118853" + "sha256": "sha256:d49540933edd174dfa2f44b69cf3efdaf85f13daa1de0cfada8f3ba98c5ecc54" }, "channel": "ax", "environment": { @@ -1184,16 +1184,16 @@ "viewport": "1024x768" }, "id": "action-focused-ax", - "recorded_at": "2026-07-13T22:47:00.309Z", + "recorded_at": "2026-07-13T23:12:58.781Z", "result": { "observed": "Recorded ax evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-focused", @@ -1211,10 +1211,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1223,9 +1223,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1343,9 +1343,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1383,9 +1383,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1417,16 +1417,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-visual", - "recorded_at": "2026-07-13T22:47:00.476Z", + "recorded_at": "2026-07-13T23:12:58.929Z", "result": { "observed": "Recorded visual evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1444,10 +1444,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1456,9 +1456,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1576,9 +1576,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1616,9 +1616,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1635,7 +1635,7 @@ "path": "runtime/editorial-action-loading-busy.dom.json", "byte_length": 8276, "media_type": "application/json", - "sha256": "sha256:857ee7a1ba66b002ffae9ab3fd741240b3429329215bd0046e0b4ab991df6712" + "sha256": "sha256:e9b7ffadb2a74f51f3db232b685494fb8fc3a09c2801fc2394a243ab79a2fcf1" }, "channel": "dom", "environment": { @@ -1650,16 +1650,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-dom", - "recorded_at": "2026-07-13T22:47:00.476Z", + "recorded_at": "2026-07-13T23:12:58.929Z", "result": { "observed": "Recorded dom evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1677,10 +1677,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1689,9 +1689,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1809,9 +1809,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1849,9 +1849,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1868,7 +1868,7 @@ "path": "runtime/editorial-action-loading-busy.ax.json", "byte_length": 8176, "media_type": "application/json", - "sha256": "sha256:d0ce37683f38a6db749fe55b900358d219c187424d0055ac2f81c04c1bff37ad" + "sha256": "sha256:0081593ccbaa6c2009fa28f2791dae7c3d70691f0aeabcc3e17b7a037d2fdd4a" }, "channel": "ax", "environment": { @@ -1883,16 +1883,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-ax", - "recorded_at": "2026-07-13T22:47:00.476Z", + "recorded_at": "2026-07-13T23:12:58.929Z", "result": { "observed": "Recorded ax evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1910,10 +1910,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1922,9 +1922,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2042,9 +2042,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2082,9 +2082,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2118,16 +2118,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-visual", - "recorded_at": "2026-07-13T22:47:00.604Z", + "recorded_at": "2026-07-13T23:12:59.127Z", "result": { "observed": "Recorded visual evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2145,10 +2145,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2157,9 +2157,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2277,9 +2277,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2317,9 +2317,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2337,7 +2337,7 @@ "path": "runtime/editorial-disclosure-expanded-loading.dom.json", "byte_length": 8333, "media_type": "application/json", - "sha256": "sha256:4f0e84c6fa3e32e6194270b94216eba75e1cfe01cc5e41ddd2643a36a310d589" + "sha256": "sha256:e8ca7506b86a5ef0df66c2d8161961ec4c97d3ebf4ded3d0592455b778b72a0e" }, "channel": "dom", "environment": { @@ -2352,16 +2352,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-dom", - "recorded_at": "2026-07-13T22:47:00.604Z", + "recorded_at": "2026-07-13T23:12:59.127Z", "result": { "observed": "Recorded dom evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2379,10 +2379,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2391,9 +2391,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2511,9 +2511,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2551,9 +2551,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2571,7 +2571,7 @@ "path": "runtime/editorial-disclosure-expanded-loading.ax.json", "byte_length": 8214, "media_type": "application/json", - "sha256": "sha256:2938a8a30def39da0915a4bdee022b4ca0e01258667a025fb310e823b972741f" + "sha256": "sha256:530a9451ea6111e4b4abca606e485ed1077dd586af828f77d51c340fde498d3c" }, "channel": "ax", "environment": { @@ -2586,16 +2586,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-ax", - "recorded_at": "2026-07-13T22:47:00.604Z", + "recorded_at": "2026-07-13T23:12:59.127Z", "result": { "observed": "Recorded ax evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2613,10 +2613,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2625,9 +2625,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2745,9 +2745,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2785,9 +2785,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2822,16 +2822,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-visual", - "recorded_at": "2026-07-13T22:47:00.776Z", + "recorded_at": "2026-07-13T23:12:59.237Z", "result": { "observed": "Recorded visual evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -2849,10 +2849,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2861,9 +2861,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2981,9 +2981,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -3021,9 +3021,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -3039,7 +3039,7 @@ "path": "runtime/editorial-toggle-focused-pressed.dom.json", "byte_length": 8270, "media_type": "application/json", - "sha256": "sha256:d9a086cc6b5384720106cc958869cf479f7ebe9b8ba2f9a0c41faaebb1fc2873" + "sha256": "sha256:4e35af3021a5f705bff515515c91b665ab0bce37ef65fc06c60e465a0b91fc56" }, "channel": "dom", "environment": { @@ -3054,16 +3054,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-dom", - "recorded_at": "2026-07-13T22:47:00.776Z", + "recorded_at": "2026-07-13T23:12:59.237Z", "result": { "observed": "Recorded dom evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3081,10 +3081,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -3093,9 +3093,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -3213,9 +3213,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -3253,9 +3253,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -3271,7 +3271,7 @@ "path": "runtime/editorial-toggle-focused-pressed.ax.json", "byte_length": 8184, "media_type": "application/json", - "sha256": "sha256:b5c7f94341b9e348fc5cfbfe06c490f5b26e2aaf7ccc2a9cee984f4c13cfa12b" + "sha256": "sha256:fd45a7d6519c314e6e70481f26607237323551f250960c6be9e40b77be0466e0" }, "channel": "ax", "environment": { @@ -3286,16 +3286,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-ax", - "recorded_at": "2026-07-13T22:47:00.776Z", + "recorded_at": "2026-07-13T23:12:59.237Z", "result": { "observed": "Recorded ax evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3313,10 +3313,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -3325,9 +3325,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -3445,9 +3445,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -3485,9 +3485,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", diff --git a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json index f754565..ede8178 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json @@ -24,16 +24,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-visual", - "recorded_at": "2026-07-13T22:47:00.941Z", + "recorded_at": "2026-07-13T23:12:59.346Z", "result": { "observed": "Recorded visual evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -51,10 +51,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -63,9 +63,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -183,9 +183,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -223,9 +223,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -241,7 +241,7 @@ "path": "runtime/terminal-action-disabled-busy.dom.json", "byte_length": 8289, "media_type": "application/json", - "sha256": "sha256:71d3e5f60eacc2276bc41e95caf5d25d9e96a003ac915c0236930affa7b4da99" + "sha256": "sha256:43526b9a748c8c1d0f62aa6941fd4e570d75fe0392d7f8371cc35b69be8937b0" }, "channel": "dom", "environment": { @@ -256,16 +256,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-dom", - "recorded_at": "2026-07-13T22:47:00.941Z", + "recorded_at": "2026-07-13T23:12:59.346Z", "result": { "observed": "Recorded dom evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -283,10 +283,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -295,9 +295,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -415,9 +415,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -455,9 +455,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -473,7 +473,7 @@ "path": "runtime/terminal-action-disabled-busy.ax.json", "byte_length": 8208, "media_type": "application/json", - "sha256": "sha256:e101912252a44f199e1bf8dd7d6a0b296b38cac85bbefffcff0c222bee5f9b90" + "sha256": "sha256:f83050d61458d6731184e9ccd3463b8cc421fb889566c2f5c506026bbb918f8b" }, "channel": "ax", "environment": { @@ -488,16 +488,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-ax", - "recorded_at": "2026-07-13T22:47:00.941Z", + "recorded_at": "2026-07-13T23:12:59.346Z", "result": { "observed": "Recorded ax evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -515,10 +515,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -527,9 +527,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -647,9 +647,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -687,9 +687,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -722,16 +722,16 @@ "viewport": "1024x768" }, "id": "action-focused-visual", - "recorded_at": "2026-07-13T22:47:01.043Z", + "recorded_at": "2026-07-13T23:12:59.442Z", "result": { "observed": "Recorded visual evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-focused", @@ -749,10 +749,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -761,9 +761,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -881,9 +881,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -921,9 +921,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -938,7 +938,7 @@ "path": "runtime/terminal-action-focused.dom.json", "byte_length": 8229, "media_type": "application/json", - "sha256": "sha256:bcd6a06c58eafdf5aec6f61c272c4c26d2580f2f74d0fe25c2d83b07e211d475" + "sha256": "sha256:f120a28e85dab701b86245560e42697f3e46a34e7f947be171118a14516f28f0" }, "channel": "dom", "environment": { @@ -953,16 +953,16 @@ "viewport": "1024x768" }, "id": "action-focused-dom", - "recorded_at": "2026-07-13T22:47:01.043Z", + "recorded_at": "2026-07-13T23:12:59.442Z", "result": { "observed": "Recorded dom evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-focused", @@ -980,10 +980,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -992,9 +992,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1112,9 +1112,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1152,9 +1152,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1169,7 +1169,7 @@ "path": "runtime/terminal-action-focused.ax.json", "byte_length": 8153, "media_type": "application/json", - "sha256": "sha256:e12246b87fbe9f8b055a85ea5285771223057d46c469d0a9ac843c8d1e6dc220" + "sha256": "sha256:b6cf8fec865510678be60f26c600641d43d63efb1e2a0ed1cb240b3c65936858" }, "channel": "ax", "environment": { @@ -1184,16 +1184,16 @@ "viewport": "1024x768" }, "id": "action-focused-ax", - "recorded_at": "2026-07-13T22:47:01.043Z", + "recorded_at": "2026-07-13T23:12:59.442Z", "result": { "observed": "Recorded ax evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-focused", @@ -1211,10 +1211,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1223,9 +1223,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1343,9 +1343,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1383,9 +1383,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1417,16 +1417,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-visual", - "recorded_at": "2026-07-13T22:47:01.179Z", + "recorded_at": "2026-07-13T23:12:59.548Z", "result": { "observed": "Recorded visual evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1444,10 +1444,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1456,9 +1456,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1576,9 +1576,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1616,9 +1616,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1635,7 +1635,7 @@ "path": "runtime/terminal-action-loading-busy.dom.json", "byte_length": 8275, "media_type": "application/json", - "sha256": "sha256:b85bf78aadc96eb104d6b8a51831ea2cc20681bc9bcf47abecfaf583bfe18df8" + "sha256": "sha256:71d97da3c895cb1b4ec9cda5339b9583fc08874df4ee5136da8696cef97f7d54" }, "channel": "dom", "environment": { @@ -1650,16 +1650,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-dom", - "recorded_at": "2026-07-13T22:47:01.179Z", + "recorded_at": "2026-07-13T23:12:59.548Z", "result": { "observed": "Recorded dom evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1677,10 +1677,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1689,9 +1689,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -1809,9 +1809,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -1849,9 +1849,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -1868,7 +1868,7 @@ "path": "runtime/terminal-action-loading-busy.ax.json", "byte_length": 8180, "media_type": "application/json", - "sha256": "sha256:d9e4afb0ff0f5f27d2c2f993b3e9cc3894149738c4f4d7efe1d2c048c4d0d698" + "sha256": "sha256:9a827efa4fdd43287af5cd28ea80386b7c6a22b98922474d194a6d780faf6302" }, "channel": "ax", "environment": { @@ -1883,16 +1883,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-ax", - "recorded_at": "2026-07-13T22:47:01.179Z", + "recorded_at": "2026-07-13T23:12:59.548Z", "result": { "observed": "Recorded ax evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1910,10 +1910,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -1922,9 +1922,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2042,9 +2042,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2082,9 +2082,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2118,16 +2118,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-visual", - "recorded_at": "2026-07-13T22:47:01.371Z", + "recorded_at": "2026-07-13T23:12:59.723Z", "result": { "observed": "Recorded visual evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2145,10 +2145,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2157,9 +2157,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2277,9 +2277,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2317,9 +2317,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2337,7 +2337,7 @@ "path": "runtime/terminal-disclosure-expanded-loading.dom.json", "byte_length": 8332, "media_type": "application/json", - "sha256": "sha256:bc2150a332b593b481e28f3ceeaaf69e26bfdb652bfd24675a6b205c097e14bf" + "sha256": "sha256:4b84d8de3b79ab76b7cef9929f8fceda0423784227047ac9a61c0691f87dbafc" }, "channel": "dom", "environment": { @@ -2352,16 +2352,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-dom", - "recorded_at": "2026-07-13T22:47:01.371Z", + "recorded_at": "2026-07-13T23:12:59.723Z", "result": { "observed": "Recorded dom evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2379,10 +2379,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2391,9 +2391,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2511,9 +2511,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2551,9 +2551,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2571,7 +2571,7 @@ "path": "runtime/terminal-disclosure-expanded-loading.ax.json", "byte_length": 8215, "media_type": "application/json", - "sha256": "sha256:c478c6f24274b363f77dbcfc7dad86f54f664c712eac8a1dc612eab60bd18a95" + "sha256": "sha256:135cdd6dd399394c0c9f7fa423744397fff38a9b6dbfb8507b71a6ce348cf240" }, "channel": "ax", "environment": { @@ -2586,16 +2586,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-ax", - "recorded_at": "2026-07-13T22:47:01.371Z", + "recorded_at": "2026-07-13T23:12:59.723Z", "result": { "observed": "Recorded ax evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2613,10 +2613,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2625,9 +2625,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2745,9 +2745,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -2785,9 +2785,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -2822,16 +2822,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-visual", - "recorded_at": "2026-07-13T22:47:01.504Z", + "recorded_at": "2026-07-13T23:12:59.872Z", "result": { "observed": "Recorded visual evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -2849,10 +2849,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -2861,9 +2861,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -2981,9 +2981,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -3021,9 +3021,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -3039,7 +3039,7 @@ "path": "runtime/terminal-toggle-focused-pressed.dom.json", "byte_length": 8269, "media_type": "application/json", - "sha256": "sha256:e77eba7af268dd94831680f56132dd9f1378b0acc767e13c752788040038d36a" + "sha256": "sha256:f957638c87f9093a99b49865af6e85ab8efef6a56376dbc6061187ffac217dcd" }, "channel": "dom", "environment": { @@ -3054,16 +3054,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-dom", - "recorded_at": "2026-07-13T22:47:01.504Z", + "recorded_at": "2026-07-13T23:12:59.872Z", "result": { "observed": "Recorded dom evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3081,10 +3081,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -3093,9 +3093,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -3213,9 +3213,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -3253,9 +3253,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", @@ -3271,7 +3271,7 @@ "path": "runtime/terminal-toggle-focused-pressed.ax.json", "byte_length": 8181, "media_type": "application/json", - "sha256": "sha256:18656b4749689d71ece82fa63ef4e576948b8e619a696e437442d3e190efcce8" + "sha256": "sha256:0328282033d16b9b8dfa29e31c2702feb5aabb405b6ae9fb806401fa68032ee1" }, "channel": "ax", "environment": { @@ -3286,16 +3286,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-ax", - "recorded_at": "2026-07-13T22:47:01.504Z", + "recorded_at": "2026-07-13T23:12:59.872Z", "result": { "observed": "Recorded ax evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "repository": "changeroa/StyleGallery", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3313,10 +3313,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "5bb6b9aee04d67b84c4c9096154775e164d7957d9a37b9a8be7623a9fae03a61", - "receipt_sha256": "sha256:e6d9de4faeed514953acf64dfb43e9ebd783701d365ea33f24d04bf546b7e55d", - "revision": "0e4603bddb187c9d61ad29177cf239f6aceead66", - "session_id": "6be007f2-cbd4-4028-b9b0-01b4f1565235", + "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", + "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", + "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", "source": { "files": [ { @@ -3325,9 +3325,9 @@ "sha256": "sha256:15e9e7f777c2929ac5a5474a5d94baef2dc6c1adb94bee5610fed488f00ea20f" }, { - "byte_length": 7261, + "byte_length": 7291, "path": "consumer-reference/schema/capture-session.schema.json", - "sha256": "sha256:898a818d0b820c7baf4b298d70ebd1cc5345d89acb58756e78c9f1d7b34c4c6f" + "sha256": "sha256:cc3010fa76b06b3109417184cb30a98492ca2ad6aa56f4ba27505c1b228821f8" }, { "byte_length": 7101, @@ -3445,9 +3445,9 @@ "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" }, { - "byte_length": 5099, + "byte_length": 5181, "path": "scripts/component-state-contract.mjs", - "sha256": "sha256:a7d395b008b86ef2520702e97d488bea7065820ce15e18d4b1eb937ba2d78668" + "sha256": "sha256:4de341b2b4557ab9a91fd35c7604ef915071a41394ccc86a7d0395a3f84f2381" }, { "byte_length": 12400, @@ -3485,9 +3485,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:f756fd65aff144adab940ad27d0911a399fa94473ac7139534b8a209a85b0daf" + "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" }, - "started_at": "2026-07-13T22:46:47.631Z" + "started_at": "2026-07-13T23:12:45.252Z" }, "scope": { "component": "button", From 4029a1b556b11808583afb03e369b7d18e4dc98a Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 08:41:13 +0900 Subject: [PATCH 19/44] Enforce strict component evidence validation --- quality/evidence/executable-evidence.md | 2 +- scripts/component-state-artifact-fixture.mjs | 44 +++++++++++++++ scripts/evidence-artifact-contract.mjs | 15 +++-- scripts/finalize-component-state-evidence.mjs | 3 +- ...est-validate-component-state-artifacts.mjs | 56 ++++++------------- scripts/test-validate-component-state.mjs | 3 + scripts/validate-component-state.mjs | 43 +++++++++++++- 7 files changed, 120 insertions(+), 46 deletions(-) create mode 100644 scripts/component-state-artifact-fixture.mjs diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index ece7b33..2fd29eb 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -33,7 +33,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract. | `scripts/validate-promotion-rfc.mjs` and `scripts/test-validate-promotion-rfc.mjs` | `node scripts/validate-promotion-rfc.mjs --json`; `node scripts/test-validate-promotion-rfc.mjs --json` | The manifest-declared policy and two canonical examples pass with deferred decisions, zero attestations, exact support commitment, and no promotion claim. | Manifest-declared invalid fixtures plus forged, unregistered, duplicate, path, extension, support, evidence, canonical-drift, and lifecycle mutations exit non-zero with named findings. | Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision; the current canonical inventory supplies no real independent consumers. | | Portable token source and generated CSS agree through the pinned restricted adapter. | `scripts/build-reference-artifacts.mjs`, `scripts/validate-reference-artifacts.mjs`, and `scripts/test-reference-adapters.mjs` | Build with `--fail-on-warning`, validate the generated manifest, then run the adapter fixture harness. | Dimension, color, duration, tested border values, and whole-token curly aliases emit nonzero CSS declarations with matching counts and hashes; output references remain CSS variable references. | `$extends`, JSON Pointer, resolver/modifier/theme documents, unknown reserved fields, non-string descriptions, adapter-unsafe path segments, untested type/unit, partial/dangling/cyclic/type-mismatched aliases, missing output, zero count, warning, scaffold/unknown manifest fields, missing/duplicate declaration, object sentinel, unresolved or forged value, and count drift must fail with named findings and non-zero child exit. | Proves the pinned adapter preserves this tested subset and that committed artifacts independently rebuild from their canonical source, not that tokens are product defaults, portable beyond the tested adapters, visually suitable, or adopted by a consumer. | | The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence. | `tests/consumer-reference-sentinels.spec.mjs`, `scripts/test-consumer-reference-sentinel.mjs`, `scripts/validate-baseline-manifest.mjs`, and strict raw calibration harnesses. | Run the ordinary sentinel and semantic negatives in the digest-pinned nonblocking Chromium job; validate manifest/raw evidence in the contract job. | Computed visibility/cardinality/grid/gap/geometry/overflow checks precede a locator screenshot; raw DOM and AX bytes are recorded and hash-checked across runs; completion and zero diff derive only from a zero child exit, the exact passing Playwright identity, verified raw hashes, and post-assertion comparison proof. | Hidden layout, long-content overflow, fake/malformed Playwright, missing or nonzero exit, absent comparison/artifact, tampered hash, duplicate/unknown metadata, malformed recursive records, and incomplete run sets must fail without writing a completed aggregate. | Local DOM/AX/rendered evidence does not prove semantic DOM/AX conformance, product suitability, accessibility, or owner approval. Completed-CI repository, workflow, run ID and attempt, SHA, and artifact-name fields are workflow-recorded, self-asserted metadata, not an external attestation. The self-asserted repository field names the canonical upstream changeroa/StyleGallery; the self-asserted execution_repository field names the actual GitHub Actions repository and is limited to changeroa/StyleGallery or ark-jo/StyleGallery. The committed calibration's external_verification object records an independently checked GitHub Actions run and artifact API identity; artifact.api_digest is distinct from committed_ci.raw_evidence_sha256. Future CI aggregates remain awaiting_external_verification until their uploaded artifact API identity is independently checked. Linux/amd64 repeatability is externally verified only for committed run 29260372260; it does not establish baseline-owner approval or product suitability. Baseline-owner approval remains unclaimed until the named owner explicitly approves it. Synthetic fixtures validate rejection and acceptance behavior only; they are not authenticated provenance. Both Chromium jobs are nonblocking and CI never updates snapshots. | -| Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles. | `scripts/create-component-state-session.mjs`, `scripts/finalize-component-state-evidence.mjs`, `scripts/validate-component-state.mjs`, and the component-state source, contract, artifact, generator, and sentinel harnesses. | `npm run test:component-state`; `npm run validate:component-state`; run the digest-pinned component-state browser capture and `npm run test:component-state:runtime-negative`. | Editorial and terminal each declare the five compound scenarios `action-disabled-busy`, `action-focused`, `action-loading-busy`, `disclosure-expanded-loading`, and `toggle-focused-pressed`; their environment-scoped approved PNG hashes and dimensions are canonical pre-capture inputs; the generated state, keyboard, and evidence matrices match their records; one clean capture session binds the browser artifacts to the governed source inventory. | Missing or altered source bindings, missing executable source manifests, invalid state/profile records, scenario drift, coordinated PNG/manifest/sidecar substitution, unapproved environment-specific raster identity, malformed or replayed runtime artifacts, hash/count mismatch, insufficient focus clearance, and sentinel mutations must exit non-zero with named findings. | Proves the declared examples and captured channels agree for an explicitly approved browser environment. The capture-session schema remains parse-compatible with legacy receipts, while executable finalization and validation require source manifests. Environment-scoped PNG identity does not imply cross-platform pixel equivalence. It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval; synthetic negative fixtures are not authenticated provenance. | +| Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles. | `scripts/create-component-state-session.mjs`, `scripts/finalize-component-state-evidence.mjs`, `scripts/validate-component-state.mjs`, and the component-state source, contract, artifact, generator, and sentinel harnesses. | `npm run test:component-state`; `npm run validate:component-state`; run the digest-pinned component-state browser capture and `npm run test:component-state:runtime-negative`. | Editorial and terminal each declare the five compound scenarios `action-disabled-busy`, `action-focused`, `action-loading-busy`, `disclosure-expanded-loading`, and `toggle-focused-pressed`; their environment-scoped approved PNG hashes and dimensions are canonical pre-capture inputs; the generated state, keyboard, and evidence matrices match their records; one clean capture session binds the browser artifacts to the governed source inventory. | Missing or altered source bindings, missing executable source manifests, invalid state/profile records, scenario drift, coordinated PNG/manifest/sidecar substitution, unapproved environment-specific raster identity, malformed or replayed runtime artifacts, hash/count mismatch, insufficient focus clearance, and sentinel mutations must exit non-zero with named findings. | Proves the declared examples and captured channels agree for an explicitly approved browser environment. Legacy source-less receipts are intentionally rejected by the capture receipt, `session_link`, and `completed_session` schemas and by executable finalization and validation. Environment-scoped PNG identity does not imply cross-platform pixel equivalence. It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval; synthetic negative fixtures are not authenticated provenance. | ## Validator Severity Contract diff --git a/scripts/component-state-artifact-fixture.mjs b/scripts/component-state-artifact-fixture.mjs new file mode 100644 index 0000000..5b77bf7 --- /dev/null +++ b/scripts/component-state-artifact-fixture.mjs @@ -0,0 +1,44 @@ +import zlib from "node:zlib"; + +function crc32(bytes) { + let crc = 0xffffffff; + for (const byte of bytes) { + crc ^= byte; + for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); + } + return (crc ^ 0xffffffff) >>> 0; +} + +function pngChunk(type, data) { + const name = Buffer.from(type, "ascii"); + const output = Buffer.alloc(12 + data.length); + output.writeUInt32BE(data.length, 0); + name.copy(output, 4); + data.copy(output, 8); + output.writeUInt32BE(crc32(Buffer.concat([name, data])), 8 + data.length); + return output; +} + +export function makePng(red) { + const width = 64; + const height = 64; + const header = Buffer.alloc(13); + header.writeUInt32BE(width, 0); + header.writeUInt32BE(height, 4); + header[8] = 8; + header[9] = 6; + const row = Buffer.alloc(1 + (width * 4)); + for (let pixel = 0; pixel < width; pixel += 1) row.set([red, 64, 128, 255], 1 + (pixel * 4)); + return Buffer.concat([ + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), + pngChunk("IHDR", header), + pngChunk("IDAT", zlib.deflateSync(Buffer.concat(Array.from({ length: height }, () => row)))), + pngChunk("IEND", Buffer.alloc(0)), + ]); +} + +const canonicalPng = makePng(32); + +export function syntheticImage(profile, scenario) { + return Buffer.concat([canonicalPng, Buffer.from(`${profile}:${scenario}`)]); +} diff --git a/scripts/evidence-artifact-contract.mjs b/scripts/evidence-artifact-contract.mjs index 3fe2f66..fc84e45 100644 --- a/scripts/evidence-artifact-contract.mjs +++ b/scripts/evidence-artifact-contract.mjs @@ -3,6 +3,7 @@ import path from "node:path"; import { artifactMetadata } from "./artifact-metadata.mjs"; import { sameJson, withinSession } from "./capture-session-contract.mjs"; import { parseStrictJson } from "./strict-json.mjs"; +import { visualExpectationFor } from "./visual-expectation-contract.mjs"; function finding(code, file, message) { return { code, message, path: file }; @@ -65,13 +66,19 @@ function validateAx(document, pass, scenario, fixture, profileId, evidenceFile, } } -function validateVisual(document, pass, scenario, profileId, actual, evidenceFile, failures) { +function validateVisual(document, pass, scenario, profileId, actual, visualEnvironments, evidenceFile, failures) { if (document.schema_version !== "1.0" || document.channel !== "visual" || document.profile_id !== profileId || document.scenario_id !== pass.scenario_id || document.semantic_mode !== scenario.semantic_mode) { failures.push(finding("evidence_visual_identity_mismatch", evidenceFile, `${pass.id} visual identity does not match its pass`)); } if (document.source_sha256 !== pass.session?.source?.sha256) failures.push(finding("capture_source_mismatch", evidenceFile, `${pass.id} visual source digest differs from its capture session`)); const expectedImage = { path: path.posix.basename(pass.artifact.path), ...actual }; if (!sameJson(document.image, expectedImage)) failures.push(finding("evidence_visual_image_mismatch", evidenceFile, `${pass.id} visual metadata does not match its PNG`)); + try { + const expectation = visualExpectationFor(scenario, pass.environment, visualEnvironments); + if (actual.sha256 !== expectation.sha256 || actual.width !== expectation.width || actual.height !== expectation.height) failures.push(finding("evidence_visual_expectation_mismatch", evidenceFile, `${pass.id} PNG differs from its pre-capture approved visual expectation`)); + } catch (error) { + failures.push(finding("evidence_visual_expectation_invalid", evidenceFile, error instanceof Error ? error.message : String(error))); + } } function parseArtifactJson(bytes, pass, scenario, fixture, profileId, evidenceFile, failures, schemas, session) { @@ -127,7 +134,7 @@ function readArtifact(pass, artifactRoot, evidenceFile, failures) { return fs.readFileSync(realTarget); } -function parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, evidenceFile, failures, schemas, session) { +function parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, visualEnvironments, evidenceFile, failures, schemas, session) { const reference = pass.artifact.path.replace(/\.png$/, ".visual.json"); const bytes = readArtifact({ artifact: { path: reference }, id: `${pass.id}-metadata` }, artifactRoot, evidenceFile, failures); if (!bytes) return undefined; @@ -145,7 +152,7 @@ function parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, ev if (!sameJson(document.capture_session, pass.session)) failures.push(finding("capture_session_mismatch", evidenceFile, `${pass.id} visual metadata session differs from its pass`)); if (document.captured_at !== pass.recorded_at) failures.push(finding("evidence_recorded_at_mismatch", evidenceFile, `${pass.id} recorded_at is not derived from visual metadata`)); if (session && !withinSession(document.captured_at, session.started_at, session.completed_at)) failures.push(finding("capture_session_time_outside", evidenceFile, `${pass.id} visual metadata was not captured within its session`)); - validateVisual(document, pass, scenario, profileId, actual, evidenceFile, failures); + validateVisual(document, pass, scenario, profileId, actual, visualEnvironments, evidenceFile, failures); return document; } @@ -168,7 +175,7 @@ export function validateEvidenceArtifacts({ artifactRoot, evidence, evidenceFile capturedTimes.get(pass.scenario_id).add(document.captured_at); } } else if (pass.channel === "visual") { - const document = parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, evidenceFile, failures, schemas, session); + const document = parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, states.visual_environments, evidenceFile, failures, schemas, session); if (document?.captured_at) { if (!capturedTimes.has(pass.scenario_id)) capturedTimes.set(pass.scenario_id, new Set()); capturedTimes.get(pass.scenario_id).add(document.captured_at); diff --git a/scripts/finalize-component-state-evidence.mjs b/scripts/finalize-component-state-evidence.mjs index 07051ca..9e05651 100644 --- a/scripts/finalize-component-state-evidence.mjs +++ b/scripts/finalize-component-state-evidence.mjs @@ -51,7 +51,8 @@ const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/sch const capture = receiptFile ? readCaptureSession(receiptFile, schemas.capture, failures) : undefined; const intended = canonicalIntended(options.profileRoot, failures); if (capture && !sameJson(capture.receipt.intended, intended)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from canonical profile scenarios", path: receiptFile }); -if (capture && !sourceManifestMatches(capture.receipt.source, repositoryRoot, options.profileRoot)) failures.push({ code: "capture_source_drift", message: "capture sources differ from the receipt source manifest", path: receiptFile }); +if (capture && !capture.receipt.source) failures.push({ code: "capture_source_missing", message: "capture receipt must include its canonical source manifest", path: receiptFile }); +else if (capture && !sourceManifestMatches(capture.receipt.source, repositoryRoot, options.profileRoot)) failures.push({ code: "capture_source_drift", message: "capture sources differ from the receipt source manifest", path: receiptFile }); const completedAt = new Date().toISOString(); const completedSession = capture ? { diff --git a/scripts/test-validate-component-state-artifacts.mjs b/scripts/test-validate-component-state-artifacts.mjs index 603a714..aa73d50 100644 --- a/scripts/test-validate-component-state-artifacts.mjs +++ b/scripts/test-validate-component-state-artifacts.mjs @@ -5,9 +5,9 @@ import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; -import zlib from "node:zlib"; import { artifactMetadata } from "./artifact-metadata.mjs"; import { sessionLink, sha256 } from "./capture-session-contract.mjs"; +import { makePng, syntheticImage } from "./component-state-artifact-fixture.mjs"; import { resolveProfileRecords } from "./profile-record-contract.mjs"; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -16,43 +16,6 @@ const validator = path.join(repositoryRoot, "scripts/validate-component-state.mj const finalizer = path.join(repositoryRoot, "scripts/finalize-component-state-evidence.mjs"); const creator = path.join(repositoryRoot, "scripts/create-component-state-session.mjs"); -function crc32(bytes) { - let crc = 0xffffffff; - for (const byte of bytes) { - crc ^= byte; - for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); - } - return (crc ^ 0xffffffff) >>> 0; -} - -function pngChunk(type, data) { - const name = Buffer.from(type, "ascii"); - const output = Buffer.alloc(12 + data.length); - output.writeUInt32BE(data.length, 0); - name.copy(output, 4); - data.copy(output, 8); - output.writeUInt32BE(crc32(Buffer.concat([name, data])), 8 + data.length); - return output; -} - -function makePng(red) { - const width = 64; - const height = 64; - const header = Buffer.alloc(13); - header.writeUInt32BE(width, 0); - header.writeUInt32BE(height, 4); - header[8] = 8; - header[9] = 6; - const row = Buffer.alloc(1 + (width * 4)); - for (let pixel = 0; pixel < width; pixel += 1) row.set([red, 64, 128, 255], 1 + (pixel * 4)); - return Buffer.concat([ - Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), - pngChunk("IHDR", header), - pngChunk("IDAT", zlib.deflateSync(Buffer.concat(Array.from({ length: height }, () => row)))), - pngChunk("IEND", Buffer.alloc(0)), - ]); -} - const png = makePng(32); function writeJson(file, value) { @@ -65,6 +28,15 @@ function prepareCanonical() { const profileRoot = path.join(root, "profiles"); const artifactRoot = path.join(root, "artifacts"); fs.cpSync(sourceProfiles, profileRoot, { recursive: true }); + for (const entry of fs.readdirSync(profileRoot, { withFileTypes: true }).filter((item) => item.isDirectory())) { + const resolved = resolveProfileRecords(path.join(profileRoot, entry.name), []); + const statesRecord = resolved.records.states[0]; + for (const scenario of statesRecord.value.scenarios) { + const metadata = artifactMetadata(syntheticImage(entry.name, scenario.id), "image/png"); + scenario.expected.visual_image = scenario.expected.visual_image.map((expectation) => ({ environment_id: expectation.environment_id, height: metadata.height, sha256: metadata.sha256, width: metadata.width })); + } + writeJson(statesRecord.path, statesRecord.value); + } const receiptFile = path.join(artifactRoot, "capture-session.json"); const created = spawnSync(process.execPath, [creator, "--root", profileRoot, "--output", receiptFile, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); if (created.status !== 0) throw new Error(`canonical session creation failed: ${created.stdout}${created.stderr}`); @@ -108,7 +80,7 @@ function prepareCanonical() { semantic_mode: scenario.semantic_mode, }); fs.mkdirSync(path.dirname(`${prefix}.png`), { recursive: true }); - const image = Buffer.concat([png, Buffer.from(`${entry.name}:${fixture.id}`)]); + const image = syntheticImage(entry.name, fixture.id); fs.writeFileSync(`${prefix}.png`, image); writeJson(`${prefix}.visual.json`, { capture_session: captureLink, @@ -163,6 +135,7 @@ const cases = [ { expect: "evidence_channel_duplicate", name: "channel_substitution", mutate: (m) => { findPass(m, "action-focused", "ax").channel = "dom"; } }, { expect: "evidence_png_invalid", name: "dummy_visual_bytes", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); fs.writeFileSync(path.join(a, p.artifact.path), "not a png"); refresh(p, a, true); } }, { expect: "evidence_visual_image_mismatch", name: "wrong_valid_png", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); fs.writeFileSync(path.join(a, p.artifact.path), makePng(224)); refresh(p, a); } }, + { expect: "evidence_visual_expectation_mismatch", name: "coordinated_visual_substitution", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); const file = path.join(a, p.artifact.path); const image = makePng(224); fs.writeFileSync(file, image); refresh(p, a); const sidecar = path.join(a, p.artifact.path.replace(/\.png$/, ".visual.json")); const document = JSON.parse(fs.readFileSync(sidecar)); document.image = { path: path.basename(file), ...artifactMetadata(image, "image/png") }; writeJson(sidecar, document); } }, { expect: "evidence_json_invalid", name: "png_as_dom", mutate: (m, a) => { const p = findPass(m, "action-focused", "dom"); fs.writeFileSync(path.join(a, p.artifact.path), png); refresh(p, a); } }, { expect: "evidence_artifact_symlink", name: "artifact_symlink", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); const f = path.join(a, p.artifact.path); fs.rmSync(f); fs.symlinkSync("/etc/hosts", f); } }, { expect: "evidence_artifact_reused", name: "artifact_path_reuse", mutate: (m) => { const p = findPass(m, "action-loading-busy", "visual"); p.artifact = structuredClone(findPass(m, "action-focused", "visual").artifact); } }, @@ -185,6 +158,11 @@ const cases = [ { expect: "evidence_ax_schema_invalid", name: "ax_null_type", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { d.properties.focused = null; }); } }, { expect: "evidence_ax_schema_invalid", name: "ax_missing_key", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { delete d.role; }); } }, { expect: "capture_session_receipt_mismatch", name: "receipt_tamper", mutate: (m, a) => { const f = path.join(a, "capture-session.json"); const r = JSON.parse(fs.readFileSync(f)); r.nonce = "0".repeat(64); writeJson(f, r); } }, + { expect: "capture_session_schema_invalid", name: "receipt_source_missing", mutate: (_m, a) => { const f = path.join(a, "capture-session.json"); const r = JSON.parse(fs.readFileSync(f)); delete r.source; writeJson(f, r); } }, + { expect: "runtime_manifest_schema_invalid", name: "completed_session_source_missing", mutate: (m) => { delete m.session.source; } }, + { expect: "evidence_visual_schema_invalid", name: "visual_session_source_missing", mutate: (m, a) => { const p = findPass(m, "action-focused", "visual"); const f = path.join(a, p.artifact.path.replace(/\.png$/, ".visual.json")); const d = JSON.parse(fs.readFileSync(f)); delete d.capture_session.source; writeJson(f, d); } }, + { expect: "evidence_dom_schema_invalid", name: "dom_session_source_missing", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "dom", (d) => { delete d.capture_session.source; }); } }, + { expect: "evidence_ax_schema_invalid", name: "ax_session_source_missing", mutate: (m, a) => { mutateJsonArtifact(m, a, "action-focused", "ax", (d) => { delete d.capture_session.source; }); } }, { expect: "capture_session_receipt_mismatch", name: "isolated_session_replay", mutate: (_m, a) => { fs.copyFileSync(path.join(alternate.artifactRoot, "capture-session.json"), path.join(a, "capture-session.json")); } }, { expect: "capture_session_missing", name: "receipt_missing", mutate: (m, a) => { fs.rmSync(path.join(a, "capture-session.json")); } }, { expect: "capture_session_mismatch", name: "cross_session_artifact_mix", mutate: (m, a) => { const target = findPass(m, "action-focused", "dom"); const sourceManifest = JSON.parse(fs.readFileSync(alternate.manifest)); const source = findPass(sourceManifest, "action-focused", "dom"); fs.copyFileSync(path.join(alternate.artifactRoot, source.artifact.path), path.join(a, target.artifact.path)); refresh(target, a); } }, diff --git a/scripts/test-validate-component-state.mjs b/scripts/test-validate-component-state.mjs index 37c988a..71a0495 100644 --- a/scripts/test-validate-component-state.mjs +++ b/scripts/test-validate-component-state.mjs @@ -47,6 +47,9 @@ const cases = [ ["missing_role_required_state", "role_required_aria_state", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.semantic_modes[1].aria_states = value.semantic_modes[1].aria_states.filter((state) => state.name !== "pressed"); })], ["switch_mixed", "switch_mixed_prohibited", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.semantic_modes[1] = { ...value.semantic_modes[1], aria_states: [{ name: "checked", status: "required", values: ["false", "mixed"] }], id: "switch", role: "switch" }; })], ["missing_environment", "evidence_schema_invalid", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { delete value.passes[0].environment; })], + ["committed_visual_expectation_mismatch", "evidence_visual_expectation_mismatch", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { pass(value, "action-disabled-busy", "visual").artifact.sha256 = `sha256:${"0".repeat(64)}`; })], + ["missing_committed_source", "capture_source_missing", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { delete value.passes[0].session.source; })], + ["committed_source_drift", "capture_source_drift", (root) => fs.appendFileSync(path.join(root, "editorial/tokens.dtcg.json"), "\n")], ["false_aggregate_pass", "aggregate_pass_forbidden", (root) => edit(root, "editorial", "evidence/button.evidence.json", (value) => { value.aggregate_pass = true; })], ["normative_conflict", "normative_activation_conflict", (root) => edit(root, "editorial", "states/button.states.json", (value) => { scenario(value, "action-focused").expected.activation = "suppressed"; })], ["preview_rolled_stable", "preview_rolled_stable", (root) => edit(root, "editorial", "components/button.component.json", (value) => { value.versions.delivery_channel = "stable"; })], diff --git a/scripts/validate-component-state.mjs b/scripts/validate-component-state.mjs index 4952d32..0a2bab1 100644 --- a/scripts/validate-component-state.mjs +++ b/scripts/validate-component-state.mjs @@ -4,6 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { + canonicalSourceManifest, canonicalIntended, readCaptureSession, sameJson, @@ -11,10 +12,33 @@ import { withinSession, } from "./capture-session-contract.mjs"; import { compileSchemas, readRecord, validateEvidenceArtifacts, validateProfile } from "./component-state-contract.mjs"; +import { resolveProfileRecords } from "./profile-record-contract.mjs"; +import { visualExpectationFor } from "./visual-expectation-contract.mjs"; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const options = { artifactRoot: undefined, json: false, root: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), runtimeManifest: undefined }; const failures = []; + +function validateCommittedVisualExpectations(resolved) { + const evidenceRecord = resolved.records.evidence[0]; + const statesRecord = resolved.records.states[0]; + if (!evidenceRecord || !statesRecord) return; + const scenarios = new Map((statesRecord.value.scenarios ?? []).map((scenario) => [scenario.id, scenario])); + for (const pass of (evidenceRecord.value.passes ?? []).filter((candidate) => candidate.channel === "visual")) { + const scenario = scenarios.get(pass.scenario_id); + if (!scenario) continue; + try { + const expected = visualExpectationFor(scenario, pass.environment, statesRecord.value.visual_environments ?? []); + const actual = pass.artifact ?? {}; + if (actual.sha256 !== expected.sha256 || actual.width !== expected.width || actual.height !== expected.height) { + failures.push({ code: "evidence_visual_expectation_mismatch", message: `${pass.id} committed visual metadata differs from its canonical environment expectation`, path: evidenceRecord.path }); + } + } catch (error) { + failures.push({ code: "evidence_visual_expectation_invalid", message: error instanceof Error ? error.message : String(error), path: evidenceRecord.path }); + } + } +} + for (let index = 2; index < process.argv.length; index += 1) { const argument = process.argv[index]; if (argument === "--json") options.json = true; @@ -33,6 +57,11 @@ for (let index = 2; index < process.argv.length; index += 1) { const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/schema")); let runtimeManifest; let capture; +let canonicalSource; +if (!options.runtimeManifest) { + try { canonicalSource = canonicalSourceManifest(repositoryRoot, options.root); } + catch (error) { failures.push({ code: "capture_source_unreadable", message: error instanceof Error ? error.message : String(error), path: options.root }); } +} if (options.runtimeManifest) { runtimeManifest = readRecord(options.runtimeManifest, failures); if (!options.artifactRoot) failures.push({ code: "runtime_manifest_artifact_root_required", message: "--runtime-manifest requires --artifact-root", path: "" }); @@ -48,7 +77,8 @@ if (options.runtimeManifest) { if (!withinSession(session?.completed_at, capture.receipt.started_at, session?.completed_at)) failures.push({ code: "capture_session_time_outside", message: "completed_at precedes session start", path: options.runtimeManifest }); const canonical = canonicalIntended(options.root, failures); if (!sameJson(capture.receipt.intended, canonical)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from current canonical records", path: options.runtimeManifest }); - if (!sourceManifestMatches(capture.receipt.source, repositoryRoot, options.root)) failures.push({ code: "capture_source_drift", message: "current capture sources differ from the receipt source manifest", path: options.runtimeManifest }); + if (!capture.receipt.source) failures.push({ code: "capture_source_missing", message: "capture receipt must include its canonical source manifest", path: options.runtimeManifest }); + else if (!sourceManifestMatches(capture.receipt.source, repositoryRoot, options.root)) failures.push({ code: "capture_source_drift", message: "current capture sources differ from the receipt source manifest", path: options.runtimeManifest }); const run = runtimeManifest.run; if (run?.id !== capture.receipt.session_id || run?.repository !== capture.receipt.repository || run?.revision !== capture.receipt.revision || run?.attempt !== capture.receipt.attempt) failures.push({ code: "capture_session_mismatch", message: "manifest run differs from receipt", path: options.runtimeManifest }); for (const record of runtimeManifest.records ?? []) { @@ -67,6 +97,17 @@ if (profiles.length === 0) failures.push({ code: "profile_root_empty", message: for (const profile of profiles) { const profileRoot = path.join(options.root, profile); failures.push(...validateProfile(profileRoot, schemas)); + if (!runtimeManifest) { + const resolved = resolveProfileRecords(profileRoot, failures); + if (resolved) validateCommittedVisualExpectations(resolved); + const evidenceRecord = resolved?.records.evidence[0]; + if (canonicalSource) { + for (const pass of evidenceRecord?.value.passes ?? []) { + if (!pass.session?.source) failures.push({ code: "capture_source_missing", message: `${pass.id} committed evidence must include its canonical source manifest`, path: evidenceRecord.path }); + else if (!sameJson(pass.session.source, canonicalSource)) failures.push({ code: "capture_source_drift", message: `${pass.id} committed evidence source differs from current canonical capture sources`, path: evidenceRecord.path }); + } + } + } const profileId = JSON.parse(fs.readFileSync(path.join(profileRoot, "profile.json"), "utf8")).id; const matching = runtimeManifest?.records?.filter((record) => record.profile_id === profileId) ?? []; if (runtimeManifest && matching.length !== 1) failures.push({ code: "runtime_manifest_profile_count", message: `${profileId} requires exactly one runtime evidence record`, path: options.runtimeManifest }); From f2a423c37b401e2ef1278e963cbc88c490c9666b Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 08:41:19 +0900 Subject: [PATCH 20/44] Close workflow governance bypasses --- scripts/governance-test-fixture.mjs | 13 +-- .../governance-workflow-negative-cases.mjs | 49 +++++++++++ scripts/test-validate-governance.mjs | 12 +-- scripts/validate-governance.mjs | 7 +- scripts/workflow-action-contract.mjs | 83 +++++++++++++++++-- 5 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 scripts/governance-workflow-negative-cases.mjs diff --git a/scripts/governance-test-fixture.mjs b/scripts/governance-test-fixture.mjs index 331e5a2..260b7fd 100644 --- a/scripts/governance-test-fixture.mjs +++ b/scripts/governance-test-fixture.mjs @@ -77,11 +77,12 @@ export const files = { ".github/workflows/validate.yml": [ "jobs:", " validate:", - "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", - " with:", - " persist-credentials: false", - "uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", - "uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", + " steps:", + " - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", + " with:", + " persist-credentials: false", + " - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", + " - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", "node -c scripts/component-state-workflow-contract.mjs", "node -c scripts/promotion-fixture-inventory.mjs", "node -c scripts/promotion-boundary-test-contract.mjs", @@ -156,7 +157,7 @@ export const files = { "\"repository\":\"%s\",\"execution_repository\":\"%s\"", "\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", "permissions:", - "contents: read", + " contents: read", "", ].join("\n"), "GOVERNANCE.md": [ diff --git a/scripts/governance-workflow-negative-cases.mjs b/scripts/governance-workflow-negative-cases.mjs new file mode 100644 index 0000000..72f4d3a --- /dev/null +++ b/scripts/governance-workflow-negative-cases.mjs @@ -0,0 +1,49 @@ +export function workflowSafetyCases(workflow) { + return [ + { + name: "floating_action_ref", + mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", "uses: actions/checkout@v4") }, + expect: ".github/workflows/validate.yml: floating or unlabeled action ref uses: actions/checkout@v4", + }, + { + name: "checkout_credentials_persisted", + mutate: { ".github/workflows/validate.yml": workflow.replace(" persist-credentials: false\n", "") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "third_party_floating_action_ref", + mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "uses: attacker/action@main") }, + expect: ".github/workflows/validate.yml: floating or unlabeled action ref uses: attacker/action@main", + }, + { + name: "checkout_true_with_later_env_false", + mutate: { ".github/workflows/validate.yml": workflow.replace(" persist-credentials: false\n", " persist-credentials: true\n env:\n persist-credentials: false\n") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "checkout_true_with_block_scalar_false", + mutate: { ".github/workflows/validate.yml": workflow.replace(" persist-credentials: false\n", " sparse-checkout: |\n persist-credentials: false\n persist-credentials: true\n") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "checkout_duplicate_credentials", + mutate: { ".github/workflows/validate.yml": workflow.replace(" persist-credentials: false\n", " persist-credentials: false\n persist-credentials: false\n") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "extra_top_level_permission", + mutate: { ".github/workflows/validate.yml": workflow.replace(" contents: read\n", " contents: read\n issues: read\n") }, + expect: ".github/workflows/validate.yml: top-level permissions must be exactly contents: read", + }, + { + name: "write_top_level_permission", + mutate: { ".github/workflows/validate.yml": workflow.replace(" contents: read\n", " contents: write\n") }, + expect: ".github/workflows/validate.yml: top-level permissions must be exactly contents: read", + }, + { + name: "job_level_permission_override", + mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\n permissions:\n contents: read\n") }, + expect: ".github/workflows/validate.yml: job-level permissions overrides are forbidden", + }, + ]; +} diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index 0aba880..e0ac211 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -6,6 +6,7 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { files, generatedWarning, sentinelProvenanceClauses } from "./governance-test-fixture.mjs"; +import { workflowSafetyCases } from "./governance-workflow-negative-cases.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-governance.mjs"); @@ -145,16 +146,6 @@ const cases = [ }, { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, { name: "missing_component_state_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles.", "Component evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles." }, - { - name: "floating_action_ref", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", "uses: actions/checkout@v4") }, - expect: ".github/workflows/validate.yml: floating or unlabeled action ref uses: actions/checkout@v4", - }, - { - name: "checkout_credentials_persisted", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" persist-credentials: false\n", "") }, - expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", - }, { name: "missing_scoped_checkout_sha_trust", mutate: { @@ -190,6 +181,7 @@ const cases = [ mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", "\"$GITHUB_REPOSITORY\" \"changeroa/StyleGallery\" \"$GITHUB_RUN_ID\"") }, expect: ".github/workflows/validate.yml: missing \"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", }, + ...workflowSafetyCases(files[".github/workflows/validate.yml"]), { name: "success_path", expect: null }, ]; diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 6272102..d14ddb2 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -5,7 +5,7 @@ import path from "node:path"; import { componentStateWorkflowFailures } from "./component-state-workflow-contract.mjs"; import { immutableActionPins, requiredCodeowners, sentinelProvenanceClauses } from "./governance-policy-contract.mjs"; import { promotionGovernanceFailures } from "./promotion-governance-contract.mjs"; -import { checkoutCredentialFailures } from "./workflow-action-contract.mjs"; +import { workflowActionFailures } from "./workflow-action-contract.mjs"; const json = new Set(process.argv.slice(2)).has("--json"); const root = process.cwd(); @@ -162,11 +162,8 @@ function requireComponentStateCiIsolation() { function requireImmutableActions() { const relative = ".github/workflows/validate.yml"; const workflow = read(relative); - for (const line of workflow.split("\n").filter((entry) => /^\s*uses:\s+actions\//.test(entry))) { - if (!/^\s*uses:\s+actions\/[a-z0-9-]+@[a-f0-9]{40}\s+#\s+v\d+\s*$/.test(line)) failures.push(`${relative}: floating or unlabeled action ref ${line.trim()}`); - } for (const pin of immutableActionPins) if (!workflow.includes(pin)) failures.push(`${relative}: missing immutable action pin ${pin}`); - failures.push(...checkoutCredentialFailures(workflow, relative)); + failures.push(...workflowActionFailures(workflow, relative)); } function requireRootLinks() { diff --git a/scripts/workflow-action-contract.mjs b/scripts/workflow-action-contract.mjs index 6e20804..49e66a3 100644 --- a/scripts/workflow-action-contract.mjs +++ b/scripts/workflow-action-contract.mjs @@ -1,17 +1,82 @@ +function indentation(line) { + return line.length - line.trimStart().length; +} + +function mappingKeyIndentation(line) { + const sequencePrefix = line.match(/^\s*-\s+/); + return sequencePrefix ? sequencePrefix[0].length : indentation(line); +} + +function directMappingLines(lines, headerIndex, headerIndentation) { + const nested = []; + for (const line of lines.slice(headerIndex + 1)) { + if (line.trim() === "" || line.trimStart().startsWith("#")) continue; + const lineIndentation = indentation(line); + if (lineIndentation <= headerIndentation) break; + nested.push({ indentation: lineIndentation, trimmed: line.trim() }); + } + if (nested.length === 0) return []; + const directIndentation = Math.min(...nested.map((line) => line.indentation)); + return nested.filter((line) => line.indentation === directIndentation).map((line) => line.trimmed); +} + +function actionRefFailures(lines, relative) { + const failures = []; + for (const line of lines) { + const match = line.trim().match(/^-?\s*uses:\s+([^\s#]+)/); + if (!match) continue; + const reference = match[1]; + if (reference.startsWith("./") || reference.startsWith("docker://")) continue; + if (!/^[^@\s]+@[a-f0-9]{40}$/i.test(reference)) failures.push(`${relative}: floating or unlabeled action ref ${line.trim().replace(/^-\s*/, "")}`); + } + return failures; +} + export function checkoutCredentialFailures(workflow, relative) { const failures = []; const lines = workflow.split("\n"); for (const [index, line] of lines.entries()) { - if (!line.trim().startsWith("uses: actions/checkout@")) continue; - const indentation = line.length - line.trimStart().length; - let credentialsPersist = true; - for (const following of lines.slice(index + 1)) { - if (following.trim() === "") continue; - const followingIndentation = following.length - following.trimStart().length; - if (followingIndentation < indentation || (followingIndentation === indentation && following.trim().startsWith("uses:"))) break; - if (followingIndentation > indentation && following.trim() === "persist-credentials: false") credentialsPersist = false; + if (!/^-?\s*uses:\s+actions\/checkout@/.test(line.trim())) continue; + const usesIndentation = mappingKeyIndentation(line); + const withHeaders = []; + for (const [offset, following] of lines.slice(index + 1).entries()) { + if (following.trim() === "" || following.trimStart().startsWith("#")) continue; + const followingIndentation = indentation(following); + if (followingIndentation < usesIndentation) break; + if (followingIndentation === usesIndentation && following.trim() === "with:") withHeaders.push(index + offset + 1); } - if (credentialsPersist) failures.push(`${relative}: every actions/checkout step must set persist-credentials: false`); + const entries = withHeaders.length === 1 ? directMappingLines(lines, withHeaders[0], usesIndentation) : []; + const credentialEntries = entries.filter((entry) => /^(?:["']persist-credentials["']|persist-credentials)\s*:/.test(entry)); + const exactFalse = credentialEntries.length === 1 && /^(?:["']persist-credentials["']|persist-credentials)\s*:\s*false(?:\s+#.*)?$/.test(credentialEntries[0]); + if (!exactFalse) failures.push(`${relative}: every actions/checkout step must set persist-credentials: false`); } return failures; } + +function permissionFailures(lines, relative) { + const failures = []; + const headers = lines.map((line, index) => ({ index, indentation: indentation(line), line })).filter(({ line }) => /^permissions\s*:/.test(line.trim())); + if (headers.some((header) => header.indentation > 0)) failures.push(`${relative}: job-level permissions overrides are forbidden`); + const topLevel = headers.filter((header) => header.indentation === 0); + let exact = topLevel.length === 1 && topLevel[0].line.trim() === "permissions:"; + if (exact) { + const entries = []; + for (const line of lines.slice(topLevel[0].index + 1)) { + if (line.trim() === "" || line.trimStart().startsWith("#")) continue; + if (indentation(line) === 0) break; + entries.push(line.trim()); + } + exact = entries.length === 1 && entries[0] === "contents: read"; + } + if (!exact) failures.push(`${relative}: top-level permissions must be exactly contents: read`); + return failures; +} + +export function workflowActionFailures(workflow, relative) { + const lines = workflow.split("\n"); + return [ + ...actionRefFailures(lines, relative), + ...checkoutCredentialFailures(workflow, relative), + ...permissionFailures(lines, relative), + ]; +} From 74f4caf16b3b91b47065f12d782319b9eb2d8826 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 09:38:48 +0900 Subject: [PATCH 21/44] Reject quoted workflow safety bypasses --- .../governance-workflow-negative-cases.mjs | 46 +++++++++++++++++++ scripts/workflow-action-contract.mjs | 32 +++++++++---- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/scripts/governance-workflow-negative-cases.mjs b/scripts/governance-workflow-negative-cases.mjs index 72f4d3a..c6722a7 100644 --- a/scripts/governance-workflow-negative-cases.mjs +++ b/scripts/governance-workflow-negative-cases.mjs @@ -45,5 +45,51 @@ export function workflowSafetyCases(workflow) { mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\n permissions:\n contents: read\n") }, expect: ".github/workflows/validate.yml: job-level permissions overrides are forbidden", }, + { + name: "single_quoted_third_party_floating_action_ref", + mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "'uses': attacker/action@main") }, + expect: ".github/workflows/validate.yml: floating or unlabeled action ref 'uses': attacker/action@main", + }, + { + name: "double_quoted_third_party_floating_action_ref", + mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "\"uses\": attacker/action@main") }, + expect: ".github/workflows/validate.yml: floating or unlabeled action ref \"uses\": attacker/action@main", + }, + { + name: "single_quoted_checkout_floating_ref", + mutate: { ".github/workflows/validate.yml": quotedCheckout(workflow, "'") }, + expect: ".github/workflows/validate.yml: floating or unlabeled action ref 'uses': actions/checkout@v4", + }, + { + name: "single_quoted_checkout_credentials_persisted", + mutate: { ".github/workflows/validate.yml": quotedCheckout(workflow, "'") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "double_quoted_checkout_floating_ref", + mutate: { ".github/workflows/validate.yml": quotedCheckout(workflow, "\"") }, + expect: ".github/workflows/validate.yml: floating or unlabeled action ref \"uses\": actions/checkout@v4", + }, + { + name: "double_quoted_checkout_credentials_persisted", + mutate: { ".github/workflows/validate.yml": quotedCheckout(workflow, "\"") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "single_quoted_job_level_permission_override", + mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\n 'permissions':\n contents: read\n") }, + expect: ".github/workflows/validate.yml: job-level permissions overrides are forbidden", + }, + { + name: "double_quoted_job_level_permission_override", + mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\n \"permissions\":\n contents: read\n") }, + expect: ".github/workflows/validate.yml: job-level permissions overrides are forbidden", + }, ]; } + +function quotedCheckout(workflow, quote) { + return workflow + .replace("uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", `${quote}uses${quote}: actions/checkout@v4`) + .replace(" with:\n persist-credentials: false", ` ${quote}with${quote}:\n ${quote}persist-credentials${quote}: true`); +} diff --git a/scripts/workflow-action-contract.mjs b/scripts/workflow-action-contract.mjs index 49e66a3..a0e5084 100644 --- a/scripts/workflow-action-contract.mjs +++ b/scripts/workflow-action-contract.mjs @@ -7,6 +7,16 @@ function mappingKeyIndentation(line) { return sequencePrefix ? sequencePrefix[0].length : indentation(line); } +function mappingEntry(line) { + const match = line.trim().replace(/^-\s+/, "").match(/^(?:([A-Za-z][A-Za-z0-9_-]*)|'([^']*)'|"([^"]*)")\s*:\s*(.*)$/); + if (!match) return null; + return { key: match[1] ?? match[2] ?? match[3], value: match[4] }; +} + +function isMappingHeader(entry, key) { + return entry?.key === key && (entry.value === "" || entry.value.startsWith("#")); +} + function directMappingLines(lines, headerIndex, headerIndentation) { const nested = []; for (const line of lines.slice(headerIndex + 1)) { @@ -23,9 +33,10 @@ function directMappingLines(lines, headerIndex, headerIndentation) { function actionRefFailures(lines, relative) { const failures = []; for (const line of lines) { - const match = line.trim().match(/^-?\s*uses:\s+([^\s#]+)/); - if (!match) continue; - const reference = match[1]; + const entry = mappingEntry(line); + if (entry?.key !== "uses") continue; + const reference = entry.value.match(/^([^\s#]+)/)?.[1]; + if (!reference) continue; if (reference.startsWith("./") || reference.startsWith("docker://")) continue; if (!/^[^@\s]+@[a-f0-9]{40}$/i.test(reference)) failures.push(`${relative}: floating or unlabeled action ref ${line.trim().replace(/^-\s*/, "")}`); } @@ -36,18 +47,19 @@ export function checkoutCredentialFailures(workflow, relative) { const failures = []; const lines = workflow.split("\n"); for (const [index, line] of lines.entries()) { - if (!/^-?\s*uses:\s+actions\/checkout@/.test(line.trim())) continue; + const usesEntry = mappingEntry(line); + if (usesEntry?.key !== "uses" || !usesEntry.value.startsWith("actions/checkout@")) continue; const usesIndentation = mappingKeyIndentation(line); const withHeaders = []; for (const [offset, following] of lines.slice(index + 1).entries()) { if (following.trim() === "" || following.trimStart().startsWith("#")) continue; const followingIndentation = indentation(following); if (followingIndentation < usesIndentation) break; - if (followingIndentation === usesIndentation && following.trim() === "with:") withHeaders.push(index + offset + 1); + if (followingIndentation === usesIndentation && isMappingHeader(mappingEntry(following), "with")) withHeaders.push(index + offset + 1); } const entries = withHeaders.length === 1 ? directMappingLines(lines, withHeaders[0], usesIndentation) : []; - const credentialEntries = entries.filter((entry) => /^(?:["']persist-credentials["']|persist-credentials)\s*:/.test(entry)); - const exactFalse = credentialEntries.length === 1 && /^(?:["']persist-credentials["']|persist-credentials)\s*:\s*false(?:\s+#.*)?$/.test(credentialEntries[0]); + const credentialEntries = entries.map(mappingEntry).filter((entry) => entry?.key === "persist-credentials"); + const exactFalse = credentialEntries.length === 1 && /^false(?:\s+#.*)?$/.test(credentialEntries[0].value); if (!exactFalse) failures.push(`${relative}: every actions/checkout step must set persist-credentials: false`); } return failures; @@ -55,10 +67,12 @@ export function checkoutCredentialFailures(workflow, relative) { function permissionFailures(lines, relative) { const failures = []; - const headers = lines.map((line, index) => ({ index, indentation: indentation(line), line })).filter(({ line }) => /^permissions\s*:/.test(line.trim())); + const headers = lines + .map((line, index) => ({ entry: mappingEntry(line), index, indentation: indentation(line) })) + .filter(({ entry }) => entry?.key === "permissions"); if (headers.some((header) => header.indentation > 0)) failures.push(`${relative}: job-level permissions overrides are forbidden`); const topLevel = headers.filter((header) => header.indentation === 0); - let exact = topLevel.length === 1 && topLevel[0].line.trim() === "permissions:"; + let exact = topLevel.length === 1 && isMappingHeader(topLevel[0].entry, "permissions"); if (exact) { const entries = []; for (const line of lines.slice(topLevel[0].index + 1)) { From 67ff91361eaacaf756ccf8d67248f2ec5f941cb1 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 09:38:56 +0900 Subject: [PATCH 22/44] Preserve portable JSON CLI output --- .github/workflows/validate.yml | 4 + scripts/build-reference-artifacts.mjs | 28 ++-- scripts/test-json-cli-output.mjs | 151 +++++++++++++++++++ scripts/test-validate-consumer-reference.mjs | 2 +- scripts/test-validate-ia.mjs | 2 +- scripts/test-validate-okf.mjs | 2 +- scripts/test-validate-patterns.mjs | 2 +- scripts/test-validate-reference-profiles.mjs | 2 +- scripts/test-validate-vocabulary.mjs | 2 +- scripts/test-validate-webpage-workflow.mjs | 2 +- scripts/validate-catalog.mjs | 2 +- scripts/validate-consumer-reference.mjs | 2 +- scripts/validate-domains.mjs | 2 +- scripts/validate-governance.mjs | 2 +- scripts/validate-ia.mjs | 2 +- scripts/validate-links.mjs | 2 +- scripts/validate-okf.mjs | 2 +- scripts/validate-patterns.mjs | 2 +- scripts/validate-reference-artifacts.mjs | 2 +- scripts/validate-vocabulary.mjs | 2 +- scripts/validate-webpage-workflow.mjs | 2 +- 21 files changed, 187 insertions(+), 32 deletions(-) create mode 100644 scripts/test-json-cli-output.mjs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0c2ecbf..d3321fd 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -62,6 +62,7 @@ jobs: node -c scripts/test-validate-webpage-workflow.mjs node -c scripts/validate-consumer-reference.mjs node -c scripts/test-validate-consumer-reference.mjs + node -c scripts/test-json-cli-output.mjs node -c scripts/consumer-reference-case-runner.mjs node -c scripts/validate-promotion-rfc.mjs node -c scripts/test-validate-promotion-rfc.mjs @@ -160,6 +161,9 @@ jobs: node scripts/validate-consumer-reference.mjs --json node scripts/test-validate-consumer-reference.mjs --json + - name: JSON CLI output integrity + run: node scripts/test-json-cli-output.mjs + - name: Shared promotion policy contract run: | node scripts/validate-promotion-rfc.mjs --json diff --git a/scripts/build-reference-artifacts.mjs b/scripts/build-reference-artifacts.mjs index c3242f4..d520c04 100644 --- a/scripts/build-reference-artifacts.mjs +++ b/scripts/build-reference-artifacts.mjs @@ -47,23 +47,21 @@ function emit(report, json) { } const parsed = parseArguments(); -if (parsed.failure) { - emit({ failures: [parsed.failure], ok: false, warnings: [] }, parsed.options.json); - process.exit(1); -} const options = parsed.options; const source = path.resolve(root, options.source); const output = path.resolve(root, options.output); const manifestPath = path.resolve(root, options.manifest); -const failures = []; +const failures = parsed.failure ? [parsed.failure] : []; const warnings = []; -for (const [code, target] of [ - ["token_source_untrusted", source], - ["artifact_output_untrusted", output], - ["artifact_manifest_untrusted", manifestPath], -]) { - const inspection = inspectArtifactPath(root, target, code !== "token_source_untrusted"); - if (!inspection.ok) failures.push({ code, message: `${target} must be a contained regular non-symlink file`, path: path.relative(root, target) || "." }); +if (!parsed.failure) { + for (const [code, target] of [ + ["token_source_untrusted", source], + ["artifact_output_untrusted", output], + ["artifact_manifest_untrusted", manifestPath], + ]) { + const inspection = inspectArtifactPath(root, target, code !== "token_source_untrusted"); + if (!inspection.ok) failures.push({ code, message: `${target} must be a contained regular non-symlink file`, path: path.relative(root, target) || "." }); + } } let sourceBytes; let document; @@ -122,6 +120,8 @@ if (failures.length === 0) { fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); } } -const report = { adapter, failures, manifest: options.manifest, ok: failures.length === 0, output: options.output, outputCount: failures.length === 0 ? contract.tokens.length : 0, warnings }; +const report = parsed.failure + ? { failures: [parsed.failure], ok: false, warnings } + : { adapter, failures, manifest: options.manifest, ok: failures.length === 0, output: options.output, outputCount: failures.length === 0 ? contract.tokens.length : 0, warnings }; emit(report, options.json); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-json-cli-output.mjs b/scripts/test-json-cli-output.mjs new file mode 100644 index 0000000..117d4e5 --- /dev/null +++ b/scripts/test-json-cli-output.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptPath = fileURLToPath(import.meta.url); +const root = path.resolve(path.dirname(scriptPath), ".."); +const cases = [ + { kind: "validator", script: "validate-consumer-reference.mjs" }, + { kind: "harness", script: "test-validate-consumer-reference.mjs" }, +]; + +function reportIsComplete(kind, report) { + if (!report || typeof report !== "object" || report.ok !== true) return false; + if (kind === "validator") { + return Number.isInteger(report.checkedItems) + && report.checkedItems > 0 + && Array.isArray(report.profiles); + } + return kind === "harness" && Array.isArray(report.results) && report.results.length > 30; +} + +function readStream(stream) { + return new Promise((resolve, reject) => { + stream.setEncoding("utf8"); + let output = ""; + stream.on("data", (chunk) => { output += chunk; }); + stream.once("end", () => resolve(output)); + stream.once("error", reject); + }); +} + +function waitForClose(child, label) { + return new Promise((resolve, reject) => { + child.once("error", (error) => reject(new Error(`${label} failed to start: ${error.message}`, { cause: error }))); + child.once("close", (status, signal) => resolve({ signal, status })); + }); +} + +async function parsePipe(kind) { + try { + const output = await readStream(process.stdin); + const report = JSON.parse(output); + const complete = reportIsComplete(kind, report); + const summary = { + bytes: Buffer.byteLength(output), + complete, + digest: createHash("sha256").update(output).digest("hex"), + }; + process.stdout.write(`${JSON.stringify(summary)}\n`); + process.exitCode = complete ? 0 : 1; + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; + } +} + +async function runPipeCase(testCase, run) { + const producer = spawn(process.execPath, [path.join(root, "scripts", testCase.script), "--json"], { + cwd: root, + stdio: ["ignore", "pipe", "pipe"], + }); + const parser = spawn(process.execPath, [scriptPath, "--pipe-parser", testCase.kind], { + cwd: root, + stdio: ["pipe", "pipe", "pipe"], + }); + const directOutputPromise = readStream(producer.stdout); + const producerStderrPromise = readStream(producer.stderr); + const parserOutputPromise = readStream(parser.stdout); + const parserStderrPromise = readStream(parser.stderr); + let pipeError = null; + parser.stdin.once("error", (error) => { pipeError = error; }); + producer.stdout.pipe(parser.stdin); + + const [producerClose, parserClose, directOutput, producerStderr, parserOutput, parserStderr] = await Promise.all([ + waitForClose(producer, `${testCase.kind} producer`), + waitForClose(parser, `${testCase.kind} parser`), + directOutputPromise, + producerStderrPromise, + parserOutputPromise, + parserStderrPromise, + ]); + + let directReport; + let pipeReport; + try { + directReport = JSON.parse(directOutput); + pipeReport = JSON.parse(parserOutput); + } catch (error) { + return { + actual: { + parserStatus: parserClose.status, + parserStderr: parserStderr.trim(), + pipeError: pipeError?.message ?? "", + producerStatus: producerClose.status, + producerStderr: producerStderr.trim(), + }, + expected: "complete parseable JSON through a real Node stream pipe", + name: `${testCase.kind}_pipe_run_${run}`, + ok: false, + reason: error instanceof Error ? error.message : String(error), + }; + } + + const directBytes = Buffer.byteLength(directOutput); + const directDigest = createHash("sha256").update(directOutput).digest("hex"); + const equivalent = pipeReport.bytes === directBytes && pipeReport.digest === directDigest; + const complete = reportIsComplete(testCase.kind, directReport) && pipeReport.complete === true; + return { + actual: { + complete, + directBytes, + equivalent, + parserSignal: parserClose.signal, + parserStatus: parserClose.status, + parserStderr: parserStderr.trim(), + pipeBytes: pipeReport.bytes, + pipeError: pipeError?.message ?? "", + producerSignal: producerClose.signal, + producerStatus: producerClose.status, + producerStderr: producerStderr.trim(), + }, + expected: "complete parseable JSON through a real Node stream pipe", + name: `${testCase.kind}_pipe_run_${run}`, + ok: producerClose.status === 0 + && producerClose.signal === null + && parserClose.status === 0 + && parserClose.signal === null + && producerStderr.length === 0 + && parserStderr.length === 0 + && pipeError === null + && directBytes > 512 + && complete + && equivalent, + }; +} + +async function main() { + const results = []; + for (const testCase of cases) { + for (const run of [1, 2]) results.push(await runPipeCase(testCase, run)); + } + const report = { ok: results.every((result) => result.ok), results }; + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + process.exitCode = report.ok ? 0 : 1; +} + +if (process.argv[2] === "--pipe-parser") await parsePipe(process.argv[3]); +else await main(); diff --git a/scripts/test-validate-consumer-reference.mjs b/scripts/test-validate-consumer-reference.mjs index f887bc3..5daa310 100644 --- a/scripts/test-validate-consumer-reference.mjs +++ b/scripts/test-validate-consumer-reference.mjs @@ -185,4 +185,4 @@ const report = { }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-ia.mjs b/scripts/test-validate-ia.mjs index d25c5e8..3a283cf 100644 --- a/scripts/test-validate-ia.mjs +++ b/scripts/test-validate-ia.mjs @@ -240,4 +240,4 @@ const report = { }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-okf.mjs b/scripts/test-validate-okf.mjs index 8dacdfd..5471ea8 100644 --- a/scripts/test-validate-okf.mjs +++ b/scripts/test-validate-okf.mjs @@ -70,4 +70,4 @@ const report = { }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-patterns.mjs b/scripts/test-validate-patterns.mjs index 890476f..af28966 100644 --- a/scripts/test-validate-patterns.mjs +++ b/scripts/test-validate-patterns.mjs @@ -154,4 +154,4 @@ const report = { }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-reference-profiles.mjs b/scripts/test-validate-reference-profiles.mjs index 94c46e6..4d882b9 100644 --- a/scripts/test-validate-reference-profiles.mjs +++ b/scripts/test-validate-reference-profiles.mjs @@ -167,4 +167,4 @@ const results = cases.map(runCase); const failures = results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`); const report = { failures, ok: failures.length === 0, results }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-vocabulary.mjs b/scripts/test-validate-vocabulary.mjs index d3ada7b..8c8ee0a 100644 --- a/scripts/test-validate-vocabulary.mjs +++ b/scripts/test-validate-vocabulary.mjs @@ -171,4 +171,4 @@ const report = { }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-webpage-workflow.mjs b/scripts/test-validate-webpage-workflow.mjs index c33237f..1d34f17 100644 --- a/scripts/test-validate-webpage-workflow.mjs +++ b/scripts/test-validate-webpage-workflow.mjs @@ -240,4 +240,4 @@ const report = { }; console.log(JSON.stringify(report, null, 2)); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/validate-catalog.mjs b/scripts/validate-catalog.mjs index e5dae6b..bde3c9c 100644 --- a/scripts/validate-catalog.mjs +++ b/scripts/validate-catalog.mjs @@ -62,4 +62,4 @@ if (json) { console.error(result.failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-consumer-reference.mjs b/scripts/validate-consumer-reference.mjs index 185e466..1c4e646 100644 --- a/scripts/validate-consumer-reference.mjs +++ b/scripts/validate-consumer-reference.mjs @@ -242,4 +242,4 @@ const result = { checkedHandoffs, checkedItems: options.items.length, failures: if (options.json) console.log(JSON.stringify(result, null, 2)); else if (result.ok) console.log(`ok: ${result.checkedItems} consumer reference items`); else console.error(result.failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")); -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-domains.mjs b/scripts/validate-domains.mjs index 58e85de..072e86c 100644 --- a/scripts/validate-domains.mjs +++ b/scripts/validate-domains.mjs @@ -264,4 +264,4 @@ const result = { ok: failures.length === 0, checkedDomains: domains.length, chec if (json) console.log(JSON.stringify(result, null, 2)); else if (result.ok) console.log(`ok: ${result.checkedDomains} domains, ${result.checkedLeaves} governed leaves`); else console.error(result.failures.join("\n")); -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index d14ddb2..2afaf1d 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -232,4 +232,4 @@ if (json) { console.error(result.failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-ia.mjs b/scripts/validate-ia.mjs index ac3831e..62b9d51 100644 --- a/scripts/validate-ia.mjs +++ b/scripts/validate-ia.mjs @@ -123,4 +123,4 @@ if (json) { console.error(result.failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-links.mjs b/scripts/validate-links.mjs index b81f1b0..dd828d4 100644 --- a/scripts/validate-links.mjs +++ b/scripts/validate-links.mjs @@ -63,4 +63,4 @@ if (json) { console.error(result.failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-okf.mjs b/scripts/validate-okf.mjs index 8dfb7c9..67cf46f 100644 --- a/scripts/validate-okf.mjs +++ b/scripts/validate-okf.mjs @@ -101,4 +101,4 @@ if (json) { console.error(failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-patterns.mjs b/scripts/validate-patterns.mjs index 0929d0e..0697157 100644 --- a/scripts/validate-patterns.mjs +++ b/scripts/validate-patterns.mjs @@ -243,4 +243,4 @@ if (json) { console.error(result.failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-reference-artifacts.mjs b/scripts/validate-reference-artifacts.mjs index d5cb8ab..a1ffb4a 100644 --- a/scripts/validate-reference-artifacts.mjs +++ b/scripts/validate-reference-artifacts.mjs @@ -122,4 +122,4 @@ const uniqueFailures = [...new Map(failures.map((item) => [`${item.code}:${item. const report = { failures: uniqueFailures, manifest: options.manifest, ok: uniqueFailures.length === 0, warnings }; const text = options.json ? JSON.stringify(report, null, 2) : report.ok ? "ok: reference artifacts" : uniqueFailures.map((item) => `${item.code}: ${item.message}`).join("\n"); (options.json || report.ok ? console.log : console.error)(text); -process.exit(report.ok ? 0 : 1); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/validate-vocabulary.mjs b/scripts/validate-vocabulary.mjs index 158735a..a971c02 100644 --- a/scripts/validate-vocabulary.mjs +++ b/scripts/validate-vocabulary.mjs @@ -112,4 +112,4 @@ if (json) { console.error(failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-webpage-workflow.mjs b/scripts/validate-webpage-workflow.mjs index 8be5b5f..f0e7778 100644 --- a/scripts/validate-webpage-workflow.mjs +++ b/scripts/validate-webpage-workflow.mjs @@ -211,4 +211,4 @@ if (json) { console.error(result.failures.join("\n")); } -process.exit(result.ok ? 0 : 1); +process.exitCode = result.ok ? 0 : 1; From 72be9fa99437cbab2947ea931a24c06c66731ac1 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 09:39:00 +0900 Subject: [PATCH 23/44] Fix component evidence operator path --- GOVERNANCE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 58d7344..7a790f8 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -181,6 +181,8 @@ node scripts/test-renderer-purity.mjs For capture-session-bound component-state evidence, also run: ```sh +STATE_EVIDENCE_ROOT=".tmp/consumer-reference-state" +mkdir -p "$STATE_EVIDENCE_ROOT" node scripts/create-component-state-session.mjs --output "$STATE_EVIDENCE_ROOT/capture-session.json" --json STATE_SESSION_RECEIPT="$STATE_EVIDENCE_ROOT/capture-session.json" \ STATE_ARTIFACT_DIR="$STATE_EVIDENCE_ROOT/runtime" \ From d8b863c8860c670e86866d6d098f17b5aeed05f2 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 09:59:10 +0900 Subject: [PATCH 24/44] Reject ambiguous workflow YAML notation --- .../governance-workflow-negative-cases.mjs | 27 +++++++++++++++++++ scripts/workflow-action-contract.mjs | 25 +++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/scripts/governance-workflow-negative-cases.mjs b/scripts/governance-workflow-negative-cases.mjs index c6722a7..84f6c5b 100644 --- a/scripts/governance-workflow-negative-cases.mjs +++ b/scripts/governance-workflow-negative-cases.mjs @@ -85,6 +85,26 @@ export function workflowSafetyCases(workflow) { mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\n \"permissions\":\n contents: read\n") }, expect: ".github/workflows/validate.yml: job-level permissions overrides are forbidden", }, + { + name: "flow_sequence_action_mapping", + mutate: { ".github/workflows/validate.yml": workflow.replace("- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "- { uses: attacker/action@main }") }, + expect: ".github/workflows/validate.yml: flow-style sequence mappings are forbidden", + }, + { + name: "flow_sequence_checkout_with_inline_map", + mutate: { ".github/workflows/validate.yml": flowCheckout(workflow) }, + expect: ".github/workflows/validate.yml: flow-style sequence mappings are forbidden", + }, + { + name: "double_quoted_escaped_action_key", + mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "\"u\\u0073es\": attacker/action@main") }, + expect: ".github/workflows/validate.yml: escape sequences in double-quoted mapping keys are forbidden", + }, + { + name: "double_quoted_escaped_permission_key", + mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\n \"permi\\u0073sions\":\n contents: write\n") }, + expect: ".github/workflows/validate.yml: escape sequences in double-quoted mapping keys are forbidden", + }, ]; } @@ -93,3 +113,10 @@ function quotedCheckout(workflow, quote) { .replace("uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", `${quote}uses${quote}: actions/checkout@v4`) .replace(" with:\n persist-credentials: false", ` ${quote}with${quote}:\n ${quote}persist-credentials${quote}: true`); } + +function flowCheckout(workflow) { + return workflow.replace( + " - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n with:\n persist-credentials: false", + " - { uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5, with: { persist-credentials: false } }", + ); +} diff --git a/scripts/workflow-action-contract.mjs b/scripts/workflow-action-contract.mjs index a0e5084..686672f 100644 --- a/scripts/workflow-action-contract.mjs +++ b/scripts/workflow-action-contract.mjs @@ -17,6 +17,30 @@ function isMappingHeader(entry, key) { return entry?.key === key && (entry.value === "" || entry.value.startsWith("#")); } +function notationFailures(lines, relative) { + let blockScalarIndentation = null; + let escapedDoubleQuotedKey = false; + let flowSequenceMapping = false; + for (const line of lines) { + if (line.trim() === "" || line.trimStart().startsWith("#")) continue; + const lineIndentation = indentation(line); + if (blockScalarIndentation !== null && lineIndentation > blockScalarIndentation) continue; + blockScalarIndentation = null; + const trimmed = line.trim(); + const quotedKey = trimmed.match(/^(?:-\s+)?"((?:[^"\\]|\\.)*)"\s*:/)?.[1]; + if (quotedKey?.includes("\\")) escapedDoubleQuotedKey = true; + if (/^-\s*\{/.test(trimmed)) flowSequenceMapping = true; + const entry = mappingEntry(line); + if (entry && /^[|>](?:[+-]?[1-9]|[1-9]?[+-])?(?:\s+#.*)?$/.test(entry.value)) { + blockScalarIndentation = lineIndentation; + } + } + const failures = []; + if (flowSequenceMapping) failures.push(`${relative}: flow-style sequence mappings are forbidden`); + if (escapedDoubleQuotedKey) failures.push(`${relative}: escape sequences in double-quoted mapping keys are forbidden`); + return failures; +} + function directMappingLines(lines, headerIndex, headerIndentation) { const nested = []; for (const line of lines.slice(headerIndex + 1)) { @@ -89,6 +113,7 @@ function permissionFailures(lines, relative) { export function workflowActionFailures(workflow, relative) { const lines = workflow.split("\n"); return [ + ...notationFailures(lines, relative), ...actionRefFailures(lines, relative), ...checkoutCredentialFailures(workflow, relative), ...permissionFailures(lines, relative), From e8394617fcfa1e760c06940694b4fd5e5e9c9a1d Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 09:59:10 +0900 Subject: [PATCH 25/44] Extract domain document boundary checks --- scripts/domain-document-boundaries.mjs | 40 ++++++++++++++++++++++++++ scripts/validate-domains.mjs | 34 ++-------------------- 2 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 scripts/domain-document-boundaries.mjs diff --git a/scripts/domain-document-boundaries.mjs b/scripts/domain-document-boundaries.mjs new file mode 100644 index 0000000..ccf1f3a --- /dev/null +++ b/scripts/domain-document-boundaries.mjs @@ -0,0 +1,40 @@ +import fs from "node:fs"; +import path from "node:path"; +import { isOmoDependency, markdownLinkDestinations } from "./markdown-structure.mjs"; + +const generatedReference = /^design-engineering\/reference-profiles\/governed-local\/(?:editorial|terminal)\/generated\/(?:state-matrix|keyboard-matrix|evidence-coverage)\.md$/; +const ignoredDirectories = new Set([".git", ".omo", "node_modules"]); + +function walkMarkdown(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + if (ignoredDirectories.has(entry.name)) return []; + const target = path.join(dir, entry.name); + if (entry.isDirectory()) return walkMarkdown(target); + return entry.isFile() && entry.name.endsWith(".md") ? [target] : []; + }); +} + +function undeclaredDomainDocumentFailures(root, domains) { + return domains.flatMap((domain) => { + const declared = new Set([`${domain.slug}/index.md`, ...domain.leaves.map(([leaf]) => leaf), ...(domain.referenceDocuments ?? [])]); + return walkMarkdown(path.join(root, domain.slug)).flatMap((absolute) => { + const relative = path.relative(root, absolute); + if (generatedReference.test(relative) || declared.has(relative)) return []; + return [`${relative}: undeclared governed domain document`]; + }); + }); +} + +function omoDependencyFailures(root) { + return walkMarkdown(root).flatMap((absolute) => { + const relative = path.relative(root, absolute); + const content = fs.readFileSync(absolute, "utf8"); + if (markdownLinkDestinations(content).some(isOmoDependency)) return [`${relative}: tracked document must not depend on .omo`]; + return []; + }); +} + +export function collectDomainBoundaryFailures(root, domains) { + return [...undeclaredDomainDocumentFailures(root, domains), ...omoDependencyFailures(root)]; +} diff --git a/scripts/validate-domains.mjs b/scripts/validate-domains.mjs index 072e86c..b530e43 100644 --- a/scripts/validate-domains.mjs +++ b/scripts/validate-domains.mjs @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; +import { collectDomainBoundaryFailures } from "./domain-document-boundaries.mjs"; import { isOmoDependency, markdownLinkDestinations, stripFencedCodeBlocks } from "./markdown-structure.mjs"; const args = new Set(process.argv.slice(2)); @@ -183,36 +184,6 @@ function checkLeaf(domain, relative, expectedSourcePath, titles) { } } -function rejectUndeclaredDomainDocuments() { - const generatedReference = /^design-engineering\/reference-profiles\/governed-local\/(?:editorial|terminal)\/generated\/(?:state-matrix|keyboard-matrix|evidence-coverage)\.md$/; - for (const domain of domains) { - const declared = new Set([`${domain.slug}/index.md`, ...domain.leaves.map(([leaf]) => leaf), ...(domain.referenceDocuments ?? [])]); - for (const absolute of walkMarkdown(path.join(root, domain.slug))) { - const relative = path.relative(root, absolute); - if (generatedReference.test(relative)) continue; - if (!declared.has(relative)) failures.push(`${relative}: undeclared governed domain document`); - } - } -} - -function walkMarkdown(dir) { - if (!fs.existsSync(dir)) return []; - return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { - if ([".git", ".omo", "node_modules"].includes(entry.name)) return []; - const target = path.join(dir, entry.name); - if (entry.isDirectory()) return walkMarkdown(target); - return entry.isFile() && entry.name.endsWith(".md") ? [target] : []; - }); -} - -function rejectOmoDependencies() { - for (const absolute of walkMarkdown(root)) { - const relative = path.relative(root, absolute); - const content = fs.readFileSync(absolute, "utf8"); - if (markdownLinkDestinations(content).some(isOmoDependency)) failures.push(`${relative}: tracked document must not depend on .omo`); - } -} - function checkReferenceDocuments() { const index = stripFencedCodeBlocks(read(referenceDocuments[0])); if (!/Domain classification:\s*design-engineering\./i.test(index)) { @@ -255,8 +226,7 @@ for (const domain of domains) { checkLeaf(domain, relative, sourcePath, titles); } } -rejectUndeclaredDomainDocuments(); -rejectOmoDependencies(); +failures.push(...collectDomainBoundaryFailures(root, domains)); checkReferenceDocuments(); checkPromotionBoundary(); From 1ad5714b940ed839a5f0fb21728764f97bad30ec Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 10:21:56 +0900 Subject: [PATCH 26/44] Fail closed on unsupported workflow YAML --- .../governance-workflow-negative-cases.mjs | 90 ++++++++++++++++++- scripts/workflow-action-contract.mjs | 51 +++++++++-- 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/scripts/governance-workflow-negative-cases.mjs b/scripts/governance-workflow-negative-cases.mjs index 84f6c5b..0ecc89e 100644 --- a/scripts/governance-workflow-negative-cases.mjs +++ b/scripts/governance-workflow-negative-cases.mjs @@ -105,6 +105,80 @@ export function workflowSafetyCases(workflow) { mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\n \"permi\\u0073sions\":\n contents: write\n") }, expect: ".github/workflows/validate.yml: escape sequences in double-quoted mapping keys are forbidden", }, + { + name: "explicit_action_mapping_key", + mutate: { ".github/workflows/validate.yml": replaceSetupNode(workflow, " ? uses\n : attacker/action@main") }, + expect: ".github/workflows/validate.yml: explicit mapping keys are forbidden", + }, + { + name: "explicit_checkout_mapping_key", + mutate: { ".github/workflows/validate.yml": replaceCheckout(workflow, " - ? uses\n : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n with:\n persist-credentials: true") }, + expect: ".github/workflows/validate.yml: explicit mapping keys are forbidden", + }, + { + name: "explicit_permissions_mapping_key", + mutate: { ".github/workflows/validate.yml": workflow.replace("permissions:\n contents: read", "? permissions\n:\n contents: write") }, + expect: ".github/workflows/validate.yml: explicit mapping keys are forbidden", + }, + { + name: "explicit_duplicate_checkout_credentials_key", + mutate: { ".github/workflows/validate.yml": workflow.replace(" persist-credentials: false\n", " ? persist-credentials\n : false\n persist-credentials: true\n") }, + expect: ".github/workflows/validate.yml: explicit mapping keys are forbidden", + }, + { + name: "tagged_action_key", + mutate: { ".github/workflows/validate.yml": replaceCheckout(workflow, " - name: Checkout\n !!str uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5") }, + expect: ".github/workflows/validate.yml: YAML tags are forbidden", + }, + { + name: "anchored_action_key", + mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "&name uses: attacker/action@main") }, + expect: ".github/workflows/validate.yml: YAML anchors and aliases are forbidden", + }, + { + name: "aliased_action_value", + mutate: { ".github/workflows/validate.yml": workflow.replace("name: Validate StyleGallery", "x-action: &action attacker/action@main\nname: Validate StyleGallery").replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "uses: *action") }, + expect: ".github/workflows/validate.yml: YAML anchors and aliases are forbidden", + }, + { + name: "merged_checkout_step", + mutate: { ".github/workflows/validate.yml": workflow.replace("name: Validate StyleGallery", "x-checkout: &checkout\n uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n with:\n persist-credentials: false\nname: Validate StyleGallery").replace(" with:\n persist-credentials: false", " <<: *checkout\n with:\n persist-credentials: true") }, + expect: ".github/workflows/validate.yml: YAML merge keys are forbidden", + }, + { + name: "literal_block_scalar_action_ref", + mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "uses: |-\n attacker/action@main") }, + expect: ".github/workflows/validate.yml: block scalar action refs are forbidden", + }, + { + name: "folded_block_scalar_checkout_ref", + mutate: { ".github/workflows/validate.yml": replaceCheckout(workflow, " - name: Checkout\n uses: >-\n actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n with:\n persist-credentials: true") }, + expect: ".github/workflows/validate.yml: block scalar action refs are forbidden", + }, + { + name: "yaml_directive", + mutate: { ".github/workflows/validate.yml": `%YAML 1.2\n---\n${workflow}` }, + expect: ".github/workflows/validate.yml: YAML directives are forbidden", + }, + { + name: "yaml_tag_directive", + mutate: { ".github/workflows/validate.yml": `%TAG !e! tag:example.com,2026:\n---\n${workflow}` }, + expect: ".github/workflows/validate.yml: YAML directives are forbidden", + }, + { + name: "yaml_document_start_marker", + mutate: { ".github/workflows/validate.yml": `---\n${workflow}` }, + expect: ".github/workflows/validate.yml: YAML document markers are forbidden", + }, + { + name: "yaml_document_end_marker", + mutate: { ".github/workflows/validate.yml": `${workflow}\n...\n` }, + expect: ".github/workflows/validate.yml: YAML document markers are forbidden", + }, + { + name: "block_scalar_body_structural_notation_ignored", + mutate: { ".github/workflows/validate.yml": workflow.replace(" run: |\n node scripts/create-component-state-session.mjs", " run: |\n ? uses\n : attacker/action@main\n !!str uses: attacker/action@main\n &action uses: attacker/action@main\n <<: *action\n uses: |\n attacker/action@main\n ---\n node scripts/create-component-state-session.mjs") }, + }, ]; } @@ -115,8 +189,20 @@ function quotedCheckout(workflow, quote) { } function flowCheckout(workflow) { - return workflow.replace( - " - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n with:\n persist-credentials: false", + return replaceCheckout( + workflow, " - { uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5, with: { persist-credentials: false } }", ); } + +function replaceCheckout(workflow, replacement) { + const named = " - name: Checkout\n uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n with:\n persist-credentials: false"; + const unnamed = " - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n with:\n persist-credentials: false"; + return workflow.includes(named) ? workflow.replace(named, replacement) : workflow.replace(unnamed, replacement); +} + +function replaceSetupNode(workflow, replacement) { + const named = " - name: Setup Node\n uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4"; + const unnamed = " - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4"; + return workflow.includes(named) ? workflow.replace(named, ` - name: Setup Node\n${replacement}`) : workflow.replace(unnamed, ` - ${replacement.trimStart()}`); +} diff --git a/scripts/workflow-action-contract.mjs b/scripts/workflow-action-contract.mjs index 686672f..48a18c6 100644 --- a/scripts/workflow-action-contract.mjs +++ b/scripts/workflow-action-contract.mjs @@ -17,25 +17,60 @@ function isMappingHeader(entry, key) { return entry?.key === key && (entry.value === "" || entry.value.startsWith("#")); } -function notationFailures(lines, relative) { +function isBlockScalarHeader(value) { + return /^[|>](?:[+-]?[1-9]|[1-9]?[+-])?(?:\s+#.*)?$/.test(value); +} + +function structuralLines(lines) { let blockScalarIndentation = null; + return lines.map((line) => { + if (line.trim() === "" || line.trimStart().startsWith("#")) return line; + const lineIndentation = indentation(line); + if (blockScalarIndentation !== null && lineIndentation > blockScalarIndentation) return ""; + blockScalarIndentation = null; + const entry = mappingEntry(line); + if (entry && isBlockScalarHeader(entry.value)) blockScalarIndentation = lineIndentation; + return line; + }); +} + +function notationFailures(lines, relative) { + let actionBlockScalar = false; + let anchorsOrAliases = false; + let directives = false; + let documentMarkers = false; let escapedDoubleQuotedKey = false; + let explicitMappingKey = false; let flowSequenceMapping = false; + let mergeKey = false; + let tags = false; for (const line of lines) { if (line.trim() === "" || line.trimStart().startsWith("#")) continue; const lineIndentation = indentation(line); - if (blockScalarIndentation !== null && lineIndentation > blockScalarIndentation) continue; - blockScalarIndentation = null; const trimmed = line.trim(); + const structural = trimmed.replace(/^-\s+/, ""); + if (lineIndentation === 0 && /^%(?:YAML|TAG)\b/.test(trimmed)) directives = true; + if (lineIndentation === 0 && /^(?:---|\.\.\.)(?:\s+#.*)?$/.test(trimmed)) documentMarkers = true; + if (/^\?(?:\s|$)/.test(structural)) explicitMappingKey = true; + if (/^!\S*(?:\s|$)/.test(structural)) tags = true; + if (/^[&*][^\s:[\]{},]+(?:\s|:|$)/.test(structural)) anchorsOrAliases = true; + if (/^<<\s*:/.test(structural)) mergeKey = true; const quotedKey = trimmed.match(/^(?:-\s+)?"((?:[^"\\]|\\.)*)"\s*:/)?.[1]; if (quotedKey?.includes("\\")) escapedDoubleQuotedKey = true; if (/^-\s*\{/.test(trimmed)) flowSequenceMapping = true; const entry = mappingEntry(line); - if (entry && /^[|>](?:[+-]?[1-9]|[1-9]?[+-])?(?:\s+#.*)?$/.test(entry.value)) { - blockScalarIndentation = lineIndentation; - } + if (/^!\S*(?:\s|$)/.test(entry?.value ?? "")) tags = true; + if (/^[&*][^\s[\]{},]+(?:\s|$)/.test(entry?.value ?? "")) anchorsOrAliases = true; + if (entry?.key === "uses" && isBlockScalarHeader(entry.value)) actionBlockScalar = true; } const failures = []; + if (directives) failures.push(`${relative}: YAML directives are forbidden`); + if (documentMarkers) failures.push(`${relative}: YAML document markers are forbidden`); + if (explicitMappingKey) failures.push(`${relative}: explicit mapping keys are forbidden`); + if (tags) failures.push(`${relative}: YAML tags are forbidden`); + if (anchorsOrAliases) failures.push(`${relative}: YAML anchors and aliases are forbidden`); + if (mergeKey) failures.push(`${relative}: YAML merge keys are forbidden`); + if (actionBlockScalar) failures.push(`${relative}: block scalar action refs are forbidden`); if (flowSequenceMapping) failures.push(`${relative}: flow-style sequence mappings are forbidden`); if (escapedDoubleQuotedKey) failures.push(`${relative}: escape sequences in double-quoted mapping keys are forbidden`); return failures; @@ -69,7 +104,7 @@ function actionRefFailures(lines, relative) { export function checkoutCredentialFailures(workflow, relative) { const failures = []; - const lines = workflow.split("\n"); + const lines = structuralLines(workflow.split("\n")); for (const [index, line] of lines.entries()) { const usesEntry = mappingEntry(line); if (usesEntry?.key !== "uses" || !usesEntry.value.startsWith("actions/checkout@")) continue; @@ -111,7 +146,7 @@ function permissionFailures(lines, relative) { } export function workflowActionFailures(workflow, relative) { - const lines = workflow.split("\n"); + const lines = structuralLines(workflow.split("\n")); return [ ...notationFailures(lines, relative), ...actionRefFailures(lines, relative), From f9fb11648306982627655713972fab6ca169b443 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 10:21:56 +0900 Subject: [PATCH 27/44] Clarify governed profile source ownership --- GOVERNANCE.md | 2 +- scripts/governance-matrix-contract.mjs | 41 ++++++++++++++++++++++++++ scripts/governance-test-fixture.mjs | 3 +- scripts/test-validate-governance.mjs | 1 + scripts/validate-governance.mjs | 3 ++ 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 scripts/governance-matrix-contract.mjs diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 7a790f8..ee3d5dd 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -23,7 +23,7 @@ Use this file before editing repository documentation. It names which file is au | Quality gates and evidence | `quality/**/*.md` | Manual | None | `stable` | Claim-boundary changes, evidence-family changes, or broken quality links. | `scripts/validate-okf.mjs`, `scripts/validate-links.mjs`, `scripts/validate-ia.mjs` | Quality owner | | Consumer reference contract | `consumer-reference/contract.md`, `consumer-reference/schema/item.schema.json` | Manual | None | `stable` contract with related fixtures | Handoff shape, path boundary, lifecycle, ownership, or dependency-direction changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Repository governance owner with Validation owner | | Portable token source | `consumer-reference/fixtures/token-portability/valid-reference.json`, `consumer-reference/schema/portable-tokens.schema.json` | `scripts/build-reference-artifacts.mjs` with Style Dictionary `5.5.0` | `consumer-reference/generated/tokens.css`, `consumer-reference/generated/manifest.json` | `stable` restricted adapter contract, `generated` output | Allowed token shape, adapter/version pin, source token count, warning, declaration, or content hash changes. | `scripts/validate-reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | -| Governed local reference profiles | `design-engineering/reference-profiles/governed-local/**` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Layout revision, identity values, UA/reset assumptions, explicit selection, or fixture relationship changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Design Engineering owner with Validation owner | +| Governed local reference profiles | `design-engineering/reference-profiles/governed-local/editorial/profile.json`, `design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/editorial/local-foundations.json`, `design-engineering/reference-profiles/governed-local/terminal/profile.json`, `design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/terminal/local-foundations.json` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Layout revision, identity values, UA/reset assumptions, explicit selection, or fixture relationship changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Design Engineering owner with Validation owner | | Component-state evidence matrices | Each profile's declared `components/*.component.json`, `states/*.states.json`, `fixtures/*.fixture.json`, and `evidence/*.evidence.json` records | `scripts/generate-consumer-reference-evidence.mjs` | `design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md` | `generated` output from `experimental` canonical records | Declared record paths, capture-session identity, scenario/mode/channel counts, claim boundary, generated escaping, or generator output changes. | `scripts/validate-component-state.mjs`, `scripts/test-validate-component-state.mjs`, `scripts/test-validate-component-state-artifacts.mjs`, `scripts/test-generate-consumer-reference-evidence.mjs` | Design Engineering owner with Validation owner | | Shared promotion policy | `consumer-reference/policies/shared-experimental.json`, `consumer-reference/schema/promotion-rfc.schema.json` | Manual | None; promotion fixtures are synthetic examples | `stable` policy with `example_only`, deferred fixtures | Gateway scope, independence basis, evidence, ownership, support, compatibility, migration, deprecation, rollback, provenance, or decision changes. | `scripts/validate-promotion-rfc.mjs`, `scripts/test-validate-promotion-rfc.mjs` | Repository governance owner with actual consumer, support, migration, rollback, and Validation owners | | Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/{baseline-manifest,calibration-record}.schema.json`, `consumer-reference/baselines/*.json` | Playwright `1.61.0` in the digest-pinned `linux/amd64` image | `tests/snapshots/consumer-reference-card-grid.png`, raw GitHub Actions calibration artifact | `experimental`, nonblocking while owner approval is pending | Renderer source, exact image/platform/tool pin, baseline bytes, computed layout contract, or calibration metadata changes. | `scripts/validate-baseline-manifest.mjs`, `scripts/test-validate-baseline-manifest.mjs`, `scripts/test-summarize-sentinel-calibration.mjs`, Playwright | Repository governance owner with Validation owner | diff --git a/scripts/governance-matrix-contract.mjs b/scripts/governance-matrix-contract.mjs new file mode 100644 index 0000000..85ef839 --- /dev/null +++ b/scripts/governance-matrix-contract.mjs @@ -0,0 +1,41 @@ +const governedProfileSources = [ + "design-engineering/reference-profiles/governed-local/editorial/profile.json", + "design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json", + "design-engineering/reference-profiles/governed-local/editorial/local-foundations.json", + "design-engineering/reference-profiles/governed-local/terminal/profile.json", + "design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json", + "design-engineering/reference-profiles/governed-local/terminal/local-foundations.json", +]; + +const componentStateSources = ["components/*.component.json", "states/*.states.json", "fixtures/*.fixture.json", "evidence/*.evidence.json"]; +const componentStateArtifacts = ["editorial", "terminal"].flatMap((profile) => + ["state-matrix.md", "keyboard-matrix.md", "evidence-coverage.md"].map( + (artifact) => `design-engineering/reference-profiles/governed-local/${profile}/generated/${artifact}`, + ), +); + +function matrixRow(governance, family) { + const line = governance.split("\n").find((candidate) => candidate.startsWith(`| ${family} |`)); + return line ? line.split("|").slice(1, -1).map((cell) => cell.trim()) : null; +} + +function hasExactCodeValues(cell, expected) { + if (typeof cell !== "string") return false; + const actual = [...cell.matchAll(/`([^`]+)`/g)].map((match) => match[1]); + return actual.length === expected.length && new Set(actual).size === expected.length && expected.every((value) => actual.includes(value)); +} + +export function referenceOwnershipFailures(governance) { + const failures = []; + const profileRow = matrixRow(governance, "Governed local reference profiles"); + if (profileRow) { + if (!hasExactCodeValues(profileRow[1], governedProfileSources)) failures.push("GOVERNANCE.md: governed local reference profile sources must be the six explicit canonical profile files"); + if (profileRow[2] !== "Manual" || profileRow[3] !== "None") failures.push("GOVERNANCE.md: governed local reference profiles must be manual sources with no generated artifacts"); + } + const stateRow = matrixRow(governance, "Component-state evidence matrices"); + if (stateRow) { + if (!hasExactCodeValues(stateRow[1], componentStateSources)) failures.push("GOVERNANCE.md: component-state evidence sources must be the four declared record families"); + if (!hasExactCodeValues(stateRow[3], componentStateArtifacts)) failures.push("GOVERNANCE.md: component-state generated artifacts must be the six declared matrices"); + } + return failures; +} diff --git a/scripts/governance-test-fixture.mjs b/scripts/governance-test-fixture.mjs index 260b7fd..aad8a8f 100644 --- a/scripts/governance-test-fixture.mjs +++ b/scripts/governance-test-fixture.mjs @@ -175,7 +175,8 @@ export const files = { "| Layout recipes | `recipes/*.md` | Manual | None | `scripts/validate-okf.mjs` | Recipe owner |", "| Quality gates and evidence | `quality/**/*.md` | Manual | None | `scripts/validate-okf.mjs` | Quality owner |", "| Consumer reference contract | `consumer-reference/contract.md` | Manual | None | `stable` | Receiver changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", - "| Component-state evidence matrices | Declared component, state, fixture, and evidence records | `scripts/generate-consumer-reference-evidence.mjs` | Six profile matrices | `generated` | Source or generator changes. | `scripts/validate-component-state.mjs` | Validation owner |", + "| Governed local reference profiles | `design-engineering/reference-profiles/governed-local/editorial/profile.json`, `design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/editorial/local-foundations.json`, `design-engineering/reference-profiles/governed-local/terminal/profile.json`, `design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/terminal/local-foundations.json` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Profile source changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", + "| Component-state evidence matrices | Each profile's declared `components/*.component.json`, `states/*.states.json`, `fixtures/*.fixture.json`, and `evidence/*.evidence.json` records | `scripts/generate-consumer-reference-evidence.mjs` | `design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md` | `generated` | Source or generator changes. | `scripts/validate-component-state.mjs` | Validation owner |", "| Shared promotion policy | Promotion policy | Manual | None | stable | Policy changes. | promotion validator | Validation owner |", "| Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/calibration-record.schema.json`, `consumer-reference/baselines/*.json` | Playwright | Snapshot and raw calibration evidence | `experimental` | Renderer or baseline changes. | `scripts/validate-baseline-manifest.mjs` | Validation owner |", "| Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner |", diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index e0ac211..ccb11cf 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -15,6 +15,7 @@ const adapterPipeParser = 'let output = ""; process.stdin.setEncoding("utf8"); p const cases = [ { name: "missing_governance", omit: ["GOVERNANCE.md"], expect: "GOVERNANCE.md: missing file" }, + { name: "broad_profile_sources_claim_no_generated_artifacts", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace(/`design-engineering\/reference-profiles\/governed-local\/editorial\/profile\.json`[^|]+/, "`design-engineering/reference-profiles/governed-local/**`") }, expect: "GOVERNANCE.md: governed local reference profile sources must be the six explicit canonical profile files" }, { name: "missing_matrix_stale_trigger", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("Generated structure changes, generated-warning changes, or generated metadata changes.", "Generated structure changes.") }, expect: "GOVERNANCE.md: missing Generated structure changes, generated-warning changes, or generated metadata changes." }, { name: "missing_evidence_fixture_coverage", mutate: { "quality/evidence/executable-evidence.md": "Missing governance file or generated warning fixtures must fail." }, expect: "quality/evidence/executable-evidence.md: missing Missing governance file, generated warning, generated metadata, CODEOWNERS coverage, or stale policy fixtures must fail." }, { diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 2afaf1d..51b3279 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { componentStateWorkflowFailures } from "./component-state-workflow-contract.mjs"; +import { referenceOwnershipFailures } from "./governance-matrix-contract.mjs"; import { immutableActionPins, requiredCodeowners, sentinelProvenanceClauses } from "./governance-policy-contract.mjs"; import { promotionGovernanceFailures } from "./promotion-governance-contract.mjs"; import { workflowActionFailures } from "./workflow-action-contract.mjs"; @@ -50,6 +51,7 @@ function requireGovernanceMatrix() { "Layout recipes", "Quality gates and evidence", "Consumer reference contract", + "Governed local reference profiles", "Component-state evidence matrices", "Shared promotion policy", "Proposed Chromium sentinel", @@ -202,6 +204,7 @@ function requireSentinelProvenanceBoundary() { } requireGovernanceMatrix(); +failures.push(...referenceOwnershipFailures(read("GOVERNANCE.md"))); requireLifecycleStates(); requireOwnership(); requireStalenessDecision(); From 3565b28f940b03e6491a773cac16edee7c02bcd0 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 10:21:56 +0900 Subject: [PATCH 28/44] Reject ambiguous consumer CLI arguments --- .../test-validate-consumer-reference-cli.mjs | 69 +++++++++++++++++++ scripts/test-validate-consumer-reference.mjs | 7 ++ scripts/validate-consumer-reference.mjs | 2 +- 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 scripts/test-validate-consumer-reference-cli.mjs diff --git a/scripts/test-validate-consumer-reference-cli.mjs b/scripts/test-validate-consumer-reference-cli.mjs new file mode 100644 index 0000000..0d45351 --- /dev/null +++ b/scripts/test-validate-consumer-reference-cli.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const validator = path.join(repositoryRoot, "scripts", "validate-consumer-reference.mjs"); +const validItem = "consumer-reference/fixtures/valid-experimental.json"; +const errorCases = [ + { args: ["--item", "--json"], codes: ["argument_value_required"], name: "item_missing_value_before_json" }, + { args: ["--item", "--unknown", "--json"], codes: ["argument_value_required", "argument_unknown"], name: "item_missing_value_before_unknown" }, + { args: ["--profile", "--json"], codes: ["argument_value_required"], name: "profile_missing_value_before_json" }, + { args: ["--profile", "--unknown", "--json"], codes: ["argument_value_required", "argument_unknown"], name: "profile_missing_value_before_unknown" }, + { args: ["--json", "--item"], codes: ["argument_value_required"], name: "json_before_item_missing_value" }, + { args: ["--json", "--profile"], codes: ["argument_value_required"], name: "json_before_profile_missing_value" }, +]; + +function run(args) { + return spawnSync(process.execPath, [validator, ...args], { + cwd: repositoryRoot, + encoding: "utf8", + }); +} + +function runErrorCase(testCase) { + const child = run(testCase.args); + let report; + try { + report = JSON.parse(child.stdout); + } catch (error) { + return { + actual: { stderr: child.stderr.trim(), status: child.status, stdout: child.stdout }, + expected: `${testCase.codes.join(",")} in parseable JSON with nonzero exit`, + name: testCase.name, + ok: false, + reason: error instanceof Error ? error.message : String(error), + }; + } + const codes = Array.isArray(report.failures) ? report.failures.map((failure) => failure.code) : []; + return { + actual: { codes, ok: report.ok, status: child.status, stderr: child.stderr.trim() }, + expected: `${testCase.codes.join(",")} in parseable JSON with nonzero exit`, + name: testCase.name, + ok: child.status !== 0 + && child.stderr.length === 0 + && report.ok === false + && codes.length === testCase.codes.length + && testCase.codes.every((code) => codes.includes(code)), + }; +} + +const results = errorCases.map(runErrorCase); +const jsonLast = run(["--item", validItem, "--json"]); +const jsonFirst = run(["--json", "--item", validItem]); +results.push({ + actual: { firstStatus: jsonFirst.status, lastStatus: jsonLast.status, stderr: `${jsonFirst.stderr}${jsonLast.stderr}` }, + expected: "valid arguments produce byte-identical JSON regardless of --json position", + name: "valid_json_option_order", + ok: jsonFirst.status === 0 + && jsonLast.status === 0 + && jsonFirst.stderr.length === 0 + && jsonLast.stderr.length === 0 + && jsonFirst.stdout === jsonLast.stdout, +}); + +const report = { ok: results.every((result) => result.ok), results }; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/test-validate-consumer-reference.mjs b/scripts/test-validate-consumer-reference.mjs index 5daa310..5391377 100644 --- a/scripts/test-validate-consumer-reference.mjs +++ b/scripts/test-validate-consumer-reference.mjs @@ -176,7 +176,14 @@ const profileHarness = spawnSync(process.execPath, [path.join(repositoryRoot, "s }); const profileReport = JSON.parse(profileHarness.stdout); if (profileHarness.status !== 0 || profileReport.ok !== true) failures.push("missing_semantic:governed_local_reference_profiles"); +const cliHarness = spawnSync(process.execPath, [path.join(repositoryRoot, "scripts", "test-validate-consumer-reference-cli.mjs")], { + cwd: repositoryRoot, + encoding: "utf8", +}); +const cliReport = JSON.parse(cliHarness.stdout); +if (cliHarness.status !== 0 || cliReport.ok !== true) failures.push("missing_semantic:consumer_reference_cli_arguments"); const report = { + cliReport, failures, ok: failures.length === 0, profileReport, diff --git a/scripts/validate-consumer-reference.mjs b/scripts/validate-consumer-reference.mjs index 1c4e646..d64cf1b 100644 --- a/scripts/validate-consumer-reference.mjs +++ b/scripts/validate-consumer-reference.mjs @@ -57,7 +57,7 @@ function parseArguments() { } if (argument === "--item" || argument === "--profile") { const value = process.argv[index + 1]; - if (!value) { + if (!value || value.startsWith("--")) { addFailure("argument_value_required", "", `${argument} requires a repository-relative JSON path`); } else { items.push(value); From da083d42cdefff75d757c2e96d5d6f928f600542 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 10:47:06 +0900 Subject: [PATCH 29/44] Close remaining workflow action bypasses --- .../governance-workflow-negative-cases.mjs | 35 ++++++++++++++++++- scripts/workflow-action-contract.mjs | 11 ++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/scripts/governance-workflow-negative-cases.mjs b/scripts/governance-workflow-negative-cases.mjs index 0ecc89e..668c5d3 100644 --- a/scripts/governance-workflow-negative-cases.mjs +++ b/scripts/governance-workflow-negative-cases.mjs @@ -10,11 +10,34 @@ export function workflowSafetyCases(workflow) { mutate: { ".github/workflows/validate.yml": workflow.replace(" persist-credentials: false\n", "") }, expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", }, + { + name: "mixed_case_checkout_credentials_missing", + mutate: { ".github/workflows/validate.yml": workflow.replace(" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", " - uses: Actions/Checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "mixed_case_checkout_credentials_true", + mutate: { ".github/workflows/validate.yml": workflow.replace(" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", " - uses: Actions/Checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n with:\n persist-credentials: true\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4") }, + expect: ".github/workflows/validate.yml: every actions/checkout step must set persist-credentials: false", + }, + { + name: "mixed_case_checkout_credentials_false", + mutate: { ".github/workflows/validate.yml": workflow.replace(" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", " - uses: Actions/Checkout@34e114876b0b11c390a56381ad16ebd13914f8d5\n with:\n persist-credentials: false\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4") }, + }, { name: "third_party_floating_action_ref", mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "uses: attacker/action@main") }, expect: ".github/workflows/validate.yml: floating or unlabeled action ref uses: attacker/action@main", }, + { + name: "docker_action_ref", + mutate: { ".github/workflows/validate.yml": workflow.replace(" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", " - uses: docker://alpine:latest\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4") }, + expect: ".github/workflows/validate.yml: docker action refs are forbidden uses: docker://alpine:latest", + }, + { + name: "repository_local_action_ref", + mutate: { ".github/workflows/validate.yml": workflow.replace(" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", " - uses: ./.github/actions/local\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4") }, + }, { name: "checkout_true_with_later_env_false", mutate: { ".github/workflows/validate.yml": workflow.replace(" persist-credentials: false\n", " persist-credentials: true\n env:\n persist-credentials: false\n") }, @@ -95,6 +118,16 @@ export function workflowSafetyCases(workflow) { mutate: { ".github/workflows/validate.yml": flowCheckout(workflow) }, expect: ".github/workflows/validate.yml: flow-style sequence mappings are forbidden", }, + { + name: "flow_collection_mapping_value", + mutate: { ".github/workflows/validate.yml": workflow.replace(" steps:\n", " steps: [{ uses: attacker/action@main }]\n") }, + expect: ".github/workflows/validate.yml: flow-style collection values are forbidden", + }, + { + name: "flow_collection_continuation_value", + mutate: { ".github/workflows/validate.yml": workflow.replace(" steps:\n", " steps:\n [{ uses: attacker/action@main }]\n") }, + expect: ".github/workflows/validate.yml: flow-style collection values are forbidden", + }, { name: "double_quoted_escaped_action_key", mutate: { ".github/workflows/validate.yml": workflow.replace("uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", "\"u\\u0073es\": attacker/action@main") }, @@ -177,7 +210,7 @@ export function workflowSafetyCases(workflow) { }, { name: "block_scalar_body_structural_notation_ignored", - mutate: { ".github/workflows/validate.yml": workflow.replace(" run: |\n node scripts/create-component-state-session.mjs", " run: |\n ? uses\n : attacker/action@main\n !!str uses: attacker/action@main\n &action uses: attacker/action@main\n <<: *action\n uses: |\n attacker/action@main\n ---\n node scripts/create-component-state-session.mjs") }, + mutate: { ".github/workflows/validate.yml": workflow.replace(" run: |\n node scripts/create-component-state-session.mjs", " run: |\n ? uses\n : attacker/action@main\n !!str uses: attacker/action@main\n &action uses: attacker/action@main\n <<: *action\n uses: |\n attacker/action@main\n steps: [{ uses: attacker/action@main }]\n ---\n node scripts/create-component-state-session.mjs") }, }, ]; } diff --git a/scripts/workflow-action-contract.mjs b/scripts/workflow-action-contract.mjs index 48a18c6..5ae011a 100644 --- a/scripts/workflow-action-contract.mjs +++ b/scripts/workflow-action-contract.mjs @@ -41,6 +41,7 @@ function notationFailures(lines, relative) { let documentMarkers = false; let escapedDoubleQuotedKey = false; let explicitMappingKey = false; + let flowCollectionValue = false; let flowSequenceMapping = false; let mergeKey = false; let tags = false; @@ -59,6 +60,7 @@ function notationFailures(lines, relative) { if (quotedKey?.includes("\\")) escapedDoubleQuotedKey = true; if (/^-\s*\{/.test(trimmed)) flowSequenceMapping = true; const entry = mappingEntry(line); + if (/^[\[{]/.test(entry?.value ?? "") || (!/^-\s*\{/.test(trimmed) && /^[\[{]/.test(structural))) flowCollectionValue = true; if (/^!\S*(?:\s|$)/.test(entry?.value ?? "")) tags = true; if (/^[&*][^\s[\]{},]+(?:\s|$)/.test(entry?.value ?? "")) anchorsOrAliases = true; if (entry?.key === "uses" && isBlockScalarHeader(entry.value)) actionBlockScalar = true; @@ -71,6 +73,7 @@ function notationFailures(lines, relative) { if (anchorsOrAliases) failures.push(`${relative}: YAML anchors and aliases are forbidden`); if (mergeKey) failures.push(`${relative}: YAML merge keys are forbidden`); if (actionBlockScalar) failures.push(`${relative}: block scalar action refs are forbidden`); + if (flowCollectionValue) failures.push(`${relative}: flow-style collection values are forbidden`); if (flowSequenceMapping) failures.push(`${relative}: flow-style sequence mappings are forbidden`); if (escapedDoubleQuotedKey) failures.push(`${relative}: escape sequences in double-quoted mapping keys are forbidden`); return failures; @@ -96,7 +99,11 @@ function actionRefFailures(lines, relative) { if (entry?.key !== "uses") continue; const reference = entry.value.match(/^([^\s#]+)/)?.[1]; if (!reference) continue; - if (reference.startsWith("./") || reference.startsWith("docker://")) continue; + if (reference.startsWith("./")) continue; + if (reference.startsWith("docker://")) { + failures.push(`${relative}: docker action refs are forbidden ${line.trim().replace(/^-\s*/, "")}`); + continue; + } if (!/^[^@\s]+@[a-f0-9]{40}$/i.test(reference)) failures.push(`${relative}: floating or unlabeled action ref ${line.trim().replace(/^-\s*/, "")}`); } return failures; @@ -107,7 +114,7 @@ export function checkoutCredentialFailures(workflow, relative) { const lines = structuralLines(workflow.split("\n")); for (const [index, line] of lines.entries()) { const usesEntry = mappingEntry(line); - if (usesEntry?.key !== "uses" || !usesEntry.value.startsWith("actions/checkout@")) continue; + if (usesEntry?.key !== "uses" || !/^actions\/checkout@/i.test(usesEntry.value)) continue; const usesIndentation = mappingKeyIndentation(line); const withHeaders = []; for (const [offset, following] of lines.slice(index + 1).entries()) { From 37280144038fc45e099594e5bb6709b669b5e2ec Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 10:47:06 +0900 Subject: [PATCH 30/44] Require unique governance ownership rows --- scripts/governance-matrix-contract.mjs | 58 +++++++++++--- scripts/governance-matrix-negative-cases.mjs | 83 ++++++++++++++++++++ scripts/test-validate-governance.mjs | 2 + 3 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 scripts/governance-matrix-negative-cases.mjs diff --git a/scripts/governance-matrix-contract.mjs b/scripts/governance-matrix-contract.mjs index 85ef839..ad1ee9c 100644 --- a/scripts/governance-matrix-contract.mjs +++ b/scripts/governance-matrix-contract.mjs @@ -14,28 +14,60 @@ const componentStateArtifacts = ["editorial", "terminal"].flatMap((profile) => ), ); -function matrixRow(governance, family) { - const line = governance.split("\n").find((candidate) => candidate.startsWith(`| ${family} |`)); - return line ? line.split("|").slice(1, -1).map((cell) => cell.trim()) : null; +function matrixRows(governance, family) { + return governance + .split("\n") + .filter((candidate) => candidate.startsWith(`| ${family} |`)) + .map((line) => line.split("|").slice(1, -1).map((cell) => cell.trim())); } -function hasExactCodeValues(cell, expected) { - if (typeof cell !== "string") return false; - const actual = [...cell.matchAll(/`([^`]+)`/g)].map((match) => match[1]); +function codeListValues(cell, finalAnd) { + if (typeof cell !== "string" || cell.length === 0) return null; + const parts = cell.split(", "); + if (finalAnd) { + const last = parts.at(-1); + if (parts.length < 2 || !last?.startsWith("and ")) return null; + parts[parts.length - 1] = last.slice(4); + } else if (parts.some((part) => part.startsWith("and "))) { + return null; + } + const actual = []; + for (const part of parts) { + const match = part.match(/^`([^`]+)`$/); + if (!match) return null; + actual.push(match[1]); + } + return actual; +} + +function hasExpectedValues(actual, expected) { + if (!actual) return false; return actual.length === expected.length && new Set(actual).size === expected.length && expected.every((value) => actual.includes(value)); } export function referenceOwnershipFailures(governance) { const failures = []; - const profileRow = matrixRow(governance, "Governed local reference profiles"); - if (profileRow) { - if (!hasExactCodeValues(profileRow[1], governedProfileSources)) failures.push("GOVERNANCE.md: governed local reference profile sources must be the six explicit canonical profile files"); + const profileRows = matrixRows(governance, "Governed local reference profiles"); + if (profileRows.length !== 1) { + failures.push("GOVERNANCE.md: Governed local reference profiles row must appear exactly once"); + } else { + const profileRow = profileRows[0]; + if (!hasExpectedValues(codeListValues(profileRow[1], false), governedProfileSources)) failures.push("GOVERNANCE.md: governed local reference profile sources must be the six explicit canonical profile files"); if (profileRow[2] !== "Manual" || profileRow[3] !== "None") failures.push("GOVERNANCE.md: governed local reference profiles must be manual sources with no generated artifacts"); } - const stateRow = matrixRow(governance, "Component-state evidence matrices"); - if (stateRow) { - if (!hasExactCodeValues(stateRow[1], componentStateSources)) failures.push("GOVERNANCE.md: component-state evidence sources must be the four declared record families"); - if (!hasExactCodeValues(stateRow[3], componentStateArtifacts)) failures.push("GOVERNANCE.md: component-state generated artifacts must be the six declared matrices"); + const stateRows = matrixRows(governance, "Component-state evidence matrices"); + if (stateRows.length !== 1) { + failures.push("GOVERNANCE.md: Component-state evidence matrices row must appear exactly once"); + } else { + const stateRow = stateRows[0]; + const sourceCell = stateRow[1]; + const sourcePrefix = "Each profile's declared "; + const sourceSuffix = " records"; + const sourceValues = sourceCell?.startsWith(sourcePrefix) && sourceCell.endsWith(sourceSuffix) + ? codeListValues(sourceCell.slice(sourcePrefix.length, -sourceSuffix.length), true) + : null; + if (!hasExpectedValues(sourceValues, componentStateSources)) failures.push("GOVERNANCE.md: component-state evidence sources must be the four declared record families"); + if (!hasExpectedValues(codeListValues(stateRow[3], false), componentStateArtifacts)) failures.push("GOVERNANCE.md: component-state generated artifacts must be the six declared matrices"); } return failures; } diff --git a/scripts/governance-matrix-negative-cases.mjs b/scripts/governance-matrix-negative-cases.mjs new file mode 100644 index 0000000..b21e3ca --- /dev/null +++ b/scripts/governance-matrix-negative-cases.mjs @@ -0,0 +1,83 @@ +function matrixRow(governance, family) { + return governance.split("\n").find((line) => line.startsWith(`| ${family} |`)); +} + +function duplicateRow(governance, row, replacement = row) { + return governance.replace(row, `${row}\n${replacement}`); +} + +function mutateCell(row, index, mutate) { + const cells = row.split("|"); + cells[index] = ` ${mutate(cells[index].trim())} `; + return cells.join("|"); +} + +function reverseCodeValues(cell) { + const values = [...cell.matchAll(/`([^`]+)`/g)].map((match) => match[1]).reverse(); + return values.map((value) => `\`${value}\``).join(", "); +} + +export function governanceMatrixCases(governance) { + const profileRow = matrixRow(governance, "Governed local reference profiles"); + const stateRow = matrixRow(governance, "Component-state evidence matrices"); + if (!profileRow || !stateRow) throw new Error("canonical governance fixture must contain both reference ownership rows"); + + const conflictingProfileRow = profileRow.replace( + "`design-engineering/reference-profiles/governed-local/editorial/profile.json`", + "`design-engineering/reference-profiles/governed-local/**`", + ); + const reorderedProfileRow = mutateCell(profileRow, 2, reverseCodeValues); + const reorderedStateSources = mutateCell(stateRow, 2, (cell) => { + const values = reverseCodeValues(cell); + return `Each profile's declared ${values.replace(/, ([^,]+)$/, ", and $1")} records`; + }); + const reorderedStateRow = mutateCell(reorderedStateSources, 4, reverseCodeValues); + + return [ + { + name: "duplicate_governed_profile_row", + mutate: { "GOVERNANCE.md": duplicateRow(governance, profileRow) }, + expect: "GOVERNANCE.md: Governed local reference profiles row must appear exactly once", + }, + { + name: "conflicting_duplicate_governed_profile_row", + mutate: { "GOVERNANCE.md": duplicateRow(governance, profileRow, conflictingProfileRow) }, + expect: "GOVERNANCE.md: Governed local reference profiles row must appear exactly once", + }, + { + name: "duplicate_component_state_row", + mutate: { "GOVERNANCE.md": duplicateRow(governance, stateRow) }, + expect: "GOVERNANCE.md: Component-state evidence matrices row must appear exactly once", + }, + { + name: "duplicate_both_reference_rows", + mutate: { "GOVERNANCE.md": duplicateRow(duplicateRow(governance, profileRow), stateRow) }, + expect: "GOVERNANCE.md: Governed local reference profiles row must appear exactly once", + }, + { + name: "profile_source_extra_prose", + mutate: { "GOVERNANCE.md": governance.replace(profileRow, mutateCell(profileRow, 2, (cell) => `${cell} plus every generated matrix and evidence receipt`)) }, + expect: "GOVERNANCE.md: governed local reference profile sources must be the six explicit canonical profile files", + }, + { + name: "profile_artifact_extra_prose", + mutate: { "GOVERNANCE.md": governance.replace(profileRow, mutateCell(profileRow, 4, (cell) => `${cell} plus every generated matrix`)) }, + expect: "GOVERNANCE.md: governed local reference profiles must be manual sources with no generated artifacts", + }, + { + name: "component_source_extra_prose", + mutate: { "GOVERNANCE.md": governance.replace(stateRow, mutateCell(stateRow, 2, (cell) => `${cell} plus every related record`)) }, + expect: "GOVERNANCE.md: component-state evidence sources must be the four declared record families", + }, + { + name: "component_artifact_extra_prose", + mutate: { "GOVERNANCE.md": governance.replace(stateRow, mutateCell(stateRow, 4, (cell) => `${cell} plus every generated matrix and evidence receipt`)) }, + expect: "GOVERNANCE.md: component-state generated artifacts must be the six declared matrices", + }, + { + name: "reordered_reference_matrix_cells", + mutate: { "GOVERNANCE.md": governance.replace(profileRow, reorderedProfileRow).replace(stateRow, reorderedStateRow) }, + expect: null, + }, + ]; +} diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index ccb11cf..09633e8 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -6,6 +6,7 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { files, generatedWarning, sentinelProvenanceClauses } from "./governance-test-fixture.mjs"; +import { governanceMatrixCases } from "./governance-matrix-negative-cases.mjs"; import { workflowSafetyCases } from "./governance-workflow-negative-cases.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -182,6 +183,7 @@ const cases = [ mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("\"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", "\"$GITHUB_REPOSITORY\" \"changeroa/StyleGallery\" \"$GITHUB_RUN_ID\"") }, expect: ".github/workflows/validate.yml: missing \"changeroa/StyleGallery\" \"$GITHUB_REPOSITORY\" \"$GITHUB_RUN_ID\"", }, + ...governanceMatrixCases(files["GOVERNANCE.md"]), ...workflowSafetyCases(files[".github/workflows/validate.yml"]), { name: "success_path", expect: null }, ]; From ccb5a44cc6b5455397c6b711bab6043f9c57d868 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 11:02:38 +0900 Subject: [PATCH 31/44] Normalize governance matrix row indentation --- scripts/governance-matrix-contract.mjs | 2 +- scripts/governance-matrix-negative-cases.mjs | 31 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/governance-matrix-contract.mjs b/scripts/governance-matrix-contract.mjs index ad1ee9c..7bec3cb 100644 --- a/scripts/governance-matrix-contract.mjs +++ b/scripts/governance-matrix-contract.mjs @@ -17,7 +17,7 @@ const componentStateArtifacts = ["editorial", "terminal"].flatMap((profile) => function matrixRows(governance, family) { return governance .split("\n") - .filter((candidate) => candidate.startsWith(`| ${family} |`)) + .filter((candidate) => /^ {0,3}\|/.test(candidate) && candidate.trimStart().startsWith(`| ${family} |`)) .map((line) => line.split("|").slice(1, -1).map((cell) => cell.trim())); } diff --git a/scripts/governance-matrix-negative-cases.mjs b/scripts/governance-matrix-negative-cases.mjs index b21e3ca..e1b09ae 100644 --- a/scripts/governance-matrix-negative-cases.mjs +++ b/scripts/governance-matrix-negative-cases.mjs @@ -44,16 +44,47 @@ export function governanceMatrixCases(governance) { mutate: { "GOVERNANCE.md": duplicateRow(governance, profileRow, conflictingProfileRow) }, expect: "GOVERNANCE.md: Governed local reference profiles row must appear exactly once", }, + { + name: "one_space_duplicate_governed_profile_row", + mutate: { "GOVERNANCE.md": duplicateRow(governance, profileRow, ` ${profileRow}`) }, + expect: "GOVERNANCE.md: Governed local reference profiles row must appear exactly once", + }, + { + name: "three_space_conflicting_governed_profile_row", + mutate: { "GOVERNANCE.md": duplicateRow(governance, profileRow, ` ${conflictingProfileRow}`) }, + expect: "GOVERNANCE.md: Governed local reference profiles row must appear exactly once", + }, { name: "duplicate_component_state_row", mutate: { "GOVERNANCE.md": duplicateRow(governance, stateRow) }, expect: "GOVERNANCE.md: Component-state evidence matrices row must appear exactly once", }, + { + name: "one_space_duplicate_component_state_row", + mutate: { "GOVERNANCE.md": duplicateRow(governance, stateRow, ` ${stateRow}`) }, + expect: "GOVERNANCE.md: Component-state evidence matrices row must appear exactly once", + }, + { + name: "three_space_conflicting_component_state_row", + mutate: { "GOVERNANCE.md": duplicateRow(governance, stateRow, ` ${mutateCell(stateRow, 2, () => "Each profile's declared `components/**` records")}`) }, + expect: "GOVERNANCE.md: Component-state evidence matrices row must appear exactly once", + }, { name: "duplicate_both_reference_rows", mutate: { "GOVERNANCE.md": duplicateRow(duplicateRow(governance, profileRow), stateRow) }, expect: "GOVERNANCE.md: Governed local reference profiles row must appear exactly once", }, + { + name: "four_space_reference_rows_are_code_blocks", + mutate: { + "GOVERNANCE.md": duplicateRow( + duplicateRow(governance, profileRow, ` ${conflictingProfileRow}`), + stateRow, + ` ${mutateCell(stateRow, 2, () => "Each profile's declared `components/**` records")}`, + ), + }, + expect: null, + }, { name: "profile_source_extra_prose", mutate: { "GOVERNANCE.md": governance.replace(profileRow, mutateCell(profileRow, 2, (cell) => `${cell} plus every generated matrix and evidence receipt`)) }, From 017b96e26be60bfffb410c1994ab2acfd09beaaa Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 15:33:07 +0900 Subject: [PATCH 32/44] Trust container-owned component evidence checkout --- scripts/capture-session-contract.mjs | 6 +++++- scripts/create-component-state-session.mjs | 3 ++- scripts/test-component-state-source-contract.mjs | 11 +++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/capture-session-contract.mjs b/scripts/capture-session-contract.mjs index 68dc982..90594c0 100644 --- a/scripts/capture-session-contract.mjs +++ b/scripts/capture-session-contract.mjs @@ -37,6 +37,10 @@ export function sha256(bytes) { return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`; } +export function repositoryGitArgs(repositoryRoot, ...args) { + return ["-c", `safe.directory=${repositoryRoot}`, ...args]; +} + function compare(left, right) { return left < right ? -1 : left > right ? 1 : 0; } @@ -104,7 +108,7 @@ export function dirtyRelevantSources(repositoryRoot, profileRoot) { .filter((file) => file.startsWith(repositoryPrefix)) .map((file) => path.relative(repositoryRoot, file)); if (tracked.length === 0) return []; - const output = execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all", "--", ...tracked], { cwd: repositoryRoot, encoding: "utf8" }); + const output = execFileSync("git", repositoryGitArgs(repositoryRoot, "status", "--porcelain=v1", "--untracked-files=all", "--", ...tracked), { cwd: repositoryRoot, encoding: "utf8" }); return output.split("\n").filter(Boolean).sort(compare); } diff --git a/scripts/create-component-state-session.mjs b/scripts/create-component-state-session.mjs index 921fcbe..b221c81 100644 --- a/scripts/create-component-state-session.mjs +++ b/scripts/create-component-state-session.mjs @@ -11,6 +11,7 @@ import { canonicalIntended, canonicalSourceManifest, dirtyRelevantSources, + repositoryGitArgs, sha256, } from "./capture-session-contract.mjs"; import { compileSchemas } from "./component-state-contract.mjs"; @@ -42,7 +43,7 @@ else if (path.basename(options.output) !== "capture-session.json") failures.push else if (fs.existsSync(options.output)) failures.push({ code: "capture_session_replay", message: "capture session receipt already exists and cannot be overwritten", path: options.output }); function git(...args) { - return execFileSync("git", args, { cwd: repositoryRoot, encoding: "utf8" }).trim(); + return execFileSync("git", repositoryGitArgs(repositoryRoot, ...args), { cwd: repositoryRoot, encoding: "utf8" }).trim(); } const executable = chromium.executablePath(); diff --git a/scripts/test-component-state-source-contract.mjs b/scripts/test-component-state-source-contract.mjs index ceffc6f..1702a0a 100644 --- a/scripts/test-component-state-source-contract.mjs +++ b/scripts/test-component-state-source-contract.mjs @@ -4,9 +4,11 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { canonicalSourceManifest, + repositoryGitArgs, sourceManifestMatches, } from "./capture-session-contract.mjs"; import { compileSchemas } from "./component-state-contract.mjs"; @@ -30,6 +32,15 @@ assert(!first.files.some((entry) => entry.path === "scripts/validate-component-s assert(first.files.some((entry) => entry.path.endsWith("/profile.json")), "profile declarations must be bound"); assert(first.files.some((entry) => entry.path.endsWith("/tokens.dtcg.json")), "profile token inputs must be bound"); assert.equal(sourceManifestMatches(first, repositoryRoot, profileRoot), true, "canonical source manifest must match itself"); +assert.equal( + execFileSync("git", repositoryGitArgs(repositoryRoot, "rev-parse", "HEAD"), { + cwd: repositoryRoot, + encoding: "utf8", + env: { ...process.env, GIT_TEST_ASSUME_DIFFERENT_OWNER: "1" }, + }).trim(), + execFileSync("git", ["rev-parse", "HEAD"], { cwd: repositoryRoot, encoding: "utf8" }).trim(), + "repository Git reads must trust only the exact checkout when container ownership differs", +); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-source-contract-")); try { From 17131d6bf4e065f18fca883d86c7f1de1c3989a0 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 15:34:22 +0900 Subject: [PATCH 33/44] Recapture container-safe component evidence --- .../editorial/evidence/button.evidence.json | 410 +++++++++--------- .../terminal/evidence/button.evidence.json | 410 +++++++++--------- 2 files changed, 410 insertions(+), 410 deletions(-) diff --git a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json index ce108b5..0ce4bdb 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json @@ -24,16 +24,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-visual", - "recorded_at": "2026-07-13T23:12:58.644Z", + "recorded_at": "2026-07-14T06:33:48.519Z", "result": { "observed": "Recorded visual evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -51,10 +51,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -178,9 +178,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -193,9 +193,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -223,9 +223,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -241,7 +241,7 @@ "path": "runtime/editorial-action-disabled-busy.dom.json", "byte_length": 8290, "media_type": "application/json", - "sha256": "sha256:7f810be204ac0e758db459768436102b6f33ce7fc08bde2864672df769193f18" + "sha256": "sha256:ab25974eaf732f35b45f1f8f6640ae43aabaf82f4a1caca5eecedec90b95347c" }, "channel": "dom", "environment": { @@ -256,16 +256,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-dom", - "recorded_at": "2026-07-13T23:12:58.644Z", + "recorded_at": "2026-07-14T06:33:48.519Z", "result": { "observed": "Recorded dom evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -283,10 +283,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -410,9 +410,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -425,9 +425,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -455,9 +455,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -473,7 +473,7 @@ "path": "runtime/editorial-action-disabled-busy.ax.json", "byte_length": 8209, "media_type": "application/json", - "sha256": "sha256:e1a8b35ccd1aeeaf67176ef1c54b88d8a52accc97179352c78279037b16a3d94" + "sha256": "sha256:d0869084b405cde4f0343f07d88b66c1331e78dcc7e0d83093d32040896cd736" }, "channel": "ax", "environment": { @@ -488,16 +488,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-ax", - "recorded_at": "2026-07-13T23:12:58.644Z", + "recorded_at": "2026-07-14T06:33:48.519Z", "result": { "observed": "Recorded ax evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -515,10 +515,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -642,9 +642,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -657,9 +657,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -687,9 +687,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -722,16 +722,16 @@ "viewport": "1024x768" }, "id": "action-focused-visual", - "recorded_at": "2026-07-13T23:12:58.781Z", + "recorded_at": "2026-07-14T06:33:48.646Z", "result": { "observed": "Recorded visual evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-focused", @@ -749,10 +749,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -876,9 +876,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -891,9 +891,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -921,9 +921,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -938,7 +938,7 @@ "path": "runtime/editorial-action-focused.dom.json", "byte_length": 8230, "media_type": "application/json", - "sha256": "sha256:d3e8aebe36e4f88f6936d0d5ea6852d9a92b1121a84c07d40f7b636b25975277" + "sha256": "sha256:0bda7500dae5d94f823831548374c2d7444eaf8bd687fcaa592caeeaf763cd2b" }, "channel": "dom", "environment": { @@ -953,16 +953,16 @@ "viewport": "1024x768" }, "id": "action-focused-dom", - "recorded_at": "2026-07-13T23:12:58.781Z", + "recorded_at": "2026-07-14T06:33:48.646Z", "result": { "observed": "Recorded dom evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-focused", @@ -980,10 +980,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1107,9 +1107,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1122,9 +1122,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1152,9 +1152,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1169,7 +1169,7 @@ "path": "runtime/editorial-action-focused.ax.json", "byte_length": 8150, "media_type": "application/json", - "sha256": "sha256:d49540933edd174dfa2f44b69cf3efdaf85f13daa1de0cfada8f3ba98c5ecc54" + "sha256": "sha256:64a6b20340c3812d06fcdff4f132df15227a3a893e4939eb61381ca12824defe" }, "channel": "ax", "environment": { @@ -1184,16 +1184,16 @@ "viewport": "1024x768" }, "id": "action-focused-ax", - "recorded_at": "2026-07-13T23:12:58.781Z", + "recorded_at": "2026-07-14T06:33:48.646Z", "result": { "observed": "Recorded ax evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-focused", @@ -1211,10 +1211,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1338,9 +1338,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1353,9 +1353,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1383,9 +1383,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1417,16 +1417,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-visual", - "recorded_at": "2026-07-13T23:12:58.929Z", + "recorded_at": "2026-07-14T06:33:48.756Z", "result": { "observed": "Recorded visual evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1444,10 +1444,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1571,9 +1571,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1586,9 +1586,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1616,9 +1616,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1635,7 +1635,7 @@ "path": "runtime/editorial-action-loading-busy.dom.json", "byte_length": 8276, "media_type": "application/json", - "sha256": "sha256:e9b7ffadb2a74f51f3db232b685494fb8fc3a09c2801fc2394a243ab79a2fcf1" + "sha256": "sha256:d61c0b69ff4751ae958e4f7bab971d8767cd5dab9e832b601cc0828da9f6aa72" }, "channel": "dom", "environment": { @@ -1650,16 +1650,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-dom", - "recorded_at": "2026-07-13T23:12:58.929Z", + "recorded_at": "2026-07-14T06:33:48.756Z", "result": { "observed": "Recorded dom evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1677,10 +1677,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1804,9 +1804,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1819,9 +1819,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1849,9 +1849,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1868,7 +1868,7 @@ "path": "runtime/editorial-action-loading-busy.ax.json", "byte_length": 8176, "media_type": "application/json", - "sha256": "sha256:0081593ccbaa6c2009fa28f2791dae7c3d70691f0aeabcc3e17b7a037d2fdd4a" + "sha256": "sha256:146911dc4f509ac7333465f252f4205c2a7eb0de4d0a4dee70b30fe71e49cd80" }, "channel": "ax", "environment": { @@ -1883,16 +1883,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-ax", - "recorded_at": "2026-07-13T23:12:58.929Z", + "recorded_at": "2026-07-14T06:33:48.756Z", "result": { "observed": "Recorded ax evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1910,10 +1910,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2037,9 +2037,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2052,9 +2052,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2082,9 +2082,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2118,16 +2118,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-visual", - "recorded_at": "2026-07-13T23:12:59.127Z", + "recorded_at": "2026-07-14T06:33:48.879Z", "result": { "observed": "Recorded visual evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2145,10 +2145,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2272,9 +2272,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2287,9 +2287,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2317,9 +2317,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2337,7 +2337,7 @@ "path": "runtime/editorial-disclosure-expanded-loading.dom.json", "byte_length": 8333, "media_type": "application/json", - "sha256": "sha256:e8ca7506b86a5ef0df66c2d8161961ec4c97d3ebf4ded3d0592455b778b72a0e" + "sha256": "sha256:649083cf7bd92df1baac83cd6e050a1e0e9ea8ea300a2edaa7ffb8dfb09f3cec" }, "channel": "dom", "environment": { @@ -2352,16 +2352,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-dom", - "recorded_at": "2026-07-13T23:12:59.127Z", + "recorded_at": "2026-07-14T06:33:48.879Z", "result": { "observed": "Recorded dom evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2379,10 +2379,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2506,9 +2506,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2521,9 +2521,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2551,9 +2551,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2571,7 +2571,7 @@ "path": "runtime/editorial-disclosure-expanded-loading.ax.json", "byte_length": 8214, "media_type": "application/json", - "sha256": "sha256:530a9451ea6111e4b4abca606e485ed1077dd586af828f77d51c340fde498d3c" + "sha256": "sha256:a335914fea6d7933d4a68f84951a53e2679be43faeda67ea3f26338c27fa1ba2" }, "channel": "ax", "environment": { @@ -2586,16 +2586,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-ax", - "recorded_at": "2026-07-13T23:12:59.127Z", + "recorded_at": "2026-07-14T06:33:48.879Z", "result": { "observed": "Recorded ax evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2613,10 +2613,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2740,9 +2740,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2755,9 +2755,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2785,9 +2785,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2822,16 +2822,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-visual", - "recorded_at": "2026-07-13T23:12:59.237Z", + "recorded_at": "2026-07-14T06:33:49.004Z", "result": { "observed": "Recorded visual evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -2849,10 +2849,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2976,9 +2976,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2991,9 +2991,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -3021,9 +3021,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -3039,7 +3039,7 @@ "path": "runtime/editorial-toggle-focused-pressed.dom.json", "byte_length": 8270, "media_type": "application/json", - "sha256": "sha256:4e35af3021a5f705bff515515c91b665ab0bce37ef65fc06c60e465a0b91fc56" + "sha256": "sha256:bbc4bc195fc3a003c8c5126637e102e7e573a8f3568cc7a725c49d506802c2e6" }, "channel": "dom", "environment": { @@ -3054,16 +3054,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-dom", - "recorded_at": "2026-07-13T23:12:59.237Z", + "recorded_at": "2026-07-14T06:33:49.004Z", "result": { "observed": "Recorded dom evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3081,10 +3081,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -3208,9 +3208,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -3223,9 +3223,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -3253,9 +3253,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -3271,7 +3271,7 @@ "path": "runtime/editorial-toggle-focused-pressed.ax.json", "byte_length": 8184, "media_type": "application/json", - "sha256": "sha256:fd45a7d6519c314e6e70481f26607237323551f250960c6be9e40b77be0466e0" + "sha256": "sha256:e0709f66f261a595827a43941a9b51a65c1789540c193c17a7dc0b92884ccdc1" }, "channel": "ax", "environment": { @@ -3286,16 +3286,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-ax", - "recorded_at": "2026-07-13T23:12:59.237Z", + "recorded_at": "2026-07-14T06:33:49.004Z", "result": { "observed": "Recorded ax evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3313,10 +3313,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -3440,9 +3440,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -3455,9 +3455,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -3485,9 +3485,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", diff --git a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json index ede8178..3565d8e 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json @@ -24,16 +24,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-visual", - "recorded_at": "2026-07-13T23:12:59.346Z", + "recorded_at": "2026-07-14T06:33:49.115Z", "result": { "observed": "Recorded visual evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -51,10 +51,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -178,9 +178,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -193,9 +193,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -223,9 +223,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -241,7 +241,7 @@ "path": "runtime/terminal-action-disabled-busy.dom.json", "byte_length": 8289, "media_type": "application/json", - "sha256": "sha256:43526b9a748c8c1d0f62aa6941fd4e570d75fe0392d7f8371cc35b69be8937b0" + "sha256": "sha256:e775814ee40672cf5a86d312d6409a89ad24250ed04bdb3687a0c5bc4186e907" }, "channel": "dom", "environment": { @@ -256,16 +256,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-dom", - "recorded_at": "2026-07-13T23:12:59.346Z", + "recorded_at": "2026-07-14T06:33:49.115Z", "result": { "observed": "Recorded dom evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -283,10 +283,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -410,9 +410,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -425,9 +425,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -455,9 +455,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -473,7 +473,7 @@ "path": "runtime/terminal-action-disabled-busy.ax.json", "byte_length": 8208, "media_type": "application/json", - "sha256": "sha256:f83050d61458d6731184e9ccd3463b8cc421fb889566c2f5c506026bbb918f8b" + "sha256": "sha256:07da1033e729a695631458754b9f53d8671b0e4b511c74372bc6543e4f352b50" }, "channel": "ax", "environment": { @@ -488,16 +488,16 @@ "viewport": "1024x768" }, "id": "action-disabled-busy-ax", - "recorded_at": "2026-07-13T23:12:59.346Z", + "recorded_at": "2026-07-14T06:33:49.115Z", "result": { "observed": "Recorded ax evidence for action-disabled-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-disabled-busy", @@ -515,10 +515,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -642,9 +642,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -657,9 +657,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -687,9 +687,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -722,16 +722,16 @@ "viewport": "1024x768" }, "id": "action-focused-visual", - "recorded_at": "2026-07-13T23:12:59.442Z", + "recorded_at": "2026-07-14T06:33:49.228Z", "result": { "observed": "Recorded visual evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-focused", @@ -749,10 +749,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -876,9 +876,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -891,9 +891,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -921,9 +921,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -938,7 +938,7 @@ "path": "runtime/terminal-action-focused.dom.json", "byte_length": 8229, "media_type": "application/json", - "sha256": "sha256:f120a28e85dab701b86245560e42697f3e46a34e7f947be171118a14516f28f0" + "sha256": "sha256:f71be6585f1ca78794f3baf6742bd25839e4ea9d6a2809b644aa2e2a811d30ae" }, "channel": "dom", "environment": { @@ -953,16 +953,16 @@ "viewport": "1024x768" }, "id": "action-focused-dom", - "recorded_at": "2026-07-13T23:12:59.442Z", + "recorded_at": "2026-07-14T06:33:49.228Z", "result": { "observed": "Recorded dom evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-focused", @@ -980,10 +980,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1107,9 +1107,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1122,9 +1122,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1152,9 +1152,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1169,7 +1169,7 @@ "path": "runtime/terminal-action-focused.ax.json", "byte_length": 8153, "media_type": "application/json", - "sha256": "sha256:b6cf8fec865510678be60f26c600641d43d63efb1e2a0ed1cb240b3c65936858" + "sha256": "sha256:0192632a1c95628b3515536a9e90606ee04e1d217031a8f1c5358a97cc32cc4f" }, "channel": "ax", "environment": { @@ -1184,16 +1184,16 @@ "viewport": "1024x768" }, "id": "action-focused-ax", - "recorded_at": "2026-07-13T23:12:59.442Z", + "recorded_at": "2026-07-14T06:33:49.228Z", "result": { "observed": "Recorded ax evidence for action-focused; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-focused", @@ -1211,10 +1211,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1338,9 +1338,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1353,9 +1353,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1383,9 +1383,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1417,16 +1417,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-visual", - "recorded_at": "2026-07-13T23:12:59.548Z", + "recorded_at": "2026-07-14T06:33:49.354Z", "result": { "observed": "Recorded visual evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1444,10 +1444,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1571,9 +1571,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1586,9 +1586,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1616,9 +1616,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1635,7 +1635,7 @@ "path": "runtime/terminal-action-loading-busy.dom.json", "byte_length": 8275, "media_type": "application/json", - "sha256": "sha256:71d97da3c895cb1b4ec9cda5339b9583fc08874df4ee5136da8696cef97f7d54" + "sha256": "sha256:ca53cb29c53b267fc92cc68475fe4c2ea2c3dcccd31c1c6e858dc8a17198e358" }, "channel": "dom", "environment": { @@ -1650,16 +1650,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-dom", - "recorded_at": "2026-07-13T23:12:59.548Z", + "recorded_at": "2026-07-14T06:33:49.354Z", "result": { "observed": "Recorded dom evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1677,10 +1677,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -1804,9 +1804,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -1819,9 +1819,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -1849,9 +1849,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -1868,7 +1868,7 @@ "path": "runtime/terminal-action-loading-busy.ax.json", "byte_length": 8180, "media_type": "application/json", - "sha256": "sha256:9a827efa4fdd43287af5cd28ea80386b7c6a22b98922474d194a6d780faf6302" + "sha256": "sha256:2ff34baf906c25aae5984939ac89a1b78464755dbd0bd8f2aa8313ec5eb6b357" }, "channel": "ax", "environment": { @@ -1883,16 +1883,16 @@ "viewport": "1024x768" }, "id": "action-loading-busy-ax", - "recorded_at": "2026-07-13T23:12:59.548Z", + "recorded_at": "2026-07-14T06:33:49.354Z", "result": { "observed": "Recorded ax evidence for action-loading-busy; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "action-loading-busy", @@ -1910,10 +1910,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2037,9 +2037,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2052,9 +2052,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2082,9 +2082,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2118,16 +2118,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-visual", - "recorded_at": "2026-07-13T23:12:59.723Z", + "recorded_at": "2026-07-14T06:33:49.484Z", "result": { "observed": "Recorded visual evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2145,10 +2145,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2272,9 +2272,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2287,9 +2287,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2317,9 +2317,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2337,7 +2337,7 @@ "path": "runtime/terminal-disclosure-expanded-loading.dom.json", "byte_length": 8332, "media_type": "application/json", - "sha256": "sha256:4b84d8de3b79ab76b7cef9929f8fceda0423784227047ac9a61c0691f87dbafc" + "sha256": "sha256:1ddd7f1de3ad2c34d9e8d718c7b3128593a99e4207dce33ffb89f2fcc8f7d5b7" }, "channel": "dom", "environment": { @@ -2352,16 +2352,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-dom", - "recorded_at": "2026-07-13T23:12:59.723Z", + "recorded_at": "2026-07-14T06:33:49.484Z", "result": { "observed": "Recorded dom evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2379,10 +2379,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2506,9 +2506,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2521,9 +2521,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2551,9 +2551,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2571,7 +2571,7 @@ "path": "runtime/terminal-disclosure-expanded-loading.ax.json", "byte_length": 8215, "media_type": "application/json", - "sha256": "sha256:135cdd6dd399394c0c9f7fa423744397fff38a9b6dbfb8507b71a6ce348cf240" + "sha256": "sha256:33371b0445ae51fc297e51103a8fd04060cc35dfd4c6abc08675463fee675c3e" }, "channel": "ax", "environment": { @@ -2586,16 +2586,16 @@ "viewport": "1024x768" }, "id": "disclosure-expanded-loading-ax", - "recorded_at": "2026-07-13T23:12:59.723Z", + "recorded_at": "2026-07-14T06:33:49.484Z", "result": { "observed": "Recorded ax evidence for disclosure-expanded-loading; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "disclosure-expanded-loading", @@ -2613,10 +2613,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2740,9 +2740,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2755,9 +2755,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -2785,9 +2785,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -2822,16 +2822,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-visual", - "recorded_at": "2026-07-13T23:12:59.872Z", + "recorded_at": "2026-07-14T06:33:49.592Z", "result": { "observed": "Recorded visual evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -2849,10 +2849,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -2976,9 +2976,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -2991,9 +2991,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -3021,9 +3021,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -3039,7 +3039,7 @@ "path": "runtime/terminal-toggle-focused-pressed.dom.json", "byte_length": 8269, "media_type": "application/json", - "sha256": "sha256:f957638c87f9093a99b49865af6e85ab8efef6a56376dbc6061187ffac217dcd" + "sha256": "sha256:5d16f2f75dddd358a1f9d9839045ae3bc73d9d9d029b4f76af8ae42d3469bab4" }, "channel": "dom", "environment": { @@ -3054,16 +3054,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-dom", - "recorded_at": "2026-07-13T23:12:59.872Z", + "recorded_at": "2026-07-14T06:33:49.592Z", "result": { "observed": "Recorded dom evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3081,10 +3081,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -3208,9 +3208,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -3223,9 +3223,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -3253,9 +3253,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", @@ -3271,7 +3271,7 @@ "path": "runtime/terminal-toggle-focused-pressed.ax.json", "byte_length": 8181, "media_type": "application/json", - "sha256": "sha256:0328282033d16b9b8dfa29e31c2702feb5aabb405b6ae9fb806401fa68032ee1" + "sha256": "sha256:596da3c726e836ad0f5a09f4c456981bb76627110b7f806f7cafad34f60ac55c" }, "channel": "ax", "environment": { @@ -3286,16 +3286,16 @@ "viewport": "1024x768" }, "id": "toggle-focused-pressed-ax", - "recorded_at": "2026-07-13T23:12:59.872Z", + "recorded_at": "2026-07-14T06:33:49.592Z", "result": { "observed": "Recorded ax evidence for toggle-focused-pressed; this is not certification.", "status": "passed" }, "run": { "attempt": 1, - "id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "id": "8e81d617-4087-4350-9100-6e03798d1ff8", "repository": "changeroa/StyleGallery", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", "source": "local" }, "scenario_id": "toggle-focused-pressed", @@ -3313,10 +3313,10 @@ "playwright": "1.61.0", "viewport": "1024x768" }, - "nonce": "d9a6c12a45294216433dc66fc8bde5b4b5fc3f8df8130c6534ac007ab254c3d4", - "receipt_sha256": "sha256:696690679fc673c15de4aefb8bf43154e950622f9020f908183fbce972ba401a", - "revision": "ad602d1b58a48857748a115e9d5746cef2990e49", - "session_id": "59e34d42-44cc-4058-bf24-8a57d39f13cc", + "nonce": "e9a2d4be58843c97ed9b5090c70f70e6db82510f84d2545ccb09e3cc3d6d46eb", + "receipt_sha256": "sha256:8d691f1a8c15f6d974e68a18281c206574207f34028e955bd3488890433c0cb2", + "revision": "017b96e26be60bfffb410c1994ab2acfd09beaaa", + "session_id": "8e81d617-4087-4350-9100-6e03798d1ff8", "source": { "files": [ { @@ -3440,9 +3440,9 @@ "sha256": "sha256:060a67cd2ae1412b950509bce72fbba8d0eabbbeba0fdf3d06edc1790ac01f6e" }, { - "byte_length": 7527, + "byte_length": 7686, "path": "scripts/capture-session-contract.mjs", - "sha256": "sha256:aa212fe478dc118eee787b4d52b70d2e803350fe807bdeba299cc3530203aed9" + "sha256": "sha256:59ab740e9ceb39e1665805dd789746d91977fe190a7868e08a6f8fd57f43863f" }, { "byte_length": 5181, @@ -3455,9 +3455,9 @@ "sha256": "sha256:7c51786051d8a1cdc0d09e77d2c06845dd8f21e36a68dbb8c09953c9eb768ae5" }, { - "byte_length": 5367, + "byte_length": 5426, "path": "scripts/create-component-state-session.mjs", - "sha256": "sha256:c38943e984282331d991f6fba61e5cb169bf2c7d5e632b4ca5b9cb1c171e719e" + "sha256": "sha256:1b56b74573af591b917dcf36ca147d6e4e8d4e1ebcb01a77ae2c6637ae311eae" }, { "byte_length": 6909, @@ -3485,9 +3485,9 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:2b2a16fd11b29aa7af1c4a55c1adac9adb00bc4a0dff2a750337b910a2674fc9" + "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" }, - "started_at": "2026-07-13T23:12:45.252Z" + "started_at": "2026-07-14T06:33:47.214Z" }, "scope": { "component": "button", From 11db754a88951b964e78372b0617bf6d77d7e210 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 15:42:58 +0900 Subject: [PATCH 34/44] Harden different-owner Git regression --- scripts/test-component-state-source-contract.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/test-component-state-source-contract.mjs b/scripts/test-component-state-source-contract.mjs index 1702a0a..ecbfc79 100644 --- a/scripts/test-component-state-source-contract.mjs +++ b/scripts/test-component-state-source-contract.mjs @@ -32,13 +32,15 @@ assert(!first.files.some((entry) => entry.path === "scripts/validate-component-s assert(first.files.some((entry) => entry.path.endsWith("/profile.json")), "profile declarations must be bound"); assert(first.files.some((entry) => entry.path.endsWith("/tokens.dtcg.json")), "profile token inputs must be bound"); assert.equal(sourceManifestMatches(first, repositoryRoot, profileRoot), true, "canonical source manifest must match itself"); +const revisionArgs = repositoryGitArgs(repositoryRoot, "rev-parse", "HEAD"); +assert.deepEqual(revisionArgs, ["-c", `safe.directory=${repositoryRoot}`, "rev-parse", "HEAD"], "repository Git trust must be scoped to the exact checkout"); assert.equal( - execFileSync("git", repositoryGitArgs(repositoryRoot, "rev-parse", "HEAD"), { + execFileSync("git", revisionArgs, { cwd: repositoryRoot, encoding: "utf8", env: { ...process.env, GIT_TEST_ASSUME_DIFFERENT_OWNER: "1" }, }).trim(), - execFileSync("git", ["rev-parse", "HEAD"], { cwd: repositoryRoot, encoding: "utf8" }).trim(), + execFileSync("git", revisionArgs, { cwd: repositoryRoot, encoding: "utf8" }).trim(), "repository Git reads must trust only the exact checkout when container ownership differs", ); From 346344d2b773e891d139006ba96e2897fab5e95c Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 15:50:10 +0900 Subject: [PATCH 35/44] Export component evidence container identity --- .github/workflows/validate.yml | 3 +-- scripts/component-state-workflow-contract.mjs | 1 + scripts/governance-test-fixture.mjs | 3 +-- scripts/test-validate-governance.mjs | 5 +++++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d3321fd..3530e5c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -234,6 +234,7 @@ jobs: continue-on-error: true env: STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state + SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a container: image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a options: --platform linux/amd64 @@ -256,8 +257,6 @@ jobs: - name: Capture and validate closed component-state evidence shell: bash - env: - SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a run: | set -o pipefail node scripts/create-component-state-session.mjs \ diff --git a/scripts/component-state-workflow-contract.mjs b/scripts/component-state-workflow-contract.mjs index d9dfc96..102ffdc 100644 --- a/scripts/component-state-workflow-contract.mjs +++ b/scripts/component-state-workflow-contract.mjs @@ -50,6 +50,7 @@ export function componentStateWorkflowFailures(workflow) { if (!hasAdjacentLines(componentJob, " container:", ` image: ${pinnedContainer}`)) failures.push("component-state container.image must equal pinned Playwright digest"); if (/runner\.temp|RUNNER_TEMP/.test(componentJob)) failures.push("component-state Playwright container job must not use runner temp paths"); if (!hasAdjacentLines(componentJob, " env:", " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state")) failures.push("missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state"); + if (!componentJob.includes(` env:\n STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state\n SENTINEL_CONTAINER_IMAGE: ${pinnedContainer}`)) failures.push("component-state job must export the pinned Playwright image identity"); const runtime = invocation(componentJob, `STATE_ARTIFACT_DIR="${sharedRoot}/runtime"`); if (!runtime.includes(`STATE_SESSION_RECEIPT="${sharedRoot}/capture-session.json"`)) failures.push("component-state runtime must bind receipt under shared root"); diff --git a/scripts/governance-test-fixture.mjs b/scripts/governance-test-fixture.mjs index aad8a8f..f710646 100644 --- a/scripts/governance-test-fixture.mjs +++ b/scripts/governance-test-fixture.mjs @@ -117,14 +117,13 @@ export const files = { " component-state-evidence:", " env:", " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", + " SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", " container:", " image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", " steps:", " - name: Run artifact harness", " run: node scripts/test-validate-component-state-artifacts.mjs", " - name: Capture evidence", - " env:", - " SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", " run: |", " node scripts/create-component-state-session.mjs \\", " --output \"$STATE_EVIDENCE_ROOT/capture-session.json\" \\", diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index 09633e8..5461fe6 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -146,6 +146,11 @@ const cases = [ mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replaceAll(".tmp/consumer-reference-state", "state-output") }, expect: ".github/workflows/validate.yml: missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", }, + { + name: "missing_component_state_image_identity", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a\n", "") }, + expect: ".github/workflows/validate.yml: component-state job must export the pinned Playwright image identity", + }, { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, { name: "missing_component_state_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles.", "Component evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles." }, { From d86a51396c186afe26a4b2fb4fdd51d0374af70c Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 14 Jul 2026 15:54:51 +0900 Subject: [PATCH 36/44] Extract component workflow negative cases --- ...omponent-state-workflow-negative-cases.mjs | 64 ++++++++++++++++++ scripts/test-validate-governance.mjs | 67 +------------------ 2 files changed, 66 insertions(+), 65 deletions(-) create mode 100644 scripts/component-state-workflow-negative-cases.mjs diff --git a/scripts/component-state-workflow-negative-cases.mjs b/scripts/component-state-workflow-negative-cases.mjs new file mode 100644 index 0000000..49c7de9 --- /dev/null +++ b/scripts/component-state-workflow-negative-cases.mjs @@ -0,0 +1,64 @@ +export function componentStateWorkflowCases(workflow) { + return [ + { + name: "browser_artifact_harness_in_static_job", + mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\nnode scripts/test-validate-component-state-artifacts.mjs\n") }, + expect: ".github/workflows/validate.yml: browser-dependent artifact/session harness must not run in validate job", + }, + { + name: "browser_artifact_harness_missing_container_job", + mutate: { ".github/workflows/validate.yml": workflow.replace(" run: node scripts/test-validate-component-state-artifacts.mjs\n", "") }, + expect: ".github/workflows/validate.yml: component-state artifact/session harness must run in Playwright container job", + }, + { + name: "duplicate_artifact_harness_in_chromium_sentinel", + mutate: { ".github/workflows/validate.yml": workflow.replace(" - run: node scripts/test-consumer-reference-sentinel.mjs\n", " - run: node scripts/test-consumer-reference-sentinel.mjs\n - run: node scripts/test-validate-component-state-artifacts.mjs\n") }, + expect: ".github/workflows/validate.yml: artifact/session harness must run exactly once and only in component-state-evidence job", + }, + { + name: "unpinned_component_container_with_pinned_env", + mutate: { ".github/workflows/validate.yml": workflow.replace(" image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", " image: mcr.microsoft.com/playwright:v1.61.0-noble") }, + expect: ".github/workflows/validate.yml: component-state container.image must equal pinned Playwright digest", + }, + { + name: "session_receipt_outside_shared_root", + mutate: { ".github/workflows/validate.yml": workflow.replace(" STATE_SESSION_RECEIPT=\"$STATE_EVIDENCE_ROOT/capture-session.json\" \\\n", " STATE_SESSION_RECEIPT=\"/tmp/capture-session.json\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state runtime must bind receipt under shared root", + }, + { + name: "finalizer_output_outside_shared_root", + mutate: { ".github/workflows/validate.yml": workflow.replace(" --output \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --output \"/tmp/runtime-manifest.json\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state finalizer must write manifest under shared root", + }, + { + name: "validator_manifest_outside_shared_root", + mutate: { ".github/workflows/validate.yml": workflow.replace(" --runtime-manifest \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --runtime-manifest \"/tmp/runtime-manifest.json\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state validator must read manifest under shared root", + }, + { + name: "finalizer_artifact_root_outside_shared_root", + mutate: { ".github/workflows/validate.yml": workflow.replace(" node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state finalizer must use shared artifact root", + }, + { + name: "validator_artifact_root_outside_shared_root", + mutate: { ".github/workflows/validate.yml": workflow.replace(" node scripts/validate-component-state.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/validate-component-state.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") }, + expect: ".github/workflows/validate.yml: component-state validator must use shared artifact root", + }, + { + name: "runner_temp_in_component_job", + mutate: { ".github/workflows/validate.yml": workflow.replace("STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", "STATE_EVIDENCE_ROOT: ${{ runner.temp }}/consumer-reference-state") }, + expect: ".github/workflows/validate.yml: component-state Playwright container job must not use runner temp paths", + }, + { + name: "component_state_workspace_root_drift", + mutate: { ".github/workflows/validate.yml": workflow.replaceAll(".tmp/consumer-reference-state", "state-output") }, + expect: ".github/workflows/validate.yml: missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", + }, + { + name: "missing_component_state_image_identity", + mutate: { ".github/workflows/validate.yml": workflow.replace(" SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a\n", "") }, + expect: ".github/workflows/validate.yml: component-state job must export the pinned Playwright image identity", + }, + ]; +} diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index 5461fe6..bfed51d 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { files, generatedWarning, sentinelProvenanceClauses } from "./governance-test-fixture.mjs"; import { governanceMatrixCases } from "./governance-matrix-negative-cases.mjs"; +import { componentStateWorkflowCases } from "./component-state-workflow-negative-cases.mjs"; import { workflowSafetyCases } from "./governance-workflow-negative-cases.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -86,71 +87,7 @@ const cases = [ { name: "missing_sentinel_ci_wiring", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replaceAll("node scripts/test-consumer-reference-sentinel.mjs", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-consumer-reference-sentinel.mjs" }, { name: "missing_component_source_contract_ci", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("node scripts/test-component-state-source-contract.mjs", "") }, expect: ".github/workflows/validate.yml: missing node scripts/test-component-state-source-contract.mjs" }, { name: "missing_visual_schema_ci", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("consumer-reference/schema/visual-evidence.schema.json", "") }, expect: ".github/workflows/validate.yml: missing consumer-reference/schema/visual-evidence.schema.json" }, - { - name: "browser_artifact_harness_in_static_job", - mutate: { - ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace( - " validate:\n", - " validate:\nnode scripts/test-validate-component-state-artifacts.mjs\n", - ), - }, - expect: ".github/workflows/validate.yml: browser-dependent artifact/session harness must not run in validate job", - }, - { - name: "browser_artifact_harness_missing_container_job", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" run: node scripts/test-validate-component-state-artifacts.mjs\n", "") }, - expect: ".github/workflows/validate.yml: component-state artifact/session harness must run in Playwright container job", - }, - { - name: "duplicate_artifact_harness_in_chromium_sentinel", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: node scripts/test-consumer-reference-sentinel.mjs\n", " - run: node scripts/test-consumer-reference-sentinel.mjs\n - run: node scripts/test-validate-component-state-artifacts.mjs\n") }, - expect: ".github/workflows/validate.yml: artifact/session harness must run exactly once and only in component-state-evidence job", - }, - { - name: "unpinned_component_container_with_pinned_env", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", " image: mcr.microsoft.com/playwright:v1.61.0-noble") }, - expect: ".github/workflows/validate.yml: component-state container.image must equal pinned Playwright digest", - }, - { - name: "session_receipt_outside_shared_root", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" STATE_SESSION_RECEIPT=\"$STATE_EVIDENCE_ROOT/capture-session.json\" \\\n", " STATE_SESSION_RECEIPT=\"/tmp/capture-session.json\" \\\n") }, - expect: ".github/workflows/validate.yml: component-state runtime must bind receipt under shared root", - }, - { - name: "finalizer_output_outside_shared_root", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" --output \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --output \"/tmp/runtime-manifest.json\" \\\n") }, - expect: ".github/workflows/validate.yml: component-state finalizer must write manifest under shared root", - }, - { - name: "validator_manifest_outside_shared_root", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" --runtime-manifest \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --runtime-manifest \"/tmp/runtime-manifest.json\" \\\n") }, - expect: ".github/workflows/validate.yml: component-state validator must read manifest under shared root", - }, - { - name: "finalizer_artifact_root_outside_shared_root", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") }, - expect: ".github/workflows/validate.yml: component-state finalizer must use shared artifact root", - }, - { - name: "validator_artifact_root_outside_shared_root", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" node scripts/validate-component-state.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/validate-component-state.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") }, - expect: ".github/workflows/validate.yml: component-state validator must use shared artifact root", - }, - { - name: "runner_temp_in_component_job", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace("STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", "STATE_EVIDENCE_ROOT: ${{ runner.temp }}/consumer-reference-state") }, - expect: ".github/workflows/validate.yml: component-state Playwright container job must not use runner temp paths", - }, - { - name: "component_state_workspace_root_drift", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replaceAll(".tmp/consumer-reference-state", "state-output") }, - expect: ".github/workflows/validate.yml: missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", - }, - { - name: "missing_component_state_image_identity", - mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a\n", "") }, - expect: ".github/workflows/validate.yml: component-state job must export the pinned Playwright image identity", - }, + ...componentStateWorkflowCases(files[".github/workflows/validate.yml"]), { name: "missing_sentinel_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence.", "Chromium evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing The proposed Chromium sentinel preserves canonical card-grid geometry and truth-derived calibration evidence." }, { name: "missing_component_state_evidence_coverage", mutate: { "quality/evidence/executable-evidence.md": files["quality/evidence/executable-evidence.md"].replace("Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles.", "Component evidence exists.") }, expect: "quality/evidence/executable-evidence.md: missing Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles." }, { From 1eff72d72bb819f08c1901590a16e6cae7bef9ee Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:34:04 +0900 Subject: [PATCH 37/44] feat(conformance): add migration readiness contract Plan-Task: 1 --- .../consumer-conformance/manifest.json | 38 +++ .../consumer-conformance/valid-migration.json | 137 ++++++++ .../consumer-conformance-record.schema.json | 322 ++++++++++++++++++ .../editorial/evidence/button.evidence.json | 210 ++++++------ .../terminal/evidence/button.evidence.json | 210 ++++++------ package-lock.json | 21 +- package.json | 3 +- scripts/consumer-conformance-contract.mjs | 130 +++++++ scripts/json-schema-formats.mjs | 31 ++ scripts/test-json-schema-formats.mjs | 40 +++ .../test-validate-consumer-conformance.mjs | 127 +++++++ scripts/validate-consumer-conformance.mjs | 118 +++++++ 12 files changed, 1175 insertions(+), 212 deletions(-) create mode 100644 consumer-reference/fixtures/consumer-conformance/manifest.json create mode 100644 consumer-reference/fixtures/consumer-conformance/valid-migration.json create mode 100644 consumer-reference/schema/consumer-conformance-record.schema.json create mode 100644 scripts/consumer-conformance-contract.mjs create mode 100644 scripts/json-schema-formats.mjs create mode 100644 scripts/test-json-schema-formats.mjs create mode 100644 scripts/test-validate-consumer-conformance.mjs create mode 100644 scripts/validate-consumer-conformance.mjs diff --git a/consumer-reference/fixtures/consumer-conformance/manifest.json b/consumer-reference/fixtures/consumer-conformance/manifest.json new file mode 100644 index 0000000..a8da42f --- /dev/null +++ b/consumer-reference/fixtures/consumer-conformance/manifest.json @@ -0,0 +1,38 @@ +{ + "id": "consumer-conformance-fixture-inventory", + "invalid_cases": [ + { + "expected_code": "migration_dimension_required", + "name": "omitted-dimension" + }, + { + "expected_code": "migration_dimension_scenario_required", + "name": "applicable-without-scenario" + }, + { + "expected_code": "runtime_evidence_method_invalid", + "name": "source-regex-runtime-proof" + }, + { + "expected_code": "adoption_scenario_unknown", + "name": "orphan-adoption-scenario" + }, + { + "expected_code": "adoption_consumer_target_invalid", + "name": "malformed-local-target" + }, + { + "expected_code": "adoption_stylegallery_revision_unpinned", + "name": "unpinned-stylegallery-revision" + }, + { + "expected_code": "adoption_debt_incomplete", + "name": "debt-without-lifecycle" + } + ], + "schema_record": "consumer-reference/schema/consumer-conformance-record.schema.json", + "schema_version": "1.0", + "valid_records": [ + "valid-migration.json" + ] +} diff --git a/consumer-reference/fixtures/consumer-conformance/valid-migration.json b/consumer-reference/fixtures/consumer-conformance/valid-migration.json new file mode 100644 index 0000000..6ed4d72 --- /dev/null +++ b/consumer-reference/fixtures/consumer-conformance/valid-migration.json @@ -0,0 +1,137 @@ +{ + "adoption_mappings": [ + { + "consumer_target": { + "identity": "GalleryShell", + "kind": "component" + }, + "debt": [], + "deviations": [], + "local_decision": { + "decision": "Keep the spacing value consumer-local and map only the layout unit role.", + "identity": "--space-shell-gap", + "kind": "token" + }, + "scenario_ids": [ + "migration-round-trip" + ], + "stylegallery": { + "anchor": "#implementation-handoff", + "path": "recipes/app-shell/index.md", + "revision": "2222222222222222222222222222222222222222" + } + } + ], + "claim_boundary": { + "certifies_accessibility": false, + "certifies_product_correctness": false, + "scope": "This record binds migration scenarios to consumer-owned runtime evidence only." + }, + "consumer": { + "relevant_sources": [ + "src/app.js", + "src/styles.css" + ], + "repository": "example/consumer-web", + "revision": "1111111111111111111111111111111111111111" + }, + "id": "example-consumer-migration", + "migration_dimensions": { + "action_parity": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "atomic_batch_behavior": { + "reason": "The migrated surface exposes no batch operation in this scope.", + "status": "not_applicable" + }, + "behavior_inventory": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "contract_precedence": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "defaults_tri_state_mapping": { + "reason": "The migrated fields do not have defaults or tri-state semantics.", + "status": "not_applicable" + }, + "direct_mutation": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "exact_time_boundary": { + "reason": "The migrated behavior has no exact-time scheduling boundary.", + "status": "not_applicable" + }, + "field_parity": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "indirect_mutation": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "persistence_round_trip": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "reset_boundary": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "route_parity": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + }, + "state_transitions": { + "scenario_ids": [ + "migration-round-trip" + ], + "status": "applicable" + } + }, + "page_evidence": { + "reason": "Page evidence is supplied by the consumer during end-to-end composition.", + "status": "not_applicable" + }, + "record_kind": "consumer_migration_conformance", + "scenarios": [ + { + "assertions": [ + "The drawer state and persisted route data match after a reload." + ], + "command": "npm run test:migration -- --reporter=json", + "evidence_method": "browser", + "exit_code": 0, + "id": "migration-round-trip", + "observable_actions": [ + "Open the gallery route, toggle the drawer, save, and reload." + ], + "result_artifact": "evidence/results/migration-round-trip.json", + "run_id": "run-20260721-001", + "session_id": "session-20260721-001", + "source_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "schema_version": "1.0" +} diff --git a/consumer-reference/schema/consumer-conformance-record.schema.json b/consumer-reference/schema/consumer-conformance-record.schema.json new file mode 100644 index 0000000..a0e2735 --- /dev/null +++ b/consumer-reference/schema/consumer-conformance-record.schema.json @@ -0,0 +1,322 @@ +{ + "$defs": { + "adoptionMapping": { + "additionalProperties": false, + "properties": { + "consumer_target": { "$ref": "#/$defs/consumerTarget" }, + "debt": { + "items": { "$ref": "#/$defs/debt" }, + "type": "array" + }, + "deviations": { + "items": { "$ref": "#/$defs/deviation" }, + "type": "array" + }, + "local_decision": { "$ref": "#/$defs/localDecision" }, + "scenario_ids": { "$ref": "#/$defs/scenarioIds" }, + "stylegallery": { "$ref": "#/$defs/stylegallerySource" } + }, + "required": [ + "consumer_target", + "debt", + "deviations", + "local_decision", + "scenario_ids", + "stylegallery" + ], + "type": "object" + }, + "consumerTarget": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "identity": { + "pattern": "^[A-Z][A-Za-z0-9]*(?:[._/-][A-Za-z0-9_-]+)*$", + "type": "string" + }, + "kind": { "const": "component" } + }, + "required": ["identity", "kind"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "identity": { + "pattern": "^(?:[.#][A-Za-z_][A-Za-z0-9_-]*|\\[[A-Za-z_:][-A-Za-z0-9_:.]*(?:=[^\\]\\r\\n]+)?\\])$", + "type": "string" + }, + "kind": { "const": "selector" } + }, + "required": ["identity", "kind"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "identity": { "$ref": "#/$defs/repositoryPath" }, + "kind": { "const": "module" } + }, + "required": ["identity", "kind"], + "type": "object" + } + ] + }, + "debt": { + "additionalProperties": false, + "properties": { + "owner": { "minLength": 1, "type": "string" }, + "review_by": { "format": "date-time", "type": "string" }, + "review_trigger": { "$ref": "#/$defs/sentence" }, + "summary": { "$ref": "#/$defs/sentence" } + }, + "required": ["owner", "review_by", "review_trigger", "summary"], + "type": "object" + }, + "deviation": { + "additionalProperties": false, + "properties": { + "reason": { "$ref": "#/$defs/sentence" }, + "summary": { "$ref": "#/$defs/sentence" } + }, + "required": ["reason", "summary"], + "type": "object" + }, + "dimension": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "scenario_ids": { "$ref": "#/$defs/scenarioIds" }, + "status": { "const": "applicable" } + }, + "required": ["scenario_ids", "status"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "reason": { "$ref": "#/$defs/sentence" }, + "status": { "const": "not_applicable" } + }, + "required": ["reason", "status"], + "type": "object" + } + ] + }, + "jsonPath": { + "pattern": "^(?!/)(?!.*//)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?![A-Za-z][A-Za-z0-9+.-]*:)(?!.*\\/$)[^\\\\?#]+\\.json$", + "type": "string" + }, + "localDecision": { + "additionalProperties": false, + "properties": { + "decision": { "$ref": "#/$defs/sentence" }, + "identity": { + "minLength": 1, + "pattern": "^\\S(?:.*\\S)?$", + "type": "string" + }, + "kind": { "enum": ["token", "unit"] } + }, + "required": ["decision", "identity", "kind"], + "type": "object" + }, + "pageEvidence": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "manifest": { "$ref": "#/$defs/jsonPath" }, + "status": { "const": "applicable" } + }, + "required": ["manifest", "status"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "reason": { "$ref": "#/$defs/sentence" }, + "status": { "const": "not_applicable" } + }, + "required": ["reason", "status"], + "type": "object" + } + ] + }, + "repositoryPath": { + "pattern": "^(?!/)(?!.*//)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?![A-Za-z][A-Za-z0-9+.-]*:)(?!.*\\/$)[^\\\\?#]+$", + "type": "string" + }, + "scenario": { + "additionalProperties": false, + "properties": { + "assertions": { + "items": { "minLength": 1, "type": "string" }, + "minItems": 1, + "type": "array" + }, + "command": { "minLength": 1, "type": "string" }, + "evidence_method": { "enum": ["unit", "integration", "browser"] }, + "exit_code": { "const": 0 }, + "id": { "$ref": "#/$defs/slug" }, + "observable_actions": { + "items": { "minLength": 1, "type": "string" }, + "minItems": 1, + "type": "array" + }, + "result_artifact": { "$ref": "#/$defs/jsonPath" }, + "run_id": { "minLength": 1, "pattern": "^\\S+$", "type": "string" }, + "session_id": { "minLength": 1, "pattern": "^\\S+$", "type": "string" }, + "source_digest": { "$ref": "#/$defs/sha256" } + }, + "required": [ + "assertions", + "command", + "evidence_method", + "exit_code", + "id", + "observable_actions", + "result_artifact", + "run_id", + "session_id", + "source_digest" + ], + "type": "object" + }, + "scenarioIds": { + "items": { "$ref": "#/$defs/slug" }, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "sentence": { + "minLength": 10, + "pattern": "^\\S+(?:\\s+\\S+){2,}\\s*[.!?]$", + "type": "string" + }, + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + }, + "slug": { + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "type": "string" + }, + "stylegallerySource": { + "additionalProperties": false, + "properties": { + "anchor": { + "pattern": "^#[a-z0-9]+(?:-[a-z0-9]+)*$", + "type": "string" + }, + "path": { "$ref": "#/$defs/repositoryPath" }, + "revision": { + "pattern": "^[a-f0-9]{40}$", + "type": "string" + } + }, + "required": ["anchor", "path", "revision"], + "type": "object" + } + }, + "$id": "https://stylegallery.example/consumer-reference/schema/consumer-conformance-record.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "adoption_mappings": { + "items": { "$ref": "#/$defs/adoptionMapping" }, + "minItems": 1, + "type": "array" + }, + "claim_boundary": { + "additionalProperties": false, + "properties": { + "certifies_accessibility": { "const": false }, + "certifies_product_correctness": { "const": false }, + "scope": { "$ref": "#/$defs/sentence" } + }, + "required": ["certifies_accessibility", "certifies_product_correctness", "scope"], + "type": "object" + }, + "consumer": { + "additionalProperties": false, + "properties": { + "relevant_sources": { + "items": { "$ref": "#/$defs/repositoryPath" }, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "repository": { + "minLength": 3, + "pattern": "^\\S+/\\S+$", + "type": "string" + }, + "revision": { + "pattern": "^[a-f0-9]{40}$", + "type": "string" + } + }, + "required": ["relevant_sources", "repository", "revision"], + "type": "object" + }, + "id": { "$ref": "#/$defs/slug" }, + "migration_dimensions": { + "additionalProperties": false, + "properties": { + "action_parity": { "$ref": "#/$defs/dimension" }, + "atomic_batch_behavior": { "$ref": "#/$defs/dimension" }, + "behavior_inventory": { "$ref": "#/$defs/dimension" }, + "contract_precedence": { "$ref": "#/$defs/dimension" }, + "defaults_tri_state_mapping": { "$ref": "#/$defs/dimension" }, + "direct_mutation": { "$ref": "#/$defs/dimension" }, + "exact_time_boundary": { "$ref": "#/$defs/dimension" }, + "field_parity": { "$ref": "#/$defs/dimension" }, + "indirect_mutation": { "$ref": "#/$defs/dimension" }, + "persistence_round_trip": { "$ref": "#/$defs/dimension" }, + "reset_boundary": { "$ref": "#/$defs/dimension" }, + "route_parity": { "$ref": "#/$defs/dimension" }, + "state_transitions": { "$ref": "#/$defs/dimension" } + }, + "required": [ + "action_parity", + "atomic_batch_behavior", + "behavior_inventory", + "contract_precedence", + "defaults_tri_state_mapping", + "direct_mutation", + "exact_time_boundary", + "field_parity", + "indirect_mutation", + "persistence_round_trip", + "reset_boundary", + "route_parity", + "state_transitions" + ], + "type": "object" + }, + "page_evidence": { "$ref": "#/$defs/pageEvidence" }, + "record_kind": { "const": "consumer_migration_conformance" }, + "scenarios": { + "items": { "$ref": "#/$defs/scenario" }, + "minItems": 1, + "type": "array" + }, + "schema_version": { "const": "1.0" } + }, + "required": [ + "adoption_mappings", + "claim_boundary", + "consumer", + "id", + "migration_dimensions", + "page_evidence", + "record_kind", + "scenarios", + "schema_version" + ], + "title": "Consumer migration conformance record", + "type": "object" +} diff --git a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json index 0ce4bdb..bb6e292 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json @@ -17,7 +17,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -45,7 +45,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -98,14 +98,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -223,7 +223,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -249,7 +249,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -277,7 +277,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -330,14 +330,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -455,7 +455,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -481,7 +481,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -509,7 +509,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -562,14 +562,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -687,7 +687,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -715,7 +715,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -743,7 +743,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -796,14 +796,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -921,7 +921,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -946,7 +946,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -974,7 +974,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1027,14 +1027,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1152,7 +1152,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1177,7 +1177,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1205,7 +1205,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1258,14 +1258,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1383,7 +1383,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1410,7 +1410,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1438,7 +1438,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1491,14 +1491,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1616,7 +1616,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1643,7 +1643,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1671,7 +1671,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1724,14 +1724,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1849,7 +1849,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1876,7 +1876,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1904,7 +1904,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1957,14 +1957,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2082,7 +2082,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2111,7 +2111,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2139,7 +2139,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2192,14 +2192,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2317,7 +2317,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2345,7 +2345,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2373,7 +2373,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2426,14 +2426,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2551,7 +2551,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2579,7 +2579,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2607,7 +2607,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2660,14 +2660,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2785,7 +2785,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2815,7 +2815,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2843,7 +2843,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2896,14 +2896,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -3021,7 +3021,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3047,7 +3047,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3075,7 +3075,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3128,14 +3128,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -3253,7 +3253,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3279,7 +3279,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3307,7 +3307,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3360,14 +3360,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -3485,7 +3485,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, diff --git a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json index 3565d8e..3a7cee4 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json @@ -17,7 +17,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -45,7 +45,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -98,14 +98,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -223,7 +223,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -249,7 +249,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -277,7 +277,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -330,14 +330,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -455,7 +455,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -481,7 +481,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -509,7 +509,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -562,14 +562,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -687,7 +687,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -715,7 +715,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -743,7 +743,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -796,14 +796,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -921,7 +921,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -946,7 +946,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -974,7 +974,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1027,14 +1027,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1152,7 +1152,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1177,7 +1177,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1205,7 +1205,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1258,14 +1258,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1383,7 +1383,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1410,7 +1410,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1438,7 +1438,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1491,14 +1491,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1616,7 +1616,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1643,7 +1643,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1671,7 +1671,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1724,14 +1724,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -1849,7 +1849,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1876,7 +1876,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1904,7 +1904,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1957,14 +1957,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2082,7 +2082,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2111,7 +2111,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2139,7 +2139,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2192,14 +2192,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2317,7 +2317,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2345,7 +2345,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2373,7 +2373,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2426,14 +2426,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2551,7 +2551,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2579,7 +2579,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2607,7 +2607,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2660,14 +2660,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -2785,7 +2785,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2815,7 +2815,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2843,7 +2843,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2896,14 +2896,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -3021,7 +3021,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3047,7 +3047,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3075,7 +3075,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3128,14 +3128,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -3253,7 +3253,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3279,7 +3279,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3307,7 +3307,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e", + "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3360,14 +3360,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59030, + "byte_length": 59586, "path": "package-lock.json", - "sha256": "sha256:f57973ff40f27c7f545f94e9529b61fcc95d8521662f7e3ea3a2ece392dfc86e" + "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1404, + "byte_length": 1432, "path": "package.json", - "sha256": "sha256:d08d6dd5282c93788b1ec54a5a9fb9306f314a7357d5c915173141d5b0c84406" + "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" }, { "byte_length": 532, @@ -3485,7 +3485,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:b3c88210f186b8c662666f73187dd7bd1cb02012079096516cb27a3142fd80bf" + "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" }, "started_at": "2026-07-14T06:33:47.214Z" }, diff --git a/package-lock.json b/package-lock.json index 2f7c7c8..e3d927c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ }, "devDependencies": { "@playwright/test": "1.61.0", - "ajv": "8.20.0" + "ajv": "8.20.0", + "ajv-formats": "3.0.1" }, "engines": { "node": ">=22" @@ -509,6 +510,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", diff --git a/package.json b/package.json index 7e766b1..a56e3d6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ }, "devDependencies": { "@playwright/test": "1.61.0", - "ajv": "8.20.0" + "ajv": "8.20.0", + "ajv-formats": "3.0.1" }, "dependencies": { "style-dictionary": "5.5.0" diff --git a/scripts/consumer-conformance-contract.mjs b/scripts/consumer-conformance-contract.mjs new file mode 100644 index 0000000..5899b12 --- /dev/null +++ b/scripts/consumer-conformance-contract.mjs @@ -0,0 +1,130 @@ +import { isPlainObject } from "./consumer-reference-schema.mjs"; + +export const MIGRATION_DIMENSIONS = Object.freeze([ + "behavior_inventory", + "route_parity", + "field_parity", + "action_parity", + "state_transitions", + "contract_precedence", + "direct_mutation", + "indirect_mutation", + "persistence_round_trip", + "reset_boundary", + "exact_time_boundary", + "defaults_tri_state_mapping", + "atomic_batch_behavior", +]); + +export const RUNTIME_EVIDENCE_METHODS = Object.freeze(["unit", "integration", "browser"]); + +const sentencePattern = /^\S+(?:\s+\S+){2,}\s*[.!?]$/; +const revisionPattern = /^[a-f0-9]{40}$/; +const componentPattern = /^[A-Z][A-Za-z0-9]*(?:[._/-][A-Za-z0-9_-]+)*$/; +const selectorPattern = /^(?:[.#][A-Za-z_][A-Za-z0-9_-]*|\[[A-Za-z_:][-A-Za-z0-9_:.]*(?:=[^\]\r\n]+)?\])$/; + +function finding(code, message, recordPath) { + return { code, message, path: recordPath }; +} + +function uniqueFindings(findings) { + return [...new Map(findings.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()]; +} + +export function isNormalizedRepositoryPath(value, { jsonOnly = false } = {}) { + if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("//") || value.includes("?") || value.includes("#")) return false; + if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return false; + if (value.split("/").some((segment) => segment === "." || segment === ".." || segment.length === 0)) return false; + return !jsonOnly || value.endsWith(".json"); +} + +function isSentence(value) { + return typeof value === "string" && value.length >= 10 && sentencePattern.test(value); +} + +function isConsumerTarget(target) { + if (!isPlainObject(target) || typeof target.identity !== "string") return false; + if (target.kind === "component") return componentPattern.test(target.identity); + if (target.kind === "selector") return selectorPattern.test(target.identity); + if (target.kind === "module") return isNormalizedRepositoryPath(target.identity); + return false; +} + +function schemaCode(error) { + const pointer = error.instancePath ?? ""; + const missing = error.params?.missingProperty; + if (pointer === "/migration_dimensions" && MIGRATION_DIMENSIONS.includes(missing)) return "migration_dimension_required"; + if (/^\/migration_dimensions\/[^/]+\/scenario_ids$/.test(pointer) && error.keyword === "minItems") return "migration_dimension_scenario_required"; + if (/^\/scenarios\/\d+\/evidence_method$/.test(pointer)) return "runtime_evidence_method_invalid"; + if (/^\/adoption_mappings\/\d+\/consumer_target/.test(pointer)) return "adoption_consumer_target_invalid"; + if (/^\/adoption_mappings\/\d+\/stylegallery\/revision$/.test(pointer)) return "adoption_stylegallery_revision_unpinned"; + if (/^\/adoption_mappings\/\d+\/debt\/\d+/.test(pointer)) return "adoption_debt_incomplete"; + return "consumer_conformance_schema_invalid"; +} + +export function consumerConformanceSchemaFindings(errors, recordPath) { + return uniqueFindings((errors ?? []).map((error) => finding( + schemaCode(error), + `${error.instancePath || "/"} ${error.message ?? "does not satisfy the schema"}`, + recordPath, + ))); +} + +export function validateConsumerConformanceSemantics(value, recordPath) { + const findings = []; + const add = (code, message) => findings.push(finding(code, message, recordPath)); + if (!isPlainObject(value)) { + add("consumer_conformance_record_invalid", "consumer conformance record must be a JSON object"); + return findings; + } + + const scenarios = Array.isArray(value.scenarios) ? value.scenarios.filter(isPlainObject) : []; + const scenarioIds = scenarios.map((scenario) => scenario.id).filter((id) => typeof id === "string"); + const scenarioSet = new Set(scenarioIds); + if (scenarioIds.length !== scenarioSet.size) add("runtime_scenario_id_duplicate", "runtime scenario IDs must be unique"); + for (const scenario of scenarios) { + if (!RUNTIME_EVIDENCE_METHODS.includes(scenario.evidence_method)) add("runtime_evidence_method_invalid", "runtime evidence must use unit, integration, or browser execution"); + if (scenario.exit_code !== 0) add("runtime_evidence_exit_nonzero", `scenario ${scenario.id ?? ""} must record exit code zero`); + if (!isNormalizedRepositoryPath(scenario.result_artifact, { jsonOnly: true })) add("runtime_result_artifact_invalid", `scenario ${scenario.id ?? ""} requires a normalized JSON result artifact`); + } + + const dimensions = isPlainObject(value.migration_dimensions) ? value.migration_dimensions : {}; + for (const dimensionName of MIGRATION_DIMENSIONS) { + if (!Object.hasOwn(dimensions, dimensionName)) { + add("migration_dimension_required", `migration dimension ${dimensionName} must be explicitly classified`); + continue; + } + const dimension = dimensions[dimensionName]; + if (!isPlainObject(dimension)) continue; + if (dimension.status === "applicable") { + if (!Array.isArray(dimension.scenario_ids) || dimension.scenario_ids.length === 0) { + add("migration_dimension_scenario_required", `applicable migration dimension ${dimensionName} requires a runtime scenario`); + } else { + for (const scenarioId of dimension.scenario_ids) { + if (!scenarioSet.has(scenarioId)) add("migration_dimension_scenario_unknown", `migration dimension ${dimensionName} references unknown scenario ${scenarioId}`); + } + } + } else if (dimension.status === "not_applicable" && !isSentence(dimension.reason)) { + add("migration_dimension_reason_invalid", `not-applicable migration dimension ${dimensionName} requires a sentence reason`); + } + } + + const mappings = Array.isArray(value.adoption_mappings) ? value.adoption_mappings.filter(isPlainObject) : []; + for (const [index, mapping] of mappings.entries()) { + if (!isConsumerTarget(mapping.consumer_target)) add("adoption_consumer_target_invalid", `adoption mapping ${index} requires a normalized local component, selector, or module identity`); + if (!revisionPattern.test(mapping.stylegallery?.revision ?? "")) add("adoption_stylegallery_revision_unpinned", `adoption mapping ${index} requires a full StyleGallery revision`); + for (const scenarioId of Array.isArray(mapping.scenario_ids) ? mapping.scenario_ids : []) { + if (!scenarioSet.has(scenarioId)) add("adoption_scenario_unknown", `adoption mapping ${index} references unknown scenario ${scenarioId}`); + } + for (const debt of Array.isArray(mapping.debt) ? mapping.debt : []) { + if (!isPlainObject(debt) || typeof debt.owner !== "string" || debt.owner.length === 0 || !isSentence(debt.review_trigger) || typeof debt.review_by !== "string") { + add("adoption_debt_incomplete", `adoption mapping ${index} debt requires owner, review trigger, and review-by date`); + } + } + } + + if (value.page_evidence?.status === "applicable" && !isNormalizedRepositoryPath(value.page_evidence.manifest, { jsonOnly: true })) { + add("page_evidence_manifest_path_invalid", "applicable page evidence requires a normalized JSON manifest path"); + } + return uniqueFindings(findings); +} diff --git a/scripts/json-schema-formats.mjs b/scripts/json-schema-formats.mjs new file mode 100644 index 0000000..0a20636 --- /dev/null +++ b/scripts/json-schema-formats.mjs @@ -0,0 +1,31 @@ +import addFormats from "ajv-formats"; + +const rfc3339DateTime = /^(?\d{4})-(?\d{2})-(?\d{2})T(?\d{2}):(?\d{2}):(?\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/; + +export function isRfc3339DateTime(value) { + if (typeof value !== "string") return false; + const match = rfc3339DateTime.exec(value); + if (!match || !Number.isFinite(Date.parse(value))) return false; + + const year = Number(match.groups.year); + const month = Number(match.groups.month); + const day = Number(match.groups.day); + const hour = Number(match.groups.hour); + const minute = Number(match.groups.minute); + const second = Number(match.groups.second); + const canonical = new Date(0); + canonical.setUTCFullYear(year, month - 1, day); + canonical.setUTCHours(hour, minute, second, 0); + + return canonical.getUTCFullYear() === year + && canonical.getUTCMonth() === month - 1 + && canonical.getUTCDate() === day + && canonical.getUTCHours() === hour + && canonical.getUTCMinutes() === minute + && canonical.getUTCSeconds() === second; +} + +export function addDateTimeFormat(ajv) { + addFormats(ajv, ["date-time"]); + return ajv; +} diff --git a/scripts/test-json-schema-formats.mjs b/scripts/test-json-schema-formats.mjs new file mode 100644 index 0000000..6d7703c --- /dev/null +++ b/scripts/test-json-schema-formats.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import test from "node:test"; +import Ajv2020 from "ajv/dist/2020.js"; +import { addDateTimeFormat, isRfc3339DateTime } from "./json-schema-formats.mjs"; + +test("rejects RFC3339 values whose calendar date would normalize", () => { + for (const value of [ + "2023-02-29T12:00:00Z", + "2023-02-30T12:00:00Z", + "2024-04-31T12:00:00+05:30", + ]) { + assert.equal(isRfc3339DateTime(value), false, value); + } +}); + +test("preserves valid RFC3339 offsets and fractional seconds", () => { + for (const value of [ + "2024-02-29T23:59:59Z", + "2024-02-29T23:59:59.123456789+05:30", + "2024-02-29T00:00:00-04:00", + ]) { + assert.equal(isRfc3339DateTime(value), true, value); + } +}); + +test("uses the strict validator for the AJV date-time format", () => { + const ajv = new Ajv2020({ allErrors: true, strict: true }); + addDateTimeFormat(ajv); + const validate = ajv.compile({ + additionalProperties: false, + properties: { at: { format: "date-time", type: "string" } }, + required: ["at"], + type: "object", + }); + + assert.equal(validate({ at: "2024-02-29T23:59:59.123Z" }), true); + assert.equal(validate({ at: "2023-02-29T23:59:59.123Z" }), false); +}); diff --git a/scripts/test-validate-consumer-conformance.mjs b/scripts/test-validate-consumer-conformance.mjs new file mode 100644 index 0000000..492d872 --- /dev/null +++ b/scripts/test-validate-consumer-conformance.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { parseStrictJson } from "./strict-json.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const fixtureRoot = path.join(repositoryRoot, "consumer-reference", "fixtures", "consumer-conformance"); +const validator = path.join(repositoryRoot, "scripts", "validate-consumer-conformance.mjs"); +const expectedCases = Object.freeze([ + ["omitted-dimension", "migration_dimension_required"], + ["applicable-without-scenario", "migration_dimension_scenario_required"], + ["source-regex-runtime-proof", "runtime_evidence_method_invalid"], + ["orphan-adoption-scenario", "adoption_scenario_unknown"], + ["malformed-local-target", "adoption_consumer_target_invalid"], + ["unpinned-stylegallery-revision", "adoption_stylegallery_revision_unpinned"], + ["debt-without-lifecycle", "adoption_debt_incomplete"], +]); + +function parseArguments(argv) { + const options = { caseName: undefined, json: false }; + for (let index = 2; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--json") options.json = true; + else if (argument === "--case") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error("--case requires a case name"); + options.caseName = value; + index += 1; + } else throw new Error(`unsupported argument ${argument}`); + } + return options; +} + +function sameSet(actual, expected) { + return actual.length === expected.length && new Set(actual).size === expected.length && expected.every((entry) => actual.includes(entry)); +} + +function readJson(file) { + return parseStrictJson(fs.readFileSync(file, "utf8")); +} + +function validateInventory(manifest) { + const expectedKeys = ["id", "invalid_cases", "schema_record", "schema_version", "valid_records"]; + const encodedCases = Array.isArray(manifest.invalid_cases) + ? manifest.invalid_cases.map((entry) => Object.keys(entry).length === 2 ? `${entry.name}:${entry.expected_code}` : "") + : []; + const expectedEncoded = expectedCases.map(([name, code]) => `${name}:${code}`); + const entries = fs.readdirSync(fixtureRoot, { withFileTypes: true }); + const closedFiles = entries.every((entry) => entry.isFile() && path.extname(entry.name) === ".json") + && sameSet(entries.map((entry) => entry.name), ["manifest.json", "valid-migration.json"]); + return Object.keys(manifest).length === expectedKeys.length + && sameSet(Object.keys(manifest), expectedKeys) + && manifest.id === "consumer-conformance-fixture-inventory" + && manifest.schema_version === "1.0" + && manifest.schema_record === "consumer-reference/schema/consumer-conformance-record.schema.json" + && Array.isArray(manifest.valid_records) + && sameSet(manifest.valid_records, ["valid-migration.json"]) + && sameSet(encodedCases, expectedEncoded) + && closedFiles; +} + +function mutatedRecord(name, source) { + const value = structuredClone(source); + if (name === "omitted-dimension") delete value.migration_dimensions.exact_time_boundary; + else if (name === "applicable-without-scenario") value.migration_dimensions.route_parity = { scenario_ids: [], status: "applicable" }; + else if (name === "source-regex-runtime-proof") value.scenarios[0].evidence_method = "source_regex"; + else if (name === "orphan-adoption-scenario") value.adoption_mappings[0].scenario_ids = ["missing-scenario"]; + else if (name === "malformed-local-target") value.adoption_mappings[0].consumer_target = { identity: "app shell", kind: "selector" }; + else if (name === "unpinned-stylegallery-revision") value.adoption_mappings[0].stylegallery.revision = "abc123"; + else if (name === "debt-without-lifecycle") value.adoption_mappings[0].debt = [{ summary: "Replace the temporary spacing bridge." }]; + else throw new Error(`unknown fixture case ${name}`); + return value; +} + +function runValidator(root, record) { + const child = spawnSync(process.execPath, [validator, "--root", root, "--record", record, "--json"], { + cwd: repositoryRoot, + encoding: "utf8", + }); + let output; + try { + output = JSON.parse(child.stdout); + } catch { + output = { failures: [], parse_error: child.stderr || child.stdout }; + } + return { output, status: child.status }; +} + +const options = parseArguments(process.argv); +const tempRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-conformance-"))); +const results = []; +try { + const manifest = readJson(path.join(fixtureRoot, "manifest.json")); + const inventoryOk = validateInventory(manifest) && fs.existsSync(path.join(repositoryRoot, manifest.schema_record)); + results.push({ actual: inventoryOk, expected: true, name: "closed-fixture-inventory", ok: inventoryOk }); + const valid = readJson(path.join(fixtureRoot, "valid-migration.json")); + const selected = options.caseName ?? "all"; + if (selected === "all" || selected === "valid-migration") { + const child = runValidator(repositoryRoot, "consumer-reference/fixtures/consumer-conformance/valid-migration.json"); + const ok = child.status === 0 && child.output.ok === true && child.output.checkedDimensions === 13 && child.output.checkedMappings === 1; + results.push({ actual: { checkedDimensions: child.output.checkedDimensions, checkedMappings: child.output.checkedMappings, codes: child.output.failures?.map((entry) => entry.code) ?? [], status: child.status }, expected: "valid record, 13 dimensions, one mapping, exit:0", name: "valid-migration", ok }); + } + for (const [name, expectedCode] of expectedCases) { + if (selected !== "all" && selected !== name) continue; + const record = path.join(tempRoot, `${name}.json`); + fs.writeFileSync(record, `${JSON.stringify(mutatedRecord(name, valid), null, 2)}\n`); + const child = runValidator(tempRoot, path.basename(record)); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes(expectedCode); + results.push({ actual: { codes, status: child.status }, expected: expectedCode, name, ok }); + } + if (selected !== "all" && selected !== "valid-migration" && !expectedCases.some(([name]) => name === selected)) { + results.push({ actual: selected, expected: "known case", name: "case-selection", ok: false }); + } +} finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); +} + +const result = { ok: results.every((entry) => entry.ok), results }; +if (options.json) console.log(JSON.stringify(result, null, 2)); +else if (result.ok) console.log(`ok: ${results.length} consumer conformance fixture cases`); +else console.error(results.filter((entry) => !entry.ok).map((entry) => `${entry.name}: expected ${entry.expected}`).join("\n")); +process.exitCode = result.ok ? 0 : 1; diff --git a/scripts/validate-consumer-conformance.mjs b/scripts/validate-consumer-conformance.mjs new file mode 100644 index 0000000..f58705c --- /dev/null +++ b/scripts/validate-consumer-conformance.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv2020 from "ajv/dist/2020.js"; +import { + MIGRATION_DIMENSIONS, + consumerConformanceSchemaFindings, + isNormalizedRepositoryPath, + validateConsumerConformanceSemantics, +} from "./consumer-conformance-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; +import { addDateTimeFormat } from "./json-schema-formats.mjs"; + +const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const schemaPath = path.join(styleGalleryRoot, "consumer-reference", "schema", "consumer-conformance-record.schema.json"); + +function finding(code, message, recordPath) { + return { code, message, path: recordPath }; +} + +function parseArguments(argv) { + const options = { json: false, record: undefined, root: process.cwd() }; + const failures = []; + for (let index = 2; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--json") options.json = true; + else if (argument === "--record" || argument === "--root") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) failures.push(finding("argument_value_required", `${argument} requires a value`, "")); + else { + options[argument === "--record" ? "record" : "root"] = value; + index += 1; + } + } else failures.push(finding("argument_unknown", `unsupported argument ${argument}`, "")); + } + if (!options.record) failures.push(finding("argument_value_required", "--record requires a normalized JSON path", "")); + return { failures, options }; +} + +function containedRecord(rootArgument, recordArgument, failures) { + const root = path.resolve(process.cwd(), rootArgument); + if (!fs.existsSync(root) || !fs.lstatSync(root).isDirectory() || fs.realpathSync(root) !== root) { + failures.push(finding("consumer_conformance_root_invalid", "consumer root must be a real directory without filesystem redirects", rootArgument)); + return undefined; + } + if (!isNormalizedRepositoryPath(recordArgument, { jsonOnly: true })) { + failures.push(finding("consumer_conformance_record_path_invalid", "--record must be a normalized repository-relative JSON path", recordArgument)); + return undefined; + } + const resolved = path.resolve(root, recordArgument); + const relative = path.relative(root, resolved); + if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || !fs.existsSync(resolved)) { + failures.push(finding("consumer_conformance_record_path_invalid", "consumer conformance record must exist inside the consumer root", recordArgument)); + return undefined; + } + let current = root; + for (const segment of relative.split(path.sep).filter(Boolean)) { + current = path.join(current, segment); + if (fs.lstatSync(current).isSymbolicLink()) { + failures.push(finding("consumer_conformance_record_path_invalid", "consumer conformance record must not traverse a symlink", recordArgument)); + return undefined; + } + } + if (!fs.lstatSync(resolved).isFile() || fs.realpathSync(resolved) !== resolved) { + failures.push(finding("consumer_conformance_record_path_invalid", "consumer conformance record must be a contained regular file", recordArgument)); + return undefined; + } + return { recordPath: relative.split(path.sep).join("/"), resolved }; +} + +function readStrictJson(file, code, displayPath, failures) { + try { + return parseStrictJson(fs.readFileSync(file, "utf8")); + } catch (error) { + failures.push(finding(code, error instanceof Error ? error.message : String(error), displayPath)); + return undefined; + } +} + +function uniqueFindings(findings) { + return [...new Map(findings.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()]; +} + +const { failures: argumentFailures, options } = parseArguments(process.argv); +const failures = [...argumentFailures]; +const record = options.record ? containedRecord(options.root, options.record, failures) : undefined; +const schema = readStrictJson(schemaPath, "consumer_conformance_schema_invalid", "consumer-reference/schema/consumer-conformance-record.schema.json", failures); +const value = record ? readStrictJson(record.resolved, "consumer_conformance_json_invalid", record.recordPath, failures) : undefined; + +if (schema && value !== undefined) { + try { + const ajv = new Ajv2020({ allErrors: true, strict: true }); + addDateTimeFormat(ajv); + const validate = ajv.compile(schema); + if (!validate(value)) failures.push(...consumerConformanceSchemaFindings(validate.errors, record.recordPath)); + failures.push(...validateConsumerConformanceSemantics(value, record.recordPath)); + } catch (error) { + failures.push(finding("consumer_conformance_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema/consumer-conformance-record.schema.json")); + } +} + +const uniqueFailures = uniqueFindings(failures); +const result = { + checkedDimensions: value && typeof value.migration_dimensions === "object" && value.migration_dimensions !== null + ? MIGRATION_DIMENSIONS.filter((name) => Object.hasOwn(value.migration_dimensions, name)).length + : 0, + checkedMappings: Array.isArray(value?.adoption_mappings) ? value.adoption_mappings.length : 0, + checkedScenarios: Array.isArray(value?.scenarios) ? value.scenarios.length : 0, + failures: uniqueFailures, + ok: uniqueFailures.length === 0, + record: record?.recordPath ?? options.record ?? null, +}; +if (options.json) console.log(JSON.stringify(result, null, 2)); +else if (result.ok) console.log(`ok: ${result.checkedDimensions} migration dimensions, ${result.checkedMappings} adoption mappings`); +else console.error(result.failures.map((entry) => `${entry.code}: ${entry.path}: ${entry.message}`).join("\n")); +process.exitCode = result.ok ? 0 : 1; From 3c9208748ded57ee0764b11ea63b2f8ce891ae87 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:34:55 +0900 Subject: [PATCH 38/44] feat(evidence): bind page evidence to source sessions Plan-Task: 2 --- .../schema/page-evidence-manifest.schema.json | 216 +++++++++++++ .../schema/page-evidence-session.schema.json | 119 +++++++ scripts/create-page-evidence-session.mjs | 139 ++++++++ scripts/finalize-page-evidence.mjs | 197 ++++++++++++ scripts/page-artifact-metadata.mjs | 124 +++++++ scripts/page-evidence-contract.mjs | 297 +++++++++++++++++ scripts/page-evidence-fixture.mjs | 271 ++++++++++++++++ scripts/test-validate-page-evidence.mjs | 302 ++++++++++++++++++ scripts/validate-page-evidence.mjs | 191 +++++++++++ 9 files changed, 1856 insertions(+) create mode 100644 consumer-reference/schema/page-evidence-manifest.schema.json create mode 100644 consumer-reference/schema/page-evidence-session.schema.json create mode 100644 scripts/create-page-evidence-session.mjs create mode 100644 scripts/finalize-page-evidence.mjs create mode 100644 scripts/page-artifact-metadata.mjs create mode 100644 scripts/page-evidence-contract.mjs create mode 100644 scripts/page-evidence-fixture.mjs create mode 100644 scripts/test-validate-page-evidence.mjs create mode 100644 scripts/validate-page-evidence.mjs diff --git a/consumer-reference/schema/page-evidence-manifest.schema.json b/consumer-reference/schema/page-evidence-manifest.schema.json new file mode 100644 index 0000000..d987ee9 --- /dev/null +++ b/consumer-reference/schema/page-evidence-manifest.schema.json @@ -0,0 +1,216 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stylegallery.local/consumer-reference/schema/page-evidence-manifest.schema.json", + "title": "Closed consumer-local page evidence manifest", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "record_kind", "claim_boundary", "completed_at", "review_by", "repository", "revision", "run", "session", "scenarios"], + "properties": { + "schema_version": { "const": "1.0" }, + "record_kind": { "const": "page_evidence_manifest" }, + "claim_boundary": { "const": "Page evidence is claim-scoped runtime evidence, not visual-regression approval or accessibility certification." }, + "completed_at": { "type": "string", "format": "date-time" }, + "review_by": { "type": "string", "format": "date-time" }, + "repository": { "type": "string", "minLength": 1 }, + "revision": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "run": { "$ref": "#/$defs/run" }, + "session": { "$ref": "https://stylegallery.local/consumer-reference/schema/page-evidence-session.schema.json#/$defs/completed_session" }, + "scenarios": { + "type": "array", + "maxItems": 256, + "minItems": 1, + "items": { "$ref": "#/$defs/manifest_scenario" } + } + }, + "$defs": { + "hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "identity": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "scenario_id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "normalized_path": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?![A-Za-z]:)(?!.*\\\\)(?!.*[?#]).+$" }, + "run": { + "type": "object", + "additionalProperties": false, + "required": ["id", "source", "repository", "revision", "attempt", "status"], + "properties": { + "id": { "$ref": "#/$defs/identity" }, + "source": { "enum": ["local", "github_actions"] }, + "repository": { "type": "string", "minLength": 1 }, + "revision": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "attempt": { "type": "integer", "minimum": 1 }, + "status": { "const": "passed" } + } + }, + "semantic_environment": { + "type": "object", + "additionalProperties": false, + "required": ["browser", "browser_revision", "platform", "viewport", "page_scale_factor", "locale", "color_scheme", "reduced_motion"], + "properties": { + "browser": { "type": "string", "minLength": 1 }, + "browser_revision": { "type": "string", "minLength": 1 }, + "platform": { "type": "string", "minLength": 1 }, + "viewport": { + "type": "object", + "additionalProperties": false, + "required": ["width", "height"], + "properties": { + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 } + } + }, + "page_scale_factor": { "type": "number", "exclusiveMinimum": 0, "maximum": 10 }, + "locale": { "type": "string", "minLength": 1 }, + "color_scheme": { "enum": ["light", "dark", "no-preference"] }, + "reduced_motion": { "enum": ["reduce", "no-preference"] } + } + }, + "artifact_declaration": { + "type": "object", + "additionalProperties": false, + "required": ["path", "media_type"], + "properties": { + "path": { "$ref": "#/$defs/normalized_path" }, + "media_type": { "enum": ["application/json", "image/png", "text/plain"] } + } + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256", "byte_length", "media_type"], + "properties": { + "path": { "$ref": "#/$defs/normalized_path" }, + "sha256": { "$ref": "#/$defs/hash" }, + "byte_length": { "type": "integer", "minimum": 1 }, + "media_type": { "enum": ["application/json", "image/png", "text/plain"] }, + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 } + }, + "allOf": [ + { + "if": { "properties": { "media_type": { "const": "image/png" } }, "required": ["media_type"] }, + "then": { + "required": ["width", "height"], + "properties": { + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 } + } + } + } + ] + }, + "reuse": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "prior_manifest_sha256", "prior_session_id", "prior_run_id", "prior_scenario_id"], + "properties": { + "kind": { "const": "reused" }, + "prior_manifest_sha256": { "$ref": "#/$defs/hash" }, + "prior_session_id": { "$ref": "#/$defs/identity" }, + "prior_run_id": { "$ref": "#/$defs/identity" }, + "prior_scenario_id": { "$ref": "#/$defs/scenario_id" } + } + }, + "runner_result": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "record_kind", "session_id", "nonce", "receipt_sha256", "repository", "revision", "source_sha256", "run", "scenario_id", "recorded_at", "semantic_environment", "status", "evidence"], + "properties": { + "schema_version": { "const": "1.0" }, + "record_kind": { "const": "page_evidence_runner_result" }, + "session_id": { "$ref": "#/$defs/identity" }, + "nonce": { "$ref": "#/$defs/hash" }, + "receipt_sha256": { "$ref": "#/$defs/hash" }, + "repository": { "type": "string", "minLength": 1 }, + "revision": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "source_sha256": { "$ref": "#/$defs/hash" }, + "run": { + "type": "object", + "additionalProperties": false, + "required": ["id", "source", "repository", "revision", "attempt"], + "properties": { + "id": { "$ref": "#/$defs/identity" }, + "source": { "enum": ["local", "github_actions"] }, + "repository": { "type": "string", "minLength": 1 }, + "revision": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "attempt": { "type": "integer", "minimum": 1 } + } + }, + "scenario_id": { "$ref": "#/$defs/scenario_id" }, + "recorded_at": { "type": "string", "format": "date-time" }, + "semantic_environment": { "$ref": "#/$defs/semantic_environment" }, + "status": { "enum": ["passed", "failed"] }, + "evidence": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["kind", "artifacts"], + "properties": { + "kind": { "const": "captured" }, + "artifacts": { "type": "array", "minItems": 1, "maxItems": 128, "items": { "$ref": "#/$defs/artifact_declaration" } } + } + }, + { "$ref": "#/$defs/reuse" }, + { + "type": "object", + "additionalProperties": false, + "required": ["kind", "reason"], + "properties": { + "kind": { "const": "failed" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + } + }, + "allOf": [ + { + "if": { "properties": { "status": { "const": "passed" } }, "required": ["status"] }, + "then": { + "properties": { + "evidence": { + "type": "object", + "oneOf": [ + { "$ref": "#/$defs/reuse" }, + { "type": "object", "properties": { "kind": { "const": "captured" } }, "required": ["kind"] } + ] + } + } + } + }, + { + "if": { "properties": { "status": { "const": "failed" } }, "required": ["status"] }, + "then": { + "properties": { + "evidence": { "type": "object", "properties": { "kind": { "const": "failed" } }, "required": ["kind"] } + } + } + } + ] + }, + "manifest_scenario": { + "type": "object", + "additionalProperties": false, + "required": ["id", "recorded_at", "semantic_environment", "runner_result", "evidence"], + "properties": { + "id": { "$ref": "#/$defs/scenario_id" }, + "recorded_at": { "type": "string", "format": "date-time" }, + "semantic_environment": { "$ref": "#/$defs/semantic_environment" }, + "runner_result": { "$ref": "#/$defs/artifact" }, + "evidence": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["kind", "artifacts"], + "properties": { + "kind": { "const": "captured" }, + "artifacts": { "type": "array", "minItems": 1, "maxItems": 128, "items": { "$ref": "#/$defs/artifact" } } + } + }, + { "$ref": "#/$defs/reuse" } + ] + } + } + } + } +} diff --git a/consumer-reference/schema/page-evidence-session.schema.json b/consumer-reference/schema/page-evidence-session.schema.json new file mode 100644 index 0000000..923541f --- /dev/null +++ b/consumer-reference/schema/page-evidence-session.schema.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stylegallery.local/consumer-reference/schema/page-evidence-session.schema.json", + "title": "Consumer-local page evidence start receipt", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "record_kind", "session_id", "run_id", "nonce", "started_at", "repository", "revision", "branch", "attempt", "environment", "conformance_record", "source", "intended_scenario_ids"], + "properties": { + "schema_version": { "const": "1.0" }, + "record_kind": { "const": "page_evidence_session" }, + "session_id": { "$ref": "#/$defs/identity" }, + "run_id": { "$ref": "#/$defs/identity" }, + "nonce": { "$ref": "#/$defs/hash" }, + "started_at": { "type": "string", "format": "date-time" }, + "repository": { "type": "string", "minLength": 1 }, + "revision": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "branch": { "type": "string", "minLength": 1 }, + "attempt": { "type": "integer", "minimum": 1 }, + "environment": { "$ref": "#/$defs/producer_environment" }, + "conformance_record": { "$ref": "#/$defs/json_artifact" }, + "source": { "$ref": "#/$defs/source_manifest" }, + "intended_scenario_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/scenario_id" } + } + }, + "$defs": { + "hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "identity": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "scenario_id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "normalized_path": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?![A-Za-z]:)(?!.*\\\\)(?!.*[?#]).+$" }, + "producer_environment": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "node", "platform", "producer"], + "properties": { + "kind": { "const": "browser_evidence" }, + "node": { "type": "string", "pattern": "^v\\d+\\.\\d+\\.\\d+$" }, + "platform": { "type": "string", "minLength": 1 }, + "producer": { "const": "stylegallery_page_evidence" } + } + }, + "file_artifact": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256", "byte_length"], + "properties": { + "path": { "$ref": "#/$defs/normalized_path" }, + "sha256": { "$ref": "#/$defs/hash" }, + "byte_length": { "type": "integer", "minimum": 1 } + } + }, + "json_artifact": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256", "byte_length", "media_type"], + "properties": { + "path": { "$ref": "#/$defs/normalized_path" }, + "sha256": { "$ref": "#/$defs/hash" }, + "byte_length": { "type": "integer", "minimum": 1 }, + "media_type": { "const": "application/json" } + } + }, + "source_manifest": { + "type": "object", + "additionalProperties": false, + "required": ["sha256", "files"], + "properties": { + "sha256": { "$ref": "#/$defs/hash" }, + "files": { + "type": "array", + "maxItems": 1024, + "minItems": 1, + "items": { "$ref": "#/$defs/file_artifact" } + } + } + }, + "receipt_artifact": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256", "byte_length", "media_type"], + "properties": { + "path": { "const": "page-evidence-session.json" }, + "sha256": { "$ref": "#/$defs/hash" }, + "byte_length": { "type": "integer", "minimum": 1 }, + "media_type": { "const": "application/json" } + } + }, + "completed_session": { + "type": "object", + "additionalProperties": false, + "required": ["session_id", "run_id", "nonce", "receipt_sha256", "receipt", "started_at", "completed_at", "repository", "revision", "branch", "attempt", "environment", "conformance_record", "source", "intended_scenario_ids"], + "properties": { + "session_id": { "$ref": "#/$defs/identity" }, + "run_id": { "$ref": "#/$defs/identity" }, + "nonce": { "$ref": "#/$defs/hash" }, + "receipt_sha256": { "$ref": "#/$defs/hash" }, + "receipt": { "$ref": "#/$defs/receipt_artifact" }, + "started_at": { "type": "string", "format": "date-time" }, + "completed_at": { "type": "string", "format": "date-time" }, + "repository": { "type": "string", "minLength": 1 }, + "revision": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "branch": { "type": "string", "minLength": 1 }, + "attempt": { "type": "integer", "minimum": 1 }, + "environment": { "$ref": "#/$defs/producer_environment" }, + "conformance_record": { "$ref": "#/$defs/json_artifact" }, + "source": { "$ref": "#/$defs/source_manifest" }, + "intended_scenario_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/scenario_id" } + } + } + } + } +} diff --git a/scripts/create-page-evidence-session.mjs b/scripts/create-page-evidence-session.mjs new file mode 100644 index 0000000..6695981 --- /dev/null +++ b/scripts/create-page-evidence-session.mjs @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + addSchemaFindings, + canonicalSourceManifest, + compilePageEvidenceSchemas, + dirtyRelevantSources, + finding, + gitIdentity, + metadata, + normalizeReference, + PAGE_EVIDENCE_RECEIPT, + readJsonFile, + resolveContained, +} from "./page-evidence-contract.mjs"; + +const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const options = { artifactRoot: undefined, json: false, record: undefined, root: undefined }; +const failures = []; + +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (["--artifact-root", "--record", "--root"].includes(argument)) { + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) failures.push(finding("argument_value_required", `${argument} requires a value`, "")); + else { + if (argument === "--artifact-root") options.artifactRoot = path.resolve(process.cwd(), value); + if (argument === "--record") options.record = value; + if (argument === "--root") options.root = path.resolve(process.cwd(), value); + index += 1; + } + } else failures.push(finding("argument_unknown", `unsupported argument ${argument}`, "")); +} + +for (const [name, value] of [["--root", options.root], ["--record", options.record], ["--artifact-root", options.artifactRoot]]) { + if (!value) failures.push(finding("argument_value_required", `${name} is required`, "")); +} + +let schemas; +try { schemas = compilePageEvidenceSchemas(path.join(styleGalleryRoot, "consumer-reference/schema")); } +catch (error) { failures.push(finding("page_evidence_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema")); } + +let artifactReference; +if (options.root && options.artifactRoot) { + artifactReference = normalizeReference(options.root, options.artifactRoot); + if (!artifactReference) failures.push(finding("page_evidence_artifact_escape", "artifact root must be inside the consumer repository", options.artifactRoot)); + else { + const resolved = resolveContained({ allowMissing: true, expectedType: "directory", prefix: "page_evidence_artifact", reference: artifactReference, root: options.root }, failures); + if (resolved && fs.existsSync(resolved.file) && fs.readdirSync(resolved.file).length > 0) failures.push(finding("page_evidence_session_replay", "artifact root must be empty before session start", artifactReference)); + } +} + +let record; +let recordReference; +if (options.root && options.record) { + recordReference = normalizeReference(options.root, options.record); + if (!recordReference) failures.push(finding("page_evidence_conformance_escape", "conformance record must be inside the consumer repository", options.record)); + else record = readJsonFile({ prefix: "page_evidence_conformance", reference: recordReference, root: options.root }, failures); +} + +const consumer = record?.value?.consumer; +const sourcePaths = consumer?.relevant_sources; +const browserScenarios = Array.isArray(record?.value?.scenarios) + ? record.value.scenarios.filter((scenario) => scenario?.evidence_method === "browser") + : []; +if (!consumer || typeof consumer.repository !== "string" || !/^[a-f0-9]{40}$/.test(consumer.revision ?? "") || !Array.isArray(sourcePaths)) { + failures.push(finding("page_evidence_conformance_invalid", "conformance record must declare consumer repository, revision, and relevant_sources", recordReference ?? "")); +} +if (browserScenarios.length === 0) failures.push(finding("page_evidence_intent_missing", "conformance record must declare at least one browser scenario", recordReference ?? "")); + +const scenarioIds = browserScenarios.map((scenario) => scenario.id); +const sessionIds = new Set(browserScenarios.map((scenario) => scenario.session_id)); +const runIds = new Set(browserScenarios.map((scenario) => scenario.run_id)); +if (new Set(scenarioIds).size !== scenarioIds.length || scenarioIds.some((id) => typeof id !== "string")) failures.push(finding("page_evidence_intent_invalid", "browser scenario IDs must be unique strings", recordReference ?? "")); +if (sessionIds.size !== 1 || runIds.size !== 1 || [...sessionIds, ...runIds].some((identity) => typeof identity !== "string" || identity.length === 0)) { + failures.push(finding("page_evidence_intent_identity_mismatch", "all page scenarios must use one nonempty session and run identity", recordReference ?? "")); +} + +const git = options.root ? gitIdentity(options.root, failures) : undefined; +if (git && consumer) { + if (git.revision !== consumer.revision || (process.env.GITHUB_SHA && process.env.GITHUB_SHA !== git.revision)) failures.push(finding("page_evidence_revision_mismatch", "conformance revision must equal checked-out Git HEAD", recordReference)); + if (git.repository && git.repository !== consumer.repository) failures.push(finding("page_evidence_repository_mismatch", "conformance repository differs from the Git remote identity", recordReference)); +} + +let source; +if (options.root && Array.isArray(sourcePaths)) { + source = canonicalSourceManifest(options.root, sourcePaths, failures); + if (source) { + const dirty = dirtyRelevantSources(options.root, sourcePaths); + if (dirty.length > 0) failures.push(finding("page_evidence_source_dirty", `relevant sources must be clean tracked files: ${dirty.join(", ")}`, options.root)); + if (browserScenarios.some((scenario) => scenario.source_digest !== source.sha256)) failures.push(finding("page_evidence_source_mismatch", "browser scenario source_digest must equal the relevant-source aggregate", recordReference)); + } +} + +const receipt = record && source && git && sessionIds.size === 1 && runIds.size === 1 ? { + attempt: Number(process.env.GITHUB_RUN_ATTEMPT ?? 1), + branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || git.branch, + conformance_record: metadata(record.bytes, "application/json", recordReference), + environment: { + kind: "browser_evidence", + node: process.version, + platform: `${process.platform}/${process.arch}`, + producer: "stylegallery_page_evidence", + }, + intended_scenario_ids: scenarioIds, + nonce: crypto.randomBytes(32).toString("hex"), + record_kind: "page_evidence_session", + repository: consumer.repository, + revision: consumer.revision, + run_id: [...runIds][0], + schema_version: "1.0", + session_id: [...sessionIds][0], + source, + started_at: new Date().toISOString(), +} : undefined; +if (receipt && schemas) addSchemaFindings(schemas.session, receipt, PAGE_EVIDENCE_RECEIPT, "page_evidence_session_schema_invalid", failures); + +const receiptFile = options.artifactRoot ? path.join(options.artifactRoot, PAGE_EVIDENCE_RECEIPT) : undefined; +if (failures.length === 0 && receiptFile) { + fs.mkdirSync(options.artifactRoot, { recursive: true }); + fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" }); +} +const result = { + failures, + intendedScenarios: receipt?.intended_scenario_ids.length ?? 0, + ok: failures.length === 0, + receipt: receiptFile, + sessionId: receipt?.session_id, + sourceSha256: receipt?.source.sha256, +}; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`created page evidence session ${result.sessionId}\n`); +else process.stderr.write(`${failures.map((issue) => `${issue.code}: ${issue.path}: ${issue.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/finalize-page-evidence.mjs b/scripts/finalize-page-evidence.mjs new file mode 100644 index 0000000..678c3d4 --- /dev/null +++ b/scripts/finalize-page-evidence.mjs @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + addSchemaFindings, + canonicalSourceManifest, + compilePageEvidenceSchemas, + dirtyRelevantSources, + finding, + gitIdentity, + listArtifactFiles, + metadata, + normalizeReference, + PAGE_EVIDENCE_CLAIM, + PAGE_EVIDENCE_MANIFEST, + PAGE_EVIDENCE_RECEIPT, + readJsonFile, + readContainedBytes, + readReceipt, + resolveContained, + sameJson, + sameStringSet, + withinSession, +} from "./page-evidence-contract.mjs"; + +const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const options = { artifactRoot: undefined, json: false, priorManifest: undefined, reviewBy: undefined, root: undefined, runnerResults: [] }; +const failures = []; + +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (["--artifact-root", "--prior-manifest", "--review-by", "--root", "--runner-result"].includes(argument)) { + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) failures.push(finding("argument_value_required", `${argument} requires a value`, "")); + else { + if (argument === "--artifact-root") options.artifactRoot = path.resolve(process.cwd(), value); + if (argument === "--prior-manifest") options.priorManifest = value; + if (argument === "--review-by") options.reviewBy = value; + if (argument === "--root") options.root = path.resolve(process.cwd(), value); + if (argument === "--runner-result") options.runnerResults.push(value); + index += 1; + } + } else failures.push(finding("argument_unknown", `unsupported argument ${argument}`, "")); +} +for (const [name, value] of [["--root", options.root], ["--artifact-root", options.artifactRoot], ["--review-by", options.reviewBy]]) { + if (!value) failures.push(finding("argument_value_required", `${name} is required`, "")); +} +if (options.runnerResults.length === 0) failures.push(finding("argument_value_required", "at least one --runner-result is required", "")); + +let schemas; +try { schemas = compilePageEvidenceSchemas(path.join(styleGalleryRoot, "consumer-reference/schema")); } +catch (error) { failures.push(finding("page_evidence_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema")); } + +let artifactReference; +let artifactFiles = []; +if (options.root && options.artifactRoot) { + artifactReference = normalizeReference(options.root, options.artifactRoot); + if (!artifactReference) failures.push(finding("page_evidence_artifact_escape", "artifact root must be inside the consumer repository", options.artifactRoot)); + else if (fs.existsSync(options.artifactRoot) && fs.lstatSync(options.artifactRoot).isSymbolicLink()) failures.push(finding("page_evidence_artifact_redirect", "artifact root must not be a filesystem redirect", artifactReference)); + else { + const resolved = resolveContained({ expectedType: "directory", prefix: "page_evidence_artifact", reference: artifactReference, root: options.root }, failures); + if (resolved) artifactFiles = listArtifactFiles(options.artifactRoot, failures); + } +} +const packetWithinLimits = !failures.some((entry) => entry.code === "page_evidence_artifact_limit"); +const manifestFile = options.artifactRoot ? path.join(options.artifactRoot, PAGE_EVIDENCE_MANIFEST) : undefined; +if (manifestFile && fs.existsSync(manifestFile)) failures.push(finding("page_evidence_session_replay", "completed manifest already exists and cannot be overwritten", PAGE_EVIDENCE_MANIFEST)); +const receiptRecord = options.artifactRoot && schemas && packetWithinLimits ? readReceipt(options.artifactRoot, schemas, failures) : undefined; +const receipt = receiptRecord?.valid ? receiptRecord : undefined; + +const git = options.root ? gitIdentity(options.root, failures) : undefined; +if (receipt && git) { + if (git.revision !== receipt.value.revision) failures.push(finding("page_evidence_revision_mismatch", "checked-out revision differs from the start receipt", PAGE_EVIDENCE_RECEIPT)); + if (git.repository && git.repository !== receipt.value.repository) failures.push(finding("page_evidence_repository_mismatch", "Git remote identity differs from the start receipt", PAGE_EVIDENCE_RECEIPT)); +} + +if (receipt && options.root) { + const sourcePaths = receipt.value.source.files.map((entry) => entry.path); + const currentSource = canonicalSourceManifest(options.root, sourcePaths, failures); + if (currentSource && !sameJson(currentSource, receipt.value.source)) failures.push(finding("page_evidence_source_drift", "relevant source bytes differ from the start receipt", PAGE_EVIDENCE_RECEIPT)); + if (dirtyRelevantSources(options.root, sourcePaths).length > 0) failures.push(finding("page_evidence_source_drift", "relevant sources became dirty after session start", PAGE_EVIDENCE_RECEIPT)); + const conformance = readJsonFile({ prefix: "page_evidence_conformance", reference: receipt.value.conformance_record.path, root: options.root }, failures); + if (conformance && !sameJson(metadata(conformance.bytes, "application/json", conformance.reference), receipt.value.conformance_record)) failures.push(finding("page_evidence_conformance_drift", "conformance record differs from the start receipt", conformance.reference)); +} + +let prior; +if (options.priorManifest && options.root && schemas) { + const reference = normalizeReference(options.root, options.priorManifest); + if (!reference) failures.push(finding("page_evidence_reuse_manifest_escape", "prior manifest must be inside the consumer repository", options.priorManifest)); + else { + prior = readJsonFile({ prefix: "page_evidence_reuse_manifest", reference, root: options.root }, failures); + if (prior) { + prior.sha256 = metadata(prior.bytes, "application/json", reference).sha256; + prior.valid = addSchemaFindings(schemas.manifest, prior.value, reference, "page_evidence_reuse_manifest_invalid", failures); + } + } +} + +const runnerReferences = options.artifactRoot ? options.runnerResults.map((candidate) => normalizeReference(options.artifactRoot, candidate)) : []; +if (runnerReferences.some((reference) => !reference) || new Set(runnerReferences).size !== runnerReferences.length) failures.push(finding("page_evidence_runner_path_invalid", "runner results must be unique normalized paths inside artifact root", "")); +const completedAt = new Date().toISOString(); +const scenarios = []; +const controlFiles = new Set([PAGE_EVIDENCE_MANIFEST, PAGE_EVIDENCE_RECEIPT, ...runnerReferences.filter(Boolean)]); +const expectedFiles = new Set([...controlFiles].filter((reference) => reference !== PAGE_EVIDENCE_MANIFEST)); +const artifactOwners = new Map(); +let canonicalRun; + +for (const reference of packetWithinLimits ? runnerReferences.filter(Boolean) : []) { + const runner = readJsonFile({ prefix: "page_evidence_runner", reference, root: options.artifactRoot }, failures); + if (!runner || !schemas) continue; + if (!addSchemaFindings(schemas.runner, runner.value, reference, "page_evidence_runner_schema_invalid", failures)) continue; + const value = runner.value; + if (value.status !== "passed") failures.push(finding("page_evidence_runner_failed", `${value.scenario_id ?? reference} runner did not pass`, reference)); + if (receipt) { + if (value.session_id !== receipt.value.session_id || value.nonce !== receipt.value.nonce || value.receipt_sha256 !== receipt.digest) failures.push(finding("page_evidence_session_mismatch", `${value.scenario_id ?? reference} runner session differs from the receipt`, reference)); + if (value.revision !== receipt.value.revision || value.run?.revision !== receipt.value.revision) failures.push(finding("page_evidence_revision_mismatch", `${value.scenario_id ?? reference} runner revision differs from the receipt`, reference)); + if (value.repository !== receipt.value.repository || value.run?.repository !== receipt.value.repository) failures.push(finding("page_evidence_repository_mismatch", `${value.scenario_id ?? reference} runner repository differs from the receipt`, reference)); + if (value.source_sha256 !== receipt.value.source.sha256) failures.push(finding("page_evidence_source_mismatch", `${value.scenario_id ?? reference} runner source differs from the receipt`, reference)); + if (value.run?.id !== receipt.value.run_id || value.run?.attempt !== receipt.value.attempt) failures.push(finding("page_evidence_run_mismatch", `${value.scenario_id ?? reference} runner identity differs from the receipt`, reference)); + if (!receipt.value.intended_scenario_ids.includes(value.scenario_id)) failures.push(finding("page_evidence_scenario_unintended", `${value.scenario_id ?? reference} was not declared at session start`, reference)); + if (!withinSession(value.recorded_at, receipt.value.started_at, completedAt)) failures.push(finding("page_evidence_capture_outside_session", `${value.scenario_id ?? reference} was recorded outside the session interval`, reference)); + } + const run = value.run ? { ...value.run, status: "passed" } : undefined; + if (canonicalRun && !sameJson(canonicalRun, run)) failures.push(finding("page_evidence_run_mismatch", "runner results mix run identities", reference)); + else if (!canonicalRun) canonicalRun = run; + let evidence; + if (value.evidence?.kind === "captured") { + const artifacts = []; + for (const declaration of value.evidence.artifacts ?? []) { + if (controlFiles.has(declaration.path)) { + failures.push(finding("page_evidence_artifact_control_collision", "captured artifacts must be disjoint from manifest, receipt, and runner-result controls", declaration.path)); + continue; + } + const owner = artifactOwners.get(declaration.path); + if (owner) failures.push(finding("page_evidence_artifact_reused", `${declaration.path} is shared by ${owner} and ${value.scenario_id}`, reference)); + else artifactOwners.set(declaration.path, value.scenario_id); + const resolved = readContainedBytes({ prefix: "page_evidence_artifact", reference: declaration.path, root: options.artifactRoot }, failures); + if (!resolved) continue; + expectedFiles.add(declaration.path); + const artifact = metadata(resolved.bytes, declaration.media_type, declaration.path); + if (declaration.media_type === "image/png" && (!artifact.width || !artifact.height)) failures.push(finding("page_evidence_png_invalid", "captured PNG must contain a valid IHDR/IDAT/IEND chunk stream", declaration.path)); + artifacts.push(artifact); + } + evidence = { artifacts, kind: "captured" }; + } else if (value.evidence?.kind === "reused") { + evidence = value.evidence; + if (!prior?.valid) failures.push(finding("page_evidence_reuse_manifest_required", "reused evidence requires a valid --prior-manifest", reference)); + else { + const priorScenario = prior.value.scenarios?.find((scenario) => scenario.id === value.evidence.prior_scenario_id); + if (value.evidence.prior_manifest_sha256 !== prior.sha256) failures.push(finding("page_evidence_reuse_manifest_mismatch", "prior manifest digest does not match", reference)); + if (value.evidence.prior_session_id !== prior.value.session?.session_id || value.evidence.prior_run_id !== prior.value.run?.id) failures.push(finding("page_evidence_reuse_identity_mismatch", "prior session or run identity does not match", reference)); + if (!priorScenario || value.evidence.prior_scenario_id !== value.scenario_id || priorScenario.evidence?.kind !== "captured") failures.push(finding("page_evidence_reuse_scenario_mismatch", "reuse must select the same captured scenario", reference)); + if (receipt && !sameJson(prior.value.session?.source, receipt.value.source)) failures.push(finding("page_evidence_reuse_source_mismatch", "reuse requires byte-identical relevant sources", reference)); + if (priorScenario && !sameJson(priorScenario.semantic_environment, value.semantic_environment)) failures.push(finding("page_evidence_reuse_environment_mismatch", "reuse requires the same semantic environment", reference)); + } + } + scenarios.push({ evidence, id: value.scenario_id, recorded_at: value.recorded_at, runner_result: metadata(runner.bytes, "application/json", reference), semantic_environment: value.semantic_environment }); +} + +if (receipt) { + const passedIds = scenarios.map((scenario) => scenario.id); + if (!sameStringSet(passedIds, receipt.value.intended_scenario_ids)) failures.push(finding("page_evidence_pass_membership_invalid", "PASS scenarios must exactly match the start intent", PAGE_EVIDENCE_MANIFEST)); +} +if (packetWithinLimits && options.artifactRoot && fs.existsSync(options.artifactRoot)) { + for (const reference of artifactFiles) if (!expectedFiles.has(reference)) failures.push(finding("page_evidence_artifact_unmanifested", "artifact root contains a file outside the closed manifest", reference)); +} + +const completedSession = receipt ? { + ...receipt.value, + completed_at: completedAt, + receipt: receipt.artifact, + receipt_sha256: receipt.digest, +} : undefined; +delete completedSession?.record_kind; +delete completedSession?.schema_version; +const manifest = receipt && canonicalRun ? { + claim_boundary: PAGE_EVIDENCE_CLAIM, + completed_at: completedAt, + record_kind: "page_evidence_manifest", + repository: receipt.value.repository, + review_by: options.reviewBy, + revision: receipt.value.revision, + run: canonicalRun, + scenarios, + schema_version: "1.0", + session: completedSession, +} : undefined; +if (manifest && schemas) addSchemaFindings(schemas.manifest, manifest, PAGE_EVIDENCE_MANIFEST, "page_evidence_manifest_schema_invalid", failures); +if (failures.length === 0 && manifestFile) fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" }); +const result = { artifactCount: artifactOwners.size, failures, manifest: manifestFile, ok: failures.length === 0, scenarioCount: scenarios.length, sessionId: receipt?.value.session_id }; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`finalized ${result.scenarioCount} page evidence scenarios\n`); +else process.stderr.write(`${failures.map((issue) => `${issue.code}: ${issue.path}: ${issue.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; diff --git a/scripts/page-artifact-metadata.mjs b/scripts/page-artifact-metadata.mjs new file mode 100644 index 0000000..21b0b83 --- /dev/null +++ b/scripts/page-artifact-metadata.mjs @@ -0,0 +1,124 @@ +import crypto from "node:crypto"; +import { inflateSync } from "node:zlib"; + +const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); +const pngBitDepths = new Map([ + [0, new Set([1, 2, 4, 8, 16])], + [2, new Set([8, 16])], + [3, new Set([1, 2, 4, 8])], + [4, new Set([8, 16])], + [6, new Set([8, 16])], +]); +const pngChannels = new Map([[0, 1], [2, 3], [3, 1], [4, 2], [6, 4]]); +const adam7Passes = Object.freeze([ + [0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], + [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2], +]); +const maxDecodedPngBytes = 256 * 1024 * 1024; +export const MAX_ARTIFACT_FILE_BYTES = 64 * 1024 * 1024; +const maxPngChunkBytes = 32 * 1024 * 1024; +const maxPngCompressedBytes = 64 * 1024 * 1024; +const crcTable = Uint32Array.from({ length: 256 }, (_, index) => { + let value = index; + for (let bit = 0; bit < 8; bit += 1) value = (value >>> 1) ^ (0xedb88320 & -(value & 1)); + return value >>> 0; +}); + +function crc32(bytes) { + let value = 0xffffffff; + for (const byte of bytes) value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8); + return (value ^ 0xffffffff) >>> 0; +} + +function passExtent(size, start, step) { + return size <= start ? 0 : Math.ceil((size - start) / step); +} + +function validateDecodedPng(idat, { bitDepth, colorType, height, interlace, width }) { + const bitsPerPixel = pngChannels.get(colorType) * bitDepth; + const passes = interlace === 0 ? [[0, 0, 1, 1]] : adam7Passes; + const rows = passes.flatMap(([startX, startY, stepX, stepY]) => { + const passWidth = passExtent(width, startX, stepX); + const passHeight = passExtent(height, startY, stepY); + return passWidth === 0 || passHeight === 0 ? [] : [{ bytes: Math.ceil((passWidth * bitsPerPixel) / 8), count: passHeight }]; + }); + const expected = rows.reduce((total, row) => total + (row.bytes + 1) * row.count, 0); + if (!Number.isSafeInteger(expected) || expected <= 0 || expected > maxDecodedPngBytes) return false; + const compressed = Buffer.concat(idat); + let decoded; + let consumed; + try { + const inflated = inflateSync(compressed, { info: true, maxOutputLength: expected + 1 }); + decoded = inflated.buffer; + consumed = inflated.engine.bytesWritten; + } + catch { return false; } + if (consumed !== compressed.length || decoded.length !== expected) return false; + let offset = 0; + for (const row of rows) { + for (let index = 0; index < row.count; index += 1) { + if (decoded[offset] > 4) return false; + offset += row.bytes + 1; + } + } + return offset === decoded.length; +} + +function pngDimensions(bytes) { + if (bytes.length > MAX_ARTIFACT_FILE_BYTES) return undefined; + if (bytes.length < 8 || !bytes.subarray(0, 8).equals(pngSignature)) return undefined; + let dimensions; + let idatClosed = false; + let sawIdat = false; + const idat = []; + let compressedBytes = 0; + let header; + let offset = 8; + while (offset < bytes.length) { + if (bytes.length - offset < 12) return undefined; + const length = bytes.readUInt32BE(offset); + if (length > maxPngChunkBytes) return undefined; + const typeOffset = offset + 4; + const dataOffset = typeOffset + 4; + const crcOffset = dataOffset + length; + const nextOffset = crcOffset + 4; + if (nextOffset > bytes.length) return undefined; + const typeBytes = bytes.subarray(typeOffset, dataOffset); + if ([...typeBytes].some((byte) => !((byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)))) return undefined; + if (crc32(bytes.subarray(typeOffset, crcOffset)) !== bytes.readUInt32BE(crcOffset)) return undefined; + const type = typeBytes.toString("ascii"); + if (offset === 8 && type !== "IHDR") return undefined; + if (type === "IHDR") { + if (offset !== 8 || length !== 13 || dimensions) return undefined; + const width = bytes.readUInt32BE(dataOffset); + const height = bytes.readUInt32BE(dataOffset + 4); + const bitDepth = bytes[dataOffset + 8]; + const colorType = bytes[dataOffset + 9]; + const bitDepths = pngBitDepths.get(colorType); + if (width === 0 || width > 0x7fffffff || height === 0 || height > 0x7fffffff || !bitDepths?.has(bitDepth)) return undefined; + if (bytes[dataOffset + 10] !== 0 || bytes[dataOffset + 11] !== 0 || bytes[dataOffset + 12] > 1) return undefined; + header = { bitDepth, colorType, height, interlace: bytes[dataOffset + 12], width }; + dimensions = { height, width }; + } else if (type === "IDAT") { + if (!dimensions || idatClosed) return undefined; + sawIdat = true; + idat.push(bytes.subarray(dataOffset, crcOffset)); + compressedBytes += length; + if (compressedBytes > maxPngCompressedBytes) return undefined; + } else if (type === "IEND") { + return length === 0 && nextOffset === bytes.length && dimensions && sawIdat && validateDecodedPng(idat, header) ? dimensions : undefined; + } else if (sawIdat) idatClosed = true; + offset = nextOffset; + } + return undefined; +} + +export function artifactMetadata(bytes, mediaType) { + const metadata = { + byte_length: bytes.length, + media_type: mediaType, + sha256: `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`, + }; + if (mediaType === "image/png") Object.assign(metadata, pngDimensions(bytes)); + return metadata; +} diff --git a/scripts/page-evidence-contract.mjs b/scripts/page-evidence-contract.mjs new file mode 100644 index 0000000..f182fba --- /dev/null +++ b/scripts/page-evidence-contract.mjs @@ -0,0 +1,297 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { isDeepStrictEqual } from "node:util"; +import Ajv2020 from "ajv/dist/2020.js"; +import { artifactMetadata, MAX_ARTIFACT_FILE_BYTES } from "./page-artifact-metadata.mjs"; +import { addDateTimeFormat } from "./json-schema-formats.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +export const PAGE_EVIDENCE_CLAIM = "Page evidence is claim-scoped runtime evidence, not visual-regression approval or accessibility certification."; +export const PAGE_EVIDENCE_MANIFEST = "page-evidence-manifest.json"; +export const PAGE_EVIDENCE_RECEIPT = "page-evidence-session.json"; +export const PAGE_EVIDENCE_MAX_PACKET_BYTES = 256 * 1024 * 1024; + +export function finding(code, message, recordPath) { + return { code, message, path: recordPath }; +} + +export function digest(bytes) { + return crypto.createHash("sha256").update(bytes).digest("hex"); +} + +export function metadata(bytes, mediaType, reference) { + const result = artifactMetadata(bytes, mediaType); + return { ...result, path: reference, sha256: result.sha256.replace(/^sha256:/, "") }; +} + +export function sameJson(left, right) { + return isDeepStrictEqual(left, right); +} + +export function sameStringSet(left, right) { + return Array.isArray(left) && Array.isArray(right) && left.length === right.length && new Set(left).size === left.length && left.every((entry) => right.includes(entry)); +} + +export function safeRelativePath(reference) { + return typeof reference === "string" + && reference.length > 0 + && !path.posix.isAbsolute(reference) + && !path.win32.isAbsolute(reference) + && !reference.includes("\\") + && !reference.includes("?") + && !reference.includes("#") + && !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference) + && path.posix.normalize(reference) === reference + && !reference.split("/").some((segment) => segment === "." || segment === ".."); +} + +function isInside(root, target) { + const relative = path.relative(root, target); + return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); +} + +export function normalizeReference(root, candidate) { + const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(root, candidate); + const reference = path.relative(path.resolve(root), absolute).split(path.sep).join("/"); + return safeRelativePath(reference) ? reference : undefined; +} + +export function resolveContained({ allowMissing = false, expectedType = "file", prefix, reference, root }, failures) { + const absoluteRoot = path.resolve(root); + if (!fs.existsSync(absoluteRoot) || !fs.lstatSync(absoluteRoot).isDirectory()) { + failures.push(finding(`${prefix}_root_invalid`, "trust root must be an existing directory", absoluteRoot)); + return undefined; + } + if (fs.lstatSync(absoluteRoot).isSymbolicLink() || fs.realpathSync(absoluteRoot) !== absoluteRoot) { + failures.push(finding(`${prefix}_redirect`, "trust root must not be a filesystem redirect", absoluteRoot)); + return undefined; + } + if (!safeRelativePath(reference)) { + failures.push(finding(`${prefix}_escape`, "path must be normalized and repository-relative", String(reference))); + return undefined; + } + const target = path.resolve(absoluteRoot, reference); + if (!isInside(absoluteRoot, target)) { + failures.push(finding(`${prefix}_escape`, "path escapes its trust root", reference)); + return undefined; + } + let current = absoluteRoot; + const segments = reference.split("/"); + for (const [index, segment] of segments.entries()) { + current = path.join(current, segment); + if (!fs.existsSync(current)) { + if (allowMissing) return { file: target, reference }; + failures.push(finding(`${prefix}_missing`, "required file is missing", reference)); + return undefined; + } + const stat = fs.lstatSync(current); + if (stat.isSymbolicLink()) { + failures.push(finding(`${prefix}_symlink`, "path must not traverse a symbolic link", reference)); + return undefined; + } + const final = index === segments.length - 1; + if ((!final && !stat.isDirectory()) || (final && expectedType === "file" && !stat.isFile()) || (final && expectedType === "directory" && !stat.isDirectory())) { + failures.push(finding(`${prefix}_type_invalid`, `path must resolve to a regular ${expectedType}`, reference)); + return undefined; + } + } + const canonical = path.join(fs.realpathSync(absoluteRoot), ...segments); + if (fs.realpathSync(target) !== canonical) { + failures.push(finding(`${prefix}_redirect`, "path resolves through a filesystem redirect", reference)); + return undefined; + } + return { file: target, reference }; +} + +export function readJsonFile({ prefix, reference, root }, failures) { + const resolved = readContainedBytes({ prefix, reference, root }, failures); + if (!resolved) return undefined; + try { + return { ...resolved, value: parseStrictJson(resolved.bytes.toString("utf8")) }; + } catch (error) { + failures.push(finding(`${prefix}_json_invalid`, error instanceof Error ? error.message : String(error), reference)); + return undefined; + } +} + +export function readContainedBytes({ prefix, reference, root }, failures) { + const resolved = resolveContained({ expectedType: "file", prefix, reference, root }, failures); + if (!resolved) return undefined; + let descriptor; + try { + descriptor = fs.openSync(resolved.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); + const before = fs.fstatSync(descriptor); + const namedBefore = fs.lstatSync(resolved.file); + if (!before.isFile() || before.dev !== namedBefore.dev || before.ino !== namedBefore.ino) throw new Error("file identity changed before read"); + if (before.size > MAX_ARTIFACT_FILE_BYTES) throw new Error(`file exceeds ${MAX_ARTIFACT_FILE_BYTES} bytes`); + const bytes = fs.readFileSync(descriptor); + const after = fs.fstatSync(descriptor); + const namedAfter = fs.lstatSync(resolved.file); + if (before.dev !== after.dev || before.ino !== after.ino || after.dev !== namedAfter.dev || after.ino !== namedAfter.ino || after.size !== bytes.length) throw new Error("file identity changed during read"); + const expected = path.join(fs.realpathSync(path.resolve(root)), ...reference.split("/")); + if (fs.realpathSync(resolved.file) !== expected) throw new Error("file path redirected during read"); + return { ...resolved, bytes }; + } catch (error) { + failures.push(finding(`${prefix}_read_untrusted`, error instanceof Error ? error.message : String(error), reference)); + return undefined; + } finally { + if (descriptor !== undefined) fs.closeSync(descriptor); + } +} + +function readSchema(schemaRoot, name) { + const file = path.join(schemaRoot, name); + try { + return parseStrictJson(fs.readFileSync(file, "utf8")); + } catch (cause) { + throw new Error(`${name} is not strict JSON: ${cause instanceof Error ? cause.message : String(cause)}`, { cause }); + } +} + +export function compilePageEvidenceSchemas(schemaRoot) { + const ajv = new Ajv2020({ allErrors: true, strict: true }); + addDateTimeFormat(ajv); + const sessionSchema = readSchema(schemaRoot, "page-evidence-session.schema.json"); + const manifestSchema = readSchema(schemaRoot, "page-evidence-manifest.schema.json"); + ajv.addSchema(sessionSchema); + ajv.addSchema(manifestSchema); + return { + manifest: ajv.getSchema(manifestSchema.$id), + runner: ajv.compile({ $ref: `${manifestSchema.$id}#/$defs/runner_result` }), + session: ajv.getSchema(sessionSchema.$id), + }; +} + +export function addSchemaFindings(validate, value, recordPath, code, failures) { + if (validate(value)) return true; + for (const error of validate.errors ?? []) failures.push(finding(code, `${error.instancePath || "/"} ${error.message}`, recordPath)); + return false; +} + +export function canonicalSourceManifest(root, references, failures) { + if (!Array.isArray(references) || references.length === 0 || new Set(references).size !== references.length) { + failures.push(finding("page_evidence_source_list_invalid", "relevant sources must be a nonempty unique path list", "")); + return undefined; + } + const files = []; + let totalBytes = 0; + for (const reference of [...references].sort()) { + const resolved = readContainedBytes({ prefix: "page_evidence_source", reference, root }, failures); + if (!resolved) continue; + const size = resolved.bytes.length; + totalBytes += size; + if (size > MAX_ARTIFACT_FILE_BYTES || totalBytes > PAGE_EVIDENCE_MAX_PACKET_BYTES) { + failures.push(finding("page_evidence_source_limit", "relevant sources exceed the bounded file or aggregate byte budget", reference)); + continue; + } + const bytes = resolved.bytes; + files.push({ byte_length: bytes.length, path: reference, sha256: digest(bytes) }); + } + return files.length === references.length ? { files, sha256: digest(Buffer.from(JSON.stringify(files))) } : undefined; +} + +function git(root, ...args) { + return execFileSync("git", ["-c", `safe.directory=${path.resolve(root)}`, ...args], { cwd: root, encoding: "utf8" }).trim(); +} + +function remoteIdentity(remote) { + if (!remote) return undefined; + const cleaned = remote.replace(/\.git$/, ""); + const scp = cleaned.match(/^[^@]+@[^:]+:(.+)$/); + if (scp) return scp[1]; + try { + const parsed = new URL(cleaned); + return parsed.pathname.replace(/^\//, ""); + } catch { + return undefined; + } +} + +export function gitIdentity(root, failures) { + try { + const canonical = git(root, "rev-parse", "--show-toplevel"); + if (path.resolve(canonical) !== path.resolve(root) || fs.realpathSync(canonical) !== path.resolve(root)) throw new Error("--root must be the canonical Git worktree root"); + let remote; + try { remote = git(root, "config", "--get", "remote.origin.url"); } catch { remote = ""; } + return { + branch: git(root, "branch", "--show-current") || "detached", + repository: remoteIdentity(remote), + revision: git(root, "rev-parse", "HEAD"), + }; + } catch (error) { + failures.push(finding("page_evidence_git_invalid", error instanceof Error ? error.message : String(error), path.resolve(root))); + return undefined; + } +} + +export function dirtyRelevantSources(root, references) { + const dirty = []; + for (const reference of references) { + try { git(root, "ls-files", "--error-unmatch", "--", reference); } + catch { dirty.push(`untracked:${reference}`); } + } + const status = git(root, "status", "--porcelain=v1", "--untracked-files=all", "--", ...references); + return [...new Set([...dirty, ...status.split("\n").filter(Boolean)])].sort(); +} + +export function readReceipt(artifactRoot, schemas, failures) { + const record = readJsonFile({ prefix: "page_evidence_session", reference: PAGE_EVIDENCE_RECEIPT, root: artifactRoot }, failures); + if (!record) return undefined; + const valid = addSchemaFindings(schemas.session, record.value, record.reference, "page_evidence_session_schema_invalid", failures); + const artifact = metadata(record.bytes, "application/json", PAGE_EVIDENCE_RECEIPT); + return { ...record, artifact, digest: artifact.sha256, valid }; +} + +export function withinSession(recordedAt, startedAt, completedAt) { + const recorded = Date.parse(recordedAt); + const started = Date.parse(startedAt); + const completed = Date.parse(completedAt); + return Number.isFinite(recorded) && Number.isFinite(started) && Number.isFinite(completed) && recorded >= started && recorded <= completed; +} + +export function listArtifactFiles(root, failures) { + const files = []; + let visited = 0; + let limitReported = false; + let totalBytes = 0; + function visit(directory, prefix = "", depth = 0) { + if (depth > 16) { + failures.push(finding("page_evidence_artifact_limit", "artifact tree exceeds the maximum directory depth", prefix)); + return; + } + const handle = fs.opendirSync(directory); + try { + let entry; + while ((entry = handle.readSync()) !== null) { + if (limitReported) return; + const reference = prefix ? `${prefix}/${entry.name}` : entry.name; + visited += 1; + if (visited > 4096) { + if (!limitReported) failures.push(finding("page_evidence_artifact_limit", "artifact tree exceeds 4096 entries", reference)); + limitReported = true; + return; + } + const absolute = path.join(directory, entry.name); + const stat = fs.lstatSync(absolute); + if (stat.isSymbolicLink()) failures.push(finding("page_evidence_artifact_symlink", "artifact root must not contain symbolic links", reference)); + else if (stat.isDirectory()) visit(absolute, reference, depth + 1); + else if (stat.isFile()) { + totalBytes += stat.size; + if (stat.size > MAX_ARTIFACT_FILE_BYTES || totalBytes > PAGE_EVIDENCE_MAX_PACKET_BYTES) { + if (!limitReported) failures.push(finding("page_evidence_artifact_limit", "artifact packet exceeds the bounded file or aggregate byte budget", reference)); + limitReported = true; + return; + } + files.push(reference); + } + else failures.push(finding("page_evidence_artifact_type_invalid", "artifact root contains a non-regular entry", reference)); + } + } finally { + handle.closeSync(); + } + } + visit(root); + return files.sort(); +} diff --git a/scripts/page-evidence-fixture.mjs b/scripts/page-evidence-fixture.mjs new file mode 100644 index 0000000..7afa8dd --- /dev/null +++ b/scripts/page-evidence-fixture.mjs @@ -0,0 +1,271 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync, spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { canonicalSourceManifest, digest, metadata } from "./page-evidence-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +export const creator = path.join(repositoryRoot, "scripts/create-page-evidence-session.mjs"); +export const finalizer = path.join(repositoryRoot, "scripts/finalize-page-evidence.mjs"); +export const validator = path.join(repositoryRoot, "scripts/validate-page-evidence.mjs"); +export const semanticEnvironment = Object.freeze({ + browser: "Chromium 140.0.0", + browser_revision: "chromium-1400", + color_scheme: "light", + locale: "en-US", + page_scale_factor: 1, + platform: "linux/amd64", + reduced_motion: "no-preference", + viewport: { height: 720, width: 320 }, +}); +const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64"); +const crcTable = Uint32Array.from({ length: 256 }, (_, index) => { + let value = index; + for (let bit = 0; bit < 8; bit += 1) value = (value >>> 1) ^ (0xedb88320 & -(value & 1)); + return value >>> 0; +}); + +function pngChunk(type, data) { + const name = Buffer.from(type, "ascii"); + const chunk = Buffer.alloc(12 + data.length); + chunk.writeUInt32BE(data.length, 0); + name.copy(chunk, 4); + data.copy(chunk, 8); + let crc = 0xffffffff; + for (const byte of chunk.subarray(4, 8 + data.length)) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8); + chunk.writeUInt32BE((crc ^ 0xffffffff) >>> 0, 8 + data.length); + return chunk; +} + +export function truncatedPng() { + return Buffer.from(png.subarray(0, 24)); +} + +export function corruptPngCrc() { + const bytes = Buffer.from(png); + bytes[bytes.length - 1] ^= 0xff; + return bytes; +} + +export function pngWithTrailingBytes() { + return Buffer.concat([png, Buffer.from("trailing bytes")]); +} + +export function pngWithInvalidImageData() { + return Buffer.concat([png.subarray(0, 33), pngChunk("IDAT", Buffer.alloc(0)), png.subarray(png.length - 12)]); +} + +export function pngWithTrailingImageData() { + const length = png.readUInt32BE(33); + const data = png.subarray(41, 41 + length); + return Buffer.concat([png.subarray(0, 33), pngChunk("IDAT", Buffer.concat([data, Buffer.from("trailing")])), png.subarray(45 + length)]); +} + +function git(root, ...args) { + return execFileSync("git", ["-c", `safe.directory=${root}`, ...args], { cwd: root, encoding: "utf8" }).trim(); +} + +export function writeJson(file, value) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +export function runCli(file, args, root, env = {}) { + const fixtureEnv = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GITHUB_"))); + const child = spawnSync(process.execPath, [file, ...args, "--json"], { cwd: repositoryRoot, encoding: "utf8", env: { ...fixtureEnv, ...env } }); + let report = { failures: [{ code: "child_output_invalid", message: child.stdout || child.stderr, path: file }], ok: false }; + try { report = JSON.parse(child.stdout); } catch (error) { if (!(error instanceof SyntaxError)) throw error; } + return { report, root, status: child.status }; +} + +export function initializeConsumer() { + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-page-evidence-"))); + execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "ignore" }); + git(root, "config", "user.email", "fixture@example.com"); + git(root, "config", "user.name", "StyleGallery fixture"); + git(root, "remote", "add", "origin", "https://github.com/example/consumer.git"); + fs.mkdirSync(path.join(root, "src"), { recursive: true }); + fs.mkdirSync(path.join(root, "tests"), { recursive: true }); + fs.writeFileSync(path.join(root, "src/page.js"), "export const page = 'ready';\n"); + fs.writeFileSync(path.join(root, "tests/page.spec.js"), "const scenario = 'responsive-layout';\nvoid scenario;\n"); + git(root, "add", "src/page.js", "tests/page.spec.js"); + git(root, "commit", "-m", "fixture sources"); + return { + relevantSources: ["src/page.js", "tests/page.spec.js"], + repository: "example/consumer", + revision: git(root, "rev-parse", "HEAD"), + root, + }; +} + +export function createConformance(fixture, { artifactName = "session-a", recordName = artifactName, runId = `run-${artifactName}`, scenarioId = "responsive-layout", sessionId = `session-${artifactName}` } = {}) { + const sourceFailures = []; + const source = canonicalSourceManifest(fixture.root, fixture.relevantSources, sourceFailures); + if (!source || sourceFailures.length > 0) throw new Error(`fixture source manifest failed: ${JSON.stringify(sourceFailures)}`); + const artifactRoot = path.join(fixture.root, "evidence", artifactName); + const recordFile = path.join(fixture.root, "records", `${recordName}.json`); + writeJson(recordFile, { + claim_boundary: "Synthetic conformance fixture; not product certification.", + consumer: { relevant_sources: fixture.relevantSources, repository: fixture.repository, revision: fixture.revision }, + id: `${recordName}-conformance`, + page_evidence: { manifest: path.relative(fixture.root, path.join(artifactRoot, "page-evidence-manifest.json")), status: "applicable" }, + record_kind: "consumer_migration_conformance", + scenarios: [{ + assertions: ["No document overflow."], + argv: ["node", "tests/page.spec.js"], + evidence_method: "browser", + exit_code: 0, + id: scenarioId, + observable_actions: ["Render the page."], + result_artifact: path.relative(fixture.root, path.join(artifactRoot, "runner", `${scenarioId}.json`)), + run_id: runId, + session_id: sessionId, + source_digest: source.sha256, + }], + schema_version: "1.0", + }); + return { artifactRoot, recordFile, runId, scenarioId, sessionId, source }; +} + +export function startSession(fixture, session, env = {}) { + return runCli(creator, ["--root", fixture.root, "--record", session.recordFile, "--artifact-root", session.artifactRoot], fixture.root, env); +} + +export function receiptFor(session) { + const file = path.join(session.artifactRoot, "page-evidence-session.json"); + const bytes = fs.readFileSync(file); + return { bytes, file, sha256: digest(bytes), value: JSON.parse(bytes) }; +} + +export function writeRunner(fixture, session, overrides = {}) { + const receipt = receiptFor(session); + const scenarioId = overrides.scenarioId ?? session.scenarioId; + const runnerReference = `runner/${scenarioId}.json`; + const artifactReference = overrides.artifactPath ?? `captures/${scenarioId}-320.png`; + if (overrides.writeArtifact !== false) { + const artifactFile = path.join(session.artifactRoot, artifactReference); + fs.mkdirSync(path.dirname(artifactFile), { recursive: true }); + fs.writeFileSync(artifactFile, overrides.artifactBytes ?? png); + } + const evidence = overrides.evidence ?? (overrides.status === "failed" + ? { kind: "failed", reason: "synthetic runner failure" } + : { artifacts: [{ media_type: "image/png", path: artifactReference }], kind: "captured" }); + const runner = { + evidence, + nonce: overrides.nonce ?? receipt.value.nonce, + receipt_sha256: overrides.receiptSha256 ?? receipt.sha256, + recorded_at: overrides.recordedAt ?? receipt.value.started_at, + record_kind: "page_evidence_runner_result", + repository: overrides.repository ?? fixture.repository, + revision: overrides.revision ?? fixture.revision, + run: { + attempt: receipt.value.attempt, + id: overrides.runId ?? session.runId, + repository: overrides.runRepository ?? fixture.repository, + revision: overrides.runRevision ?? fixture.revision, + source: "local", + }, + scenario_id: scenarioId, + schema_version: "1.0", + semantic_environment: overrides.semanticEnvironment ?? semanticEnvironment, + session_id: overrides.sessionId ?? session.sessionId, + source_sha256: overrides.sourceSha256 ?? receipt.value.source.sha256, + status: overrides.status ?? "passed", + }; + const runnerFile = path.join(session.artifactRoot, runnerReference); + writeJson(runnerFile, runner); + return { artifactReference, runner, runnerFile, runnerReference }; +} + +export function declareCapturedArtifact(session, runnerRecord, artifactReference) { + if (runnerRecord.artifactReference !== artifactReference) { + fs.rmSync(path.join(session.artifactRoot, runnerRecord.artifactReference), { force: true }); + } + const runner = { + ...runnerRecord.runner, + evidence: { artifacts: [{ media_type: "application/json", path: artifactReference }], kind: "captured" }, + }; + writeJson(runnerRecord.runnerFile, runner); + return { ...runnerRecord, artifactReference, runner }; +} + +export function rewriteCompletedCapture(session, runnerRecord, artifactReference) { + const declared = declareCapturedArtifact(session, runnerRecord, artifactReference); + const manifestFile = path.join(session.artifactRoot, "page-evidence-manifest.json"); + const manifest = JSON.parse(fs.readFileSync(manifestFile)); + const runnerResult = metadata(fs.readFileSync(declared.runnerFile), "application/json", declared.runnerReference); + const artifact = metadata(fs.readFileSync(path.join(session.artifactRoot, artifactReference)), "application/json", artifactReference); + const [scenario, ...remaining] = manifest.scenarios; + writeJson(manifestFile, { + ...manifest, + scenarios: [{ ...scenario, evidence: { artifacts: [artifact], kind: "captured" }, runner_result: runnerResult }, ...remaining], + }); +} + +export function rewriteCompletedArtifact(session, runnerRecord, bytes) { + const artifactFile = path.join(session.artifactRoot, runnerRecord.artifactReference); + fs.writeFileSync(artifactFile, bytes); + const artifact = { ...metadata(bytes, "image/png", runnerRecord.artifactReference), height: 1, width: 1 }; + const manifestFile = path.join(session.artifactRoot, "page-evidence-manifest.json"); + const manifest = JSON.parse(fs.readFileSync(manifestFile)); + const runnerResult = metadata(fs.readFileSync(runnerRecord.runnerFile), "application/json", runnerRecord.runnerReference); + const [scenario, ...remaining] = manifest.scenarios; + writeJson(manifestFile, { + ...manifest, + scenarios: [{ ...scenario, evidence: { artifacts: [artifact], kind: "captured" }, runner_result: runnerResult }, ...remaining], + }); +} + +export function finalizeSession(fixture, session, runner, priorManifest) { + const args = ["--root", fixture.root, "--artifact-root", session.artifactRoot, "--runner-result", runner.runnerReference, "--review-by", "2030-01-01T00:00:00Z"]; + if (priorManifest) args.push("--prior-manifest", priorManifest); + return runCli(finalizer, args, fixture.root); +} + +export function validateSession(fixture, session, priorManifest) { + const args = ["--root", fixture.root, "--artifact-root", session.artifactRoot]; + if (priorManifest) args.push("--prior-manifest", priorManifest); + return runCli(validator, args, fixture.root); +} + +export function completeSession(fixture, session = createConformance(fixture)) { + const start = startSession(fixture, session); + if (start.status !== 0) return { fixture, session, start }; + const runner = writeRunner(fixture, session); + const finalize = finalizeSession(fixture, session, runner); + const validate = finalize.status === 0 ? validateSession(fixture, session) : undefined; + return { finalize, fixture, runner, session, start, validate }; +} + +export function isolated(run) { + const fixture = initializeConsumer(); + try { return run(fixture); } + finally { cleanupFixture(fixture); } +} + +export function started(fixture, options) { + const session = createConformance(fixture, options); + const start = startSession(fixture, session); + if (start.status !== 0) throw new Error(`fixture start failed: ${JSON.stringify(start.report)}`); + return session; +} + +export function reuseSetup(fixture, { beforeCurrent, currentOptions } = {}) { + const prior = completeSession(fixture, createConformance(fixture, { artifactName: "prior" })); + beforeCurrent?.(fixture); + const current = started(fixture, { artifactName: "current", ...currentOptions }); + const priorManifest = path.join(prior.session.artifactRoot, "page-evidence-manifest.json"); + const evidence = { + kind: "reused", + prior_manifest_sha256: digest(fs.readFileSync(priorManifest)), + prior_run_id: prior.session.runId, + prior_scenario_id: prior.session.scenarioId, + prior_session_id: prior.session.sessionId, + }; + return { current, evidence, prior, priorManifest }; +} + +export function cleanupFixture(fixture) { + fs.rmSync(fixture.root, { force: true, recursive: true }); +} diff --git a/scripts/test-validate-page-evidence.mjs b/scripts/test-validate-page-evidence.mjs new file mode 100644 index 0000000..b4d258c --- /dev/null +++ b/scripts/test-validate-page-evidence.mjs @@ -0,0 +1,302 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { MAX_ARTIFACT_FILE_BYTES } from "./page-artifact-metadata.mjs"; +import { canonicalSourceManifest } from "./page-evidence-contract.mjs"; +import { + completeSession, + corruptPngCrc, + createConformance, + declareCapturedArtifact, + finalizeSession, + isolated, + pngWithTrailingBytes, + pngWithInvalidImageData, + pngWithTrailingImageData, + receiptFor, + reuseSetup, + rewriteCompletedCapture, + rewriteCompletedArtifact, + semanticEnvironment, + startSession, + started, + truncatedPng, + validateSession, + writeJson, + writeRunner, +} from "./page-evidence-fixture.mjs"; + +const requested = []; +let json = false; +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") json = true; + else if (argument === "--case") { + const value = process.argv[index + 1]; + if (value) { requested.push(value); index += 1; } + } +} + +function codes(child) { + return child?.report?.failures?.map((failure) => failure.code) ?? []; +} + +function outcome(name, expected, child, observe = {}) { + const actualCodes = codes(child); + const valid = expected === "ok:true"; + const ok = valid ? child?.status === 0 && child.report.ok === true : child?.status !== 0 && actualCodes.includes(expected); + return { actual: { codes: actualCodes, status: child?.status, ...observe }, expected, name, ok }; +} + +const cases = { + "complete-session": () => isolated((fixture) => { + const completed = completeSession(fixture); + return outcome("complete-session", "ok:true", completed.validate, { + finalized: completed.finalize?.report.ok === true, + scenarioCount: completed.validate?.report.scenarioCount, + started: completed.start?.report.ok === true, + }); + }), + "dirty-relevant-source": () => isolated((fixture) => { + const session = createConformance(fixture); + fs.appendFileSync(path.join(fixture.root, fixture.relevantSources[0]), "// dirty\n"); + return outcome("dirty-relevant-source", "page_evidence_source_dirty", startSession(fixture, session)); + }), + "untracked-relevant-source": () => isolated((fixture) => { + const session = createConformance(fixture); + const untracked = "src/untracked.js"; + fs.writeFileSync(path.join(fixture.root, untracked), "export const untracked = true;\n"); + const record = JSON.parse(fs.readFileSync(session.recordFile)); + record.consumer.relevant_sources.push(untracked); + const sourceFailures = []; + const source = canonicalSourceManifest(fixture.root, record.consumer.relevant_sources, sourceFailures); + record.scenarios[0].source_digest = source.sha256; + writeJson(session.recordFile, record); + return outcome("untracked-relevant-source", "page_evidence_source_dirty", startSession(fixture, session)); + }), + "source-drift-after-start": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + fs.appendFileSync(path.join(fixture.root, fixture.relevantSources[0]), "// drift\n"); + return outcome("source-drift-after-start", "page_evidence_source_drift", finalizeSession(fixture, session, runner)); + }), + "cross-revision": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { revision: "0".repeat(40), runRevision: "0".repeat(40) }); + return outcome("cross-revision", "page_evidence_revision_mismatch", finalizeSession(fixture, session, runner)); + }), + "cross-run": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { runId: "different-run" }); + return outcome("cross-run", "page_evidence_run_mismatch", finalizeSession(fixture, session, runner)); + }), + "cross-session-replay": () => isolated((fixture) => { + const target = started(fixture, { artifactName: "target" }); + const alternate = started(fixture, { artifactName: "alternate" }); + const targetRunner = writeRunner(fixture, target); + const alternateRunner = writeRunner(fixture, alternate); + fs.copyFileSync(alternateRunner.runnerFile, targetRunner.runnerFile); + fs.copyFileSync(path.join(alternate.artifactRoot, alternateRunner.artifactReference), path.join(target.artifactRoot, targetRunner.artifactReference)); + return outcome("cross-session-replay", "page_evidence_session_mismatch", finalizeSession(fixture, target, targetRunner)); + }), + "failed-runner": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { status: "failed" }); + return outcome("failed-runner", "page_evidence_runner_failed", finalizeSession(fixture, session, runner)); + }), + "finalize-runner-control-collision": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + const declared = declareCapturedArtifact(session, runner, runner.runnerReference); + return outcome("finalize-runner-control-collision", "page_evidence_artifact_control_collision", finalizeSession(fixture, session, declared)); + }), + "finalize-receipt-control-collision": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + const declared = declareCapturedArtifact(session, runner, "page-evidence-session.json"); + return outcome("finalize-receipt-control-collision", "page_evidence_artifact_control_collision", finalizeSession(fixture, session, declared)); + }), + "unmanifested-png": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + fs.writeFileSync(path.join(session.artifactRoot, "rogue.png"), "rogue"); + return outcome("unmanifested-png", "page_evidence_artifact_unmanifested", finalizeSession(fixture, session, runner)); + }), + "artifact-tree-depth-limit": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + fs.mkdirSync(path.join(session.artifactRoot, ...Array.from({ length: 18 }, (_, index) => `depth-${index}`)), { recursive: true }); + return outcome("artifact-tree-depth-limit", "page_evidence_artifact_limit", finalizeSession(fixture, session, runner)); + }), + "artifact-entry-limit": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + for (let index = 0; index < 4097; index += 1) fs.writeFileSync(path.join(session.artifactRoot, `entry-${index}.txt`), ""); + return outcome("artifact-entry-limit", "page_evidence_artifact_limit", finalizeSession(fixture, session, runner)); + }), + "artifact-file-byte-limit": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + fs.truncateSync(path.join(session.artifactRoot, runner.artifactReference), MAX_ARTIFACT_FILE_BYTES + 1); + return outcome("artifact-file-byte-limit", "page_evidence_artifact_limit", finalizeSession(fixture, session, runner)); + }), + "artifact-packet-byte-limit": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + for (let index = 0; index < 5; index += 1) { + const file = path.join(session.artifactRoot, `packet-${index}.bin`); + fs.closeSync(fs.openSync(file, "w")); + fs.truncateSync(file, 60 * 1024 * 1024); + } + return outcome("artifact-packet-byte-limit", "page_evidence_artifact_limit", finalizeSession(fixture, session, runner)); + }), + "missing-artifact": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + fs.rmSync(path.join(session.artifactRoot, runner.artifactReference)); + return outcome("missing-artifact", "page_evidence_artifact_missing", finalizeSession(fixture, session, runner)); + }), + "truncated-png": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { artifactBytes: truncatedPng() }); + return outcome("truncated-png", "page_evidence_png_invalid", finalizeSession(fixture, session, runner)); + }), + "corrupt-png-crc": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { artifactBytes: corruptPngCrc() }); + return outcome("corrupt-png-crc", "page_evidence_png_invalid", finalizeSession(fixture, session, runner)); + }), + "trailing-png-bytes": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { artifactBytes: pngWithTrailingBytes() }); + return outcome("trailing-png-bytes", "page_evidence_png_invalid", finalizeSession(fixture, session, runner)); + }), + "invalid-png-image-data-finalize": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { artifactBytes: pngWithInvalidImageData() }); + return outcome("invalid-png-image-data-finalize", "page_evidence_png_invalid", finalizeSession(fixture, session, runner)); + }), + "invalid-png-image-data-validate": () => isolated((fixture) => { + const completed = completeSession(fixture); + rewriteCompletedArtifact(completed.session, completed.runner, pngWithInvalidImageData()); + return outcome("invalid-png-image-data-validate", "page_evidence_png_invalid", validateSession(fixture, completed.session)); + }), + "trailing-png-image-data-finalize": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { artifactBytes: pngWithTrailingImageData() }); + return outcome("trailing-png-image-data-finalize", "page_evidence_png_invalid", finalizeSession(fixture, session, runner)); + }), + "hash-substitution": () => isolated((fixture) => { + const completed = completeSession(fixture); + fs.appendFileSync(path.join(completed.session.artifactRoot, completed.runner.artifactReference), "substitution"); + return outcome("hash-substitution", "page_evidence_artifact_integrity", validateSession(fixture, completed.session)); + }), + "validate-runner-control-collision": () => isolated((fixture) => { + const completed = completeSession(fixture); + rewriteCompletedCapture(completed.session, completed.runner, completed.runner.runnerReference); + return outcome("validate-runner-control-collision", "page_evidence_artifact_control_collision", validateSession(fixture, completed.session)); + }), + "validate-receipt-control-collision": () => isolated((fixture) => { + const completed = completeSession(fixture); + rewriteCompletedCapture(completed.session, completed.runner, "page-evidence-session.json"); + return outcome("validate-receipt-control-collision", "page_evidence_artifact_control_collision", validateSession(fixture, completed.session)); + }), + "validate-manifest-control-collision": () => isolated((fixture) => { + const completed = completeSession(fixture); + rewriteCompletedCapture(completed.session, completed.runner, "page-evidence-manifest.json"); + return outcome("validate-manifest-control-collision", "page_evidence_artifact_control_collision", validateSession(fixture, completed.session)); + }), + "artifact-symlink": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session); + const artifact = path.join(session.artifactRoot, runner.artifactReference); + fs.rmSync(artifact); + fs.symlinkSync("/etc/hosts", artifact); + return outcome("artifact-symlink", "page_evidence_artifact_symlink", finalizeSession(fixture, session, runner)); + }), + "artifact-redirect": () => isolated((fixture) => { + const completed = completeSession(fixture); + const realRoot = `${completed.session.artifactRoot}-real`; + fs.renameSync(completed.session.artifactRoot, realRoot); + fs.symlinkSync(realRoot, completed.session.artifactRoot); + return outcome("artifact-redirect", "page_evidence_artifact_redirect", validateSession(fixture, completed.session)); + }), + "parent-escape": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { artifactPath: "../outside.png" }); + return outcome("parent-escape", "page_evidence_artifact_escape", finalizeSession(fixture, session, runner)); + }), + "capture-outside-session": () => isolated((fixture) => { + const session = started(fixture); + const runner = writeRunner(fixture, session, { recordedAt: "2000-01-01T00:00:00Z" }); + return outcome("capture-outside-session", "page_evidence_capture_outside_session", finalizeSession(fixture, session, runner)); + }), + "missing-start-receipt": () => isolated((fixture) => { + const completed = completeSession(fixture); + fs.rmSync(path.join(completed.session.artifactRoot, "page-evidence-session.json")); + return outcome("missing-start-receipt", "page_evidence_session_missing", validateSession(fixture, completed.session)); + }), + "safe-reuse": () => isolated((fixture) => { + const setup = reuseSetup(fixture); + const runner = writeRunner(fixture, setup.current, { evidence: setup.evidence, writeArtifact: false }); + const finalized = finalizeSession(fixture, setup.current, runner, setup.priorManifest); + const validated = finalized.status === 0 ? validateSession(fixture, setup.current, setup.priorManifest) : finalized; + return outcome("safe-reuse", "ok:true", validated, { finalized: finalized.report.ok === true }); + }), + "unsafe-reuse": () => isolated((fixture) => { + const setup = reuseSetup(fixture); + setup.evidence.prior_manifest_sha256 = "0".repeat(64); + const runner = writeRunner(fixture, setup.current, { evidence: setup.evidence, writeArtifact: false }); + return outcome("unsafe-reuse", "page_evidence_reuse_manifest_mismatch", finalizeSession(fixture, setup.current, runner, setup.priorManifest)); + }), + "reuse-source-mismatch": () => isolated((fixture) => { + const setup = reuseSetup(fixture, { beforeCurrent: (consumer) => { + fs.appendFileSync(path.join(consumer.root, consumer.relevantSources[0]), "// next revision\n"); + execFileSync("git", ["add", consumer.relevantSources[0]], { cwd: consumer.root }); + execFileSync("git", ["commit", "-m", "next fixture revision"], { cwd: consumer.root }); + consumer.revision = execFileSync("git", ["rev-parse", "HEAD"], { cwd: consumer.root, encoding: "utf8" }).trim(); + } }); + const runner = writeRunner(fixture, setup.current, { evidence: setup.evidence, writeArtifact: false }); + return outcome("reuse-source-mismatch", "page_evidence_reuse_source_mismatch", finalizeSession(fixture, setup.current, runner, setup.priorManifest)); + }), + "reuse-scenario-mismatch": () => isolated((fixture) => { + const setup = reuseSetup(fixture, { currentOptions: { scenarioId: "different-scenario" } }); + const runner = writeRunner(fixture, setup.current, { evidence: setup.evidence, writeArtifact: false }); + return outcome("reuse-scenario-mismatch", "page_evidence_reuse_scenario_mismatch", finalizeSession(fixture, setup.current, runner, setup.priorManifest)); + }), + "reuse-environment-mismatch": () => isolated((fixture) => { + const setup = reuseSetup(fixture); + const changedEnvironment = { ...semanticEnvironment, viewport: { ...semanticEnvironment.viewport, width: 375 } }; + const runner = writeRunner(fixture, setup.current, { evidence: setup.evidence, semanticEnvironment: changedEnvironment, writeArtifact: false }); + return outcome("reuse-environment-mismatch", "page_evidence_reuse_environment_mismatch", finalizeSession(fixture, setup.current, runner, setup.priorManifest)); + }), + "downloaded-unchanged-session": () => isolated((fixture) => { + const completed = completeSession(fixture); + const download = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-page-download-"))); + try { + fs.cpSync(fixture.root, download, { recursive: true }); + const downloadedFixture = { ...fixture, root: download }; + const downloadedSession = { ...completed.session, artifactRoot: completed.session.artifactRoot.replace(fixture.root, download), recordFile: completed.session.recordFile.replace(fixture.root, download) }; + return outcome("downloaded-unchanged-session", "ok:true", validateSession(downloadedFixture, downloadedSession)); + } finally { fs.rmSync(download, { force: true, recursive: true }); } + }), + "past-review-by-advisory": () => isolated((fixture) => { + const completed = completeSession(fixture); + const manifestFile = path.join(completed.session.artifactRoot, "page-evidence-manifest.json"); + const manifest = JSON.parse(fs.readFileSync(manifestFile)); + manifest.review_by = "2000-01-01T00:00:00Z"; + writeJson(manifestFile, manifest); + return outcome("past-review-by-advisory", "ok:true", validateSession(fixture, completed.session)); + }), +}; + +const selected = requested.length > 0 ? requested : Object.keys(cases); +const results = selected.map((name) => cases[name]?.() ?? ({ actual: { codes: [], status: null }, expected: "known-case", name, ok: false })); +const failures = results.filter((result) => !result.ok).map((result) => `${result.name}:${result.expected}`); +const report = { adversarial: results.filter((result) => result.expected !== "ok:true").length, failures, ok: failures.length === 0, results }; +if (json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +else process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +if (!report.ok) process.exitCode = 1; diff --git a/scripts/validate-page-evidence.mjs b/scripts/validate-page-evidence.mjs new file mode 100644 index 0000000..babbc1d --- /dev/null +++ b/scripts/validate-page-evidence.mjs @@ -0,0 +1,191 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + addSchemaFindings, + canonicalSourceManifest, + compilePageEvidenceSchemas, + dirtyRelevantSources, + finding, + gitIdentity, + listArtifactFiles, + metadata, + normalizeReference, + PAGE_EVIDENCE_MANIFEST, + PAGE_EVIDENCE_RECEIPT, + readJsonFile, + readContainedBytes, + readReceipt, + resolveContained, + sameJson, + sameStringSet, + withinSession, +} from "./page-evidence-contract.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; + +const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const options = { artifactRoot: undefined, json: false, manifest: undefined, priorManifest: undefined, root: undefined }; +const argumentFailures = []; + +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") options.json = true; + else if (["--artifact-root", "--manifest", "--prior-manifest", "--root"].includes(argument)) { + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) argumentFailures.push(finding("argument_value_required", `${argument} requires a value`, "")); + else { + if (argument === "--artifact-root") options.artifactRoot = path.resolve(process.cwd(), value); + if (argument === "--manifest") options.manifest = path.resolve(process.cwd(), value); + if (argument === "--prior-manifest") options.priorManifest = path.resolve(process.cwd(), value); + if (argument === "--root") options.root = path.resolve(process.cwd(), value); + index += 1; + } + } else argumentFailures.push(finding("argument_unknown", `unsupported argument ${argument}`, "")); +} +for (const [name, value] of [["--root", options.root], ["--artifact-root", options.artifactRoot]]) { + if (!value) argumentFailures.push(finding("argument_value_required", `${name} is required`, "")); +} +if (!options.manifest && options.artifactRoot) options.manifest = path.join(options.artifactRoot, PAGE_EVIDENCE_MANIFEST); + +let schemas; +try { schemas = compilePageEvidenceSchemas(path.join(styleGalleryRoot, "consumer-reference/schema")); } +catch (error) { argumentFailures.push(finding("page_evidence_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema")); } + +function checkedArtifact(root, artifact, failures) { + const resolved = readContainedBytes({ prefix: "page_evidence_artifact", reference: artifact?.path, root }, failures); + if (!resolved || !artifact?.media_type) return undefined; + const bytes = resolved.bytes; + const actual = metadata(bytes, artifact.media_type, artifact.path); + if (!sameJson(actual, artifact)) failures.push(finding("page_evidence_artifact_integrity", "artifact bytes or metadata differ from the manifest", artifact.path)); + if (artifact.media_type === "image/png" && (!actual.width || !actual.height)) failures.push(finding("page_evidence_png_invalid", "captured PNG must contain a valid IHDR/IDAT/IEND chunk stream", artifact.path)); + return { actual, bytes, file: resolved.file }; +} + +function validatePacket({ allowReuse, artifactRoot, manifestFile, priorManifestFile, root, seen }) { + const failures = []; + const artifactReference = normalizeReference(root, artifactRoot); + if (!artifactReference) failures.push(finding("page_evidence_artifact_escape", "artifact root must be inside the consumer repository", artifactRoot)); + else if (fs.existsSync(artifactRoot) && fs.lstatSync(artifactRoot).isSymbolicLink()) failures.push(finding("page_evidence_artifact_redirect", "artifact root must not be a filesystem redirect", artifactReference)); + else resolveContained({ expectedType: "directory", prefix: "page_evidence_artifact", reference: artifactReference, root }, failures); + const artifactFiles = failures.length === 0 ? listArtifactFiles(artifactRoot, failures) : []; + if (failures.some((entry) => entry.code === "page_evidence_artifact_limit")) return { failures, manifest: undefined }; + const manifestReference = normalizeReference(artifactRoot, manifestFile); + if (manifestReference !== PAGE_EVIDENCE_MANIFEST) failures.push(finding("page_evidence_manifest_path_invalid", `manifest must be ${PAGE_EVIDENCE_MANIFEST} at artifact-root`, manifestFile)); + const manifestRecord = manifestReference ? readJsonFile({ prefix: "page_evidence_manifest", reference: manifestReference, root: artifactRoot }, failures) : undefined; + const receiptRecord = readReceipt(artifactRoot, schemas, failures); + const receipt = receiptRecord?.valid ? receiptRecord : undefined; + if (!manifestRecord || !receipt) return { failures, manifest: manifestRecord?.value }; + if (!addSchemaFindings(schemas.manifest, manifestRecord.value, manifestReference, "page_evidence_manifest_schema_invalid", failures)) return { failures, manifest: manifestRecord.value }; + const manifest = manifestRecord.value; + const manifestDigest = metadata(manifestRecord.bytes, "application/json", manifestReference).sha256; + if (seen.has(manifestDigest)) { + failures.push(finding("page_evidence_reuse_cycle", "prior manifest chain contains a cycle", manifestReference)); + return { failures, manifest }; + } + seen.add(manifestDigest); + + const session = manifest.session; + const expectedSession = session ? { + attempt: receipt.value.attempt, + branch: receipt.value.branch, + completed_at: session.completed_at, + conformance_record: receipt.value.conformance_record, + environment: receipt.value.environment, + intended_scenario_ids: receipt.value.intended_scenario_ids, + nonce: receipt.value.nonce, + receipt: receipt.artifact, + receipt_sha256: receipt.digest, + repository: receipt.value.repository, + revision: receipt.value.revision, + run_id: receipt.value.run_id, + session_id: receipt.value.session_id, + source: receipt.value.source, + started_at: receipt.value.started_at, + } : undefined; + if (!sameJson(session, expectedSession)) failures.push(finding("page_evidence_session_mismatch", "completed manifest session differs from its immutable start receipt", manifestReference)); + if (manifest.repository !== receipt.value.repository) failures.push(finding("page_evidence_repository_mismatch", "manifest repository differs from the receipt", manifestReference)); + if (manifest.revision !== receipt.value.revision || manifest.run?.revision !== receipt.value.revision) failures.push(finding("page_evidence_revision_mismatch", "manifest revision differs from the receipt", manifestReference)); + if (manifest.run?.id !== receipt.value.run_id || manifest.run?.attempt !== receipt.value.attempt || manifest.run?.repository !== receipt.value.repository) failures.push(finding("page_evidence_run_mismatch", "manifest run differs from the receipt", manifestReference)); + if (manifest.completed_at !== session?.completed_at || !withinSession(session?.completed_at, receipt.value.started_at, session?.completed_at)) failures.push(finding("page_evidence_capture_outside_session", "manifest completion precedes session start", manifestReference)); + + const git = gitIdentity(root, failures); + if (git && git.revision !== receipt.value.revision) failures.push(finding("page_evidence_revision_mismatch", "checked-out revision differs from the evidence receipt", manifestReference)); + if (git?.repository && git.repository !== receipt.value.repository) failures.push(finding("page_evidence_repository_mismatch", "Git remote identity differs from the evidence receipt", manifestReference)); + const currentSource = canonicalSourceManifest(root, receipt.value.source.files.map((entry) => entry.path), failures); + if (currentSource && !sameJson(currentSource, receipt.value.source)) failures.push(finding("page_evidence_source_drift", "relevant source bytes differ from the evidence receipt", manifestReference)); + if (dirtyRelevantSources(root, receipt.value.source.files.map((entry) => entry.path)).length > 0) failures.push(finding("page_evidence_source_drift", "relevant sources are dirty while validating evidence", manifestReference)); + const conformance = readJsonFile({ prefix: "page_evidence_conformance", reference: receipt.value.conformance_record.path, root }, failures); + if (conformance && !sameJson(metadata(conformance.bytes, "application/json", conformance.reference), receipt.value.conformance_record)) failures.push(finding("page_evidence_conformance_drift", "conformance record differs from the evidence receipt", conformance.reference)); + + let priorPacket; + if (priorManifestFile) { + const priorRoot = path.dirname(priorManifestFile); + priorPacket = validatePacket({ allowReuse: false, artifactRoot: priorRoot, manifestFile: priorManifestFile, priorManifestFile: undefined, root, seen }); + failures.push(...priorPacket.failures); + } + const scenarios = Array.isArray(manifest.scenarios) ? manifest.scenarios : []; + const controlFiles = new Set([PAGE_EVIDENCE_MANIFEST, PAGE_EVIDENCE_RECEIPT, ...scenarios.map((scenario) => scenario.runner_result.path)]); + const expectedFiles = new Set(controlFiles); + const scenarioIds = []; + for (const scenario of scenarios) { + scenarioIds.push(scenario.id); + const runnerArtifact = checkedArtifact(artifactRoot, scenario.runner_result, failures); + if (!runnerArtifact) continue; + let runner; + try { runner = parseStrictJson(runnerArtifact.bytes.toString("utf8")); } + catch (error) { failures.push(finding("page_evidence_runner_json_invalid", error instanceof Error ? error.message : String(error), scenario.runner_result.path)); continue; } + if (!addSchemaFindings(schemas.runner, runner, scenario.runner_result.path, "page_evidence_runner_schema_invalid", failures)) continue; + if (runner.status !== "passed") failures.push(finding("page_evidence_runner_failed", `${scenario.id} runner did not pass`, scenario.runner_result.path)); + if (runner.session_id !== receipt.value.session_id || runner.nonce !== receipt.value.nonce || runner.receipt_sha256 !== receipt.digest) failures.push(finding("page_evidence_session_mismatch", `${scenario.id} runner session differs from the receipt`, scenario.runner_result.path)); + if (runner.revision !== receipt.value.revision || runner.run?.revision !== receipt.value.revision) failures.push(finding("page_evidence_revision_mismatch", `${scenario.id} runner revision differs from the receipt`, scenario.runner_result.path)); + if (runner.repository !== receipt.value.repository || runner.run?.repository !== receipt.value.repository) failures.push(finding("page_evidence_repository_mismatch", `${scenario.id} runner repository differs from the receipt`, scenario.runner_result.path)); + if (runner.source_sha256 !== receipt.value.source.sha256) failures.push(finding("page_evidence_source_mismatch", `${scenario.id} runner source differs from the receipt`, scenario.runner_result.path)); + if (!sameJson({ ...runner.run, status: "passed" }, manifest.run)) failures.push(finding("page_evidence_run_mismatch", `${scenario.id} runner identity differs from the closed run`, scenario.runner_result.path)); + if (runner.scenario_id !== scenario.id || runner.recorded_at !== scenario.recorded_at || !sameJson(runner.semantic_environment, scenario.semantic_environment)) failures.push(finding("page_evidence_scenario_mismatch", `${scenario.id} runner scenario differs from the manifest`, scenario.runner_result.path)); + if (!withinSession(runner.recorded_at, receipt.value.started_at, session?.completed_at)) failures.push(finding("page_evidence_capture_outside_session", `${scenario.id} runner was recorded outside the session interval`, scenario.runner_result.path)); + if (scenario.evidence?.kind === "captured") { + const declared = runner.evidence?.kind === "captured" ? runner.evidence.artifacts : []; + const captured = scenario.evidence.artifacts ?? []; + if (!sameJson(declared, captured.map(({ path: artifactPath, media_type: mediaType }) => ({ media_type: mediaType, path: artifactPath })))) failures.push(finding("page_evidence_scenario_mismatch", `${scenario.id} artifact declarations differ from the runner result`, scenario.runner_result.path)); + for (const artifact of captured) { + if (controlFiles.has(artifact.path)) failures.push(finding("page_evidence_artifact_control_collision", "captured artifacts must be disjoint from manifest, receipt, and runner-result controls", artifact.path)); + expectedFiles.add(artifact.path); + checkedArtifact(artifactRoot, artifact, failures); + } + } else if (scenario.evidence?.kind === "reused") { + if (!allowReuse) failures.push(finding("page_evidence_reuse_chain_unsupported", "a reused manifest cannot be reused again", manifestReference)); + if (!sameJson(runner.evidence, scenario.evidence)) failures.push(finding("page_evidence_reuse_identity_mismatch", `${scenario.id} reuse differs from the runner result`, scenario.runner_result.path)); + if (!priorPacket?.manifest) failures.push(finding("page_evidence_reuse_manifest_required", "reused evidence requires a valid prior manifest", manifestReference)); + else { + const prior = priorPacket.manifest; + const priorScenario = prior.scenarios?.find((candidate) => candidate.id === scenario.evidence.prior_scenario_id); + const priorBytes = fs.readFileSync(priorManifestFile); + if (scenario.evidence.prior_manifest_sha256 !== metadata(priorBytes, "application/json", PAGE_EVIDENCE_MANIFEST).sha256) failures.push(finding("page_evidence_reuse_manifest_mismatch", "prior manifest digest does not match", manifestReference)); + if (scenario.evidence.prior_session_id !== prior.session?.session_id || scenario.evidence.prior_run_id !== prior.run?.id) failures.push(finding("page_evidence_reuse_identity_mismatch", "prior session or run identity does not match", manifestReference)); + if (!priorScenario || priorScenario.id !== scenario.id || priorScenario.evidence?.kind !== "captured") failures.push(finding("page_evidence_reuse_scenario_mismatch", "reuse must select the same captured scenario", manifestReference)); + if (!sameJson(prior.session?.source, receipt.value.source)) failures.push(finding("page_evidence_reuse_source_mismatch", "reuse requires byte-identical relevant sources", manifestReference)); + if (priorScenario && !sameJson(priorScenario.semantic_environment, scenario.semantic_environment)) failures.push(finding("page_evidence_reuse_environment_mismatch", "reuse requires the same semantic environment", manifestReference)); + } + } + } + if (!sameStringSet(scenarioIds, receipt.value.intended_scenario_ids)) failures.push(finding("page_evidence_pass_membership_invalid", "PASS scenarios must exactly match the start intent", manifestReference)); + for (const reference of artifactFiles) if (!expectedFiles.has(reference)) failures.push(finding("page_evidence_artifact_unmanifested", "artifact root contains a file outside the closed manifest", reference)); + seen.delete(manifestDigest); + return { failures, manifest }; +} + +let report = { failures: argumentFailures, manifest: undefined }; +if (argumentFailures.length === 0 && schemas) report = validatePacket({ allowReuse: true, artifactRoot: options.artifactRoot, manifestFile: options.manifest, priorManifestFile: options.priorManifest, root: options.root, seen: new Set() }); +const result = { + artifactCount: report.manifest?.scenarios?.reduce((count, scenario) => count + (scenario.evidence?.artifacts?.length ?? 0), 0) ?? 0, + failures: report.failures, + ok: report.failures.length === 0, + scenarioCount: report.manifest?.scenarios?.length ?? 0, + sessionId: report.manifest?.session?.session_id, +}; +if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +else if (result.ok) process.stdout.write(`validated ${result.scenarioCount} page evidence scenarios\n`); +else process.stderr.write(`${result.failures.map((issue) => `${issue.code}: ${issue.path}: ${issue.message}`).join("\n")}\n`); +if (!result.ok) process.exitCode = 1; From 5a8ddf5458956bd19b52358487c81007a0a29433 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:35:16 +0900 Subject: [PATCH 39/44] refactor(domains): support local governed leaves Plan-Task: 3 --- scripts/test-validate-domains.mjs | 135 +++++++++++++++++++++++++----- scripts/validate-domains.mjs | 123 ++++++++++++++++++--------- 2 files changed, 195 insertions(+), 63 deletions(-) diff --git a/scripts/test-validate-domains.mjs b/scripts/test-validate-domains.mjs index 19cb3cd..1269a44 100644 --- a/scripts/test-validate-domains.mjs +++ b/scripts/test-validate-domains.mjs @@ -4,7 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(root, "scripts", "validate-domains.mjs"); @@ -32,17 +32,28 @@ function indexPage(title, links) { ].join("\n"); } -function leafPage({ title, domain, sourcePath, parent, next, lifecycle = "experimental", body = "" }) { - return [ +function leafPage({ title, domain, sourcePath, parent, next, lifecycle = "experimental", body = "", provenanceKind, forgedSourceFields = false, omitSourceFields = false }) { + const frontmatter = [ "---", "type: Domain Guide", `title: ${title}`, `description: Bounded ${title} guidance.`, `domain: ${domain}`, `lifecycle: ${lifecycle}`, - `source_repository: ${repository}`, - `source_path: ${sourcePath}`, - `source_revision: ${revision}`, + ]; + if (provenanceKind) frontmatter.push(`provenance_kind: ${provenanceKind}`); + if ((!provenanceKind && !omitSourceFields) || forgedSourceFields) { + frontmatter.push(`source_repository: ${repository}`, `source_path: ${sourcePath}`, `source_revision: ${revision}`); + } + const attribution = provenanceKind === "local" + ? ["- Local StyleGallery method; no upstream attribution."] + : [ + `- Upstream inspiration: ${repository}/blob/${revision}/${sourcePath}`, + `- Snapshot: \`${revision}\``, + "- Reuse form: independent method rewrite.", + ]; + return [ + ...frontmatter, "---", "", `# ${title}`, @@ -74,9 +85,7 @@ function leafPage({ title, domain, sourcePath, parent, next, lifecycle = "experi "", "## Source, License, And Attribution", "", - `- Upstream inspiration: ${repository}/blob/${revision}/${sourcePath}`, - `- Snapshot: \`${revision}\``, - "- Reuse form: independent method rewrite.", + ...attribution, "", "## IA Navigation", "", @@ -86,6 +95,23 @@ function leafPage({ title, domain, sourcePath, parent, next, lifecycle = "experi ].join("\n"); } +const localLeafPath = "design-engineering/consumer-migration-readiness.md"; +const localLeafRegistry = (canonicalDomains) => { + const registry = structuredClone(canonicalDomains); + const designEngineering = registry.find((domain) => domain.slug === "design-engineering"); + if (!designEngineering.leaves.some((leaf) => leaf.path === localLeafPath)) designEngineering.leaves.push({ path: localLeafPath, provenance: "local" }); + return registry; +}; + +const localLeaf = leafPage({ + title: "Consumer Migration Readiness", + domain: "design-engineering", + parent: "index.md", + next: "../platform-guides/index.md", + provenanceKind: "local", + sourcePath: "unused/local-source.md", +}); + const baseFiles = { "README.md": "# StyleGallery\n\n- [Layout](layout/index.md)\n- [Motion](motion/index.md)\n- [Design Engineering](design-engineering/index.md)\n- [Platform Guides](platform-guides/index.md)\n", "index.md": "# StyleGallery\n\n- [Layout](layout/index.md)\n- [Motion](motion/index.md)\n- [Design Engineering](design-engineering/index.md)\n- [Platform Guides](platform-guides/index.md)\n", @@ -155,6 +181,24 @@ const cases = [ { name: "missing_domain_index", omit: ["design-engineering/index.md"], expect: "design-engineering/index.md: missing file" }, { name: "missing_domain_leaf", omit: ["motion/vocabulary.md"], expect: "motion/vocabulary.md: missing file" }, { name: "undeclared_domain_leaf", add: ["motion/rogue.md", "# Rogue\n"], expect: "motion/rogue.md: undeclared governed domain document" }, + { + name: "local_leaf_success", + registry: localLeafRegistry, + expect: null, + }, + { + name: "local_leaf_missing_provenance", + registry: localLeafRegistry, + mutate: [localLeafPath, "provenance_kind: local\n", ""], + expect: "design-engineering/consumer-migration-readiness.md: missing provenance_kind", + }, + { + name: "local_leaf_forged_source", + registry: localLeafRegistry, + add: [localLeafPath, leafPage({ title: "Consumer Migration Readiness", domain: "design-engineering", parent: "index.md", next: "../platform-guides/index.md", sourcePath: "unused/local-source.md", provenanceKind: "local", forgedSourceFields: true })], + expect: "design-engineering/consumer-migration-readiness.md: local leaf must not declare source_repository", + }, + { name: "undeclared_local_leaf", add: ["design-engineering/rogue-local.md", localLeaf.replace("title: Consumer Migration Readiness", "title: Rogue Local Method")], expect: "design-engineering/rogue-local.md: undeclared governed domain document" }, { name: "unknown_domain", mutate: ["motion/vocabulary.md", "domain: motion", "domain: unknown"], expect: "motion/vocabulary.md: unknown domain unknown" }, { name: "wrong_domain", mutate: ["motion/vocabulary.md", "domain: motion", "domain: platform-guides"], expect: "motion/vocabulary.md: domain platform-guides does not match motion" }, { name: "missing_lifecycle", mutate: ["motion/vocabulary.md", "lifecycle: experimental\n", ""], expect: "motion/vocabulary.md: missing lifecycle" }, @@ -182,13 +226,19 @@ function writeFixture(testCase) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), `stylegallery-domains-${testCase.name}-`)); const omitted = new Set(testCase.omit ?? []); const entries = { ...baseFiles }; - if (testCase.mutate) { - const [relative, before, after] = testCase.mutate; - entries[relative] = entries[relative].replace(before, after); + if (testCase.registry) { + entries[localLeafPath] = localLeaf; + entries["DOMAINS.md"] = entries["DOMAINS.md"].replace( + "`design-engineering/interface-craft.md`, `design-engineering/reference-profiles/index.md`", + "`design-engineering/interface-craft.md`, `design-engineering/consumer-migration-readiness.md`, `design-engineering/reference-profiles/index.md`", + ); + entries["design-engineering/index.md"] = indexPage("Design Engineering", [["Interface Craft", "interface-craft.md"], ["Consumer Migration Readiness", "consumer-migration-readiness.md"], ["Reference Profiles", "reference-profiles/index.md"]]); } + const mutations = testCase.mutations ?? (testCase.mutate ? [testCase.mutate] : []); + for (const [relative, before, after] of mutations) entries[relative] = entries[relative].replace(before, after); if (testCase.add) { - const [relative, content] = testCase.add; - entries[relative] = content; + const additions = Array.isArray(testCase.add[0]) ? testCase.add : [testCase.add]; + for (const [relative, content] of additions) entries[relative] = content; } for (const [relative, content] of Object.entries(entries)) { if (omitted.has(relative)) continue; @@ -199,21 +249,62 @@ function writeFixture(testCase) { return dir; } -function runCase(testCase) { +let validatorModulePromise; +async function loadValidatorModule() { + if (!validatorModulePromise) { + validatorModulePromise = (async () => { + const previousExitCode = process.exitCode; + const originalLog = console.log; + const originalError = console.error; + console.log = () => {}; + console.error = () => {}; + try { + return await import(pathToFileURL(validator).href); + } catch { + return null; + } finally { + console.log = originalLog; + console.error = originalError; + process.exitCode = previousExitCode; + } + })(); + } + return validatorModulePromise; +} + +async function runCase(testCase) { const dir = writeFixture(testCase); - const result = spawnSync(process.execPath, [validator, "--json"], { cwd: dir, encoding: "utf8" }); - fs.rmSync(dir, { force: true, recursive: true }); let output; - try { - output = JSON.parse(result.stdout); - } catch { - output = { ok: false, failures: ["validator unavailable or returned non-JSON output"] }; + if (testCase.registry) { + const module = await loadValidatorModule(); + if (module?.validateDomains && module.canonicalDomains) { + output = module.validateDomains({ root: dir, domains: testCase.registry(module.canonicalDomains) }); + } else { + output = { ok: false, failures: ["validator does not expose a registry injection function"] }; + } + } else { + const result = spawnSync(process.execPath, [validator, "--json"], { cwd: dir, encoding: "utf8" }); + try { + output = JSON.parse(result.stdout); + } catch { + output = { ok: false, failures: ["validator unavailable or returned non-JSON output"] }; + } } + fs.rmSync(dir, { force: true, recursive: true }); const passed = testCase.expect ? !output.ok && output.failures.includes(testCase.expect) : output.ok; return { name: testCase.name, ok: passed, expected: testCase.expect ?? "ok:true", actual: output }; } -const results = cases.map(runCase); +const caseIndex = process.argv.indexOf("--case"); +const requestedCase = caseIndex === -1 ? null : process.argv[caseIndex + 1]; +const selectedCases = requestedCase ? cases.filter((testCase) => testCase.name === requestedCase) : cases; +if (requestedCase && selectedCases.length === 0) { + console.error(`unknown case: ${requestedCase}`); + process.exitCode = 1; + process.exit(); +} + +const results = await Promise.all(selectedCases.map(runCase)); const report = { ok: results.every((result) => result.ok), results }; console.log(JSON.stringify(report, null, 2)); process.exitCode = report.ok ? 0 : 1; diff --git a/scripts/validate-domains.mjs b/scripts/validate-domains.mjs index b530e43..b9eb3c3 100644 --- a/scripts/validate-domains.mjs +++ b/scripts/validate-domains.mjs @@ -2,13 +2,10 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { collectDomainBoundaryFailures } from "./domain-document-boundaries.mjs"; import { isOmoDependency, markdownLinkDestinations, stripFencedCodeBlocks } from "./markdown-structure.mjs"; -const args = new Set(process.argv.slice(2)); -const json = args.has("--json"); -const root = process.cwd(); -const failures = []; const repository = "https://github.com/emilkowalski/skills"; const revision = "220e8607c90b17337d210125777b7b695f26c221"; const revisionPattern = /^[0-9a-f]{40}$/; @@ -18,25 +15,32 @@ const referenceDocuments = [ "design-engineering/reference-profiles/external-adaptation/index.md", ]; -const domains = [ +export const canonicalDomains = [ { slug: "layout", label: "Layout", leaves: [] }, { slug: "motion", label: "Motion", leaves: [ - ["motion/vocabulary.md", "skills/animation-vocabulary/SKILL.md"], - ["motion/review-workflow.md", "skills/review-animations/SKILL.md"], - ["motion/practice-reference.md", "skills/review-animations/STANDARDS.md"], + { path: "motion/vocabulary.md", provenance: "external", sourcePath: "skills/animation-vocabulary/SKILL.md" }, + { path: "motion/review-workflow.md", provenance: "external", sourcePath: "skills/review-animations/SKILL.md" }, + { path: "motion/practice-reference.md", provenance: "external", sourcePath: "skills/review-animations/STANDARDS.md" }, ], }, { slug: "design-engineering", label: "Design Engineering", - leaves: [["design-engineering/interface-craft.md", "skills/emil-design-eng/SKILL.md"]], + leaves: [ + { path: "design-engineering/interface-craft.md", provenance: "external", sourcePath: "skills/emil-design-eng/SKILL.md" }, + ], referenceDocuments, }, - { slug: "platform-guides", label: "Platform Guides", leaves: [["platform-guides/apple-interaction.md", "skills/apple-design/SKILL.md"]] }, + { slug: "platform-guides", label: "Platform Guides", leaves: [{ path: "platform-guides/apple-interaction.md", provenance: "external", sourcePath: "skills/apple-design/SKILL.md" }] }, ]; +export const domainRegistry = canonicalDomains; + +let domains = canonicalDomains; +let root = process.cwd(); +let failures = []; const requiredLeafSections = [ "Repository Boundary", @@ -120,7 +124,7 @@ function checkManifest() { if (!pageRow || pageRow[1] !== expectedManualHub) valid = false; if (domain.leaves.length > 0 && pageRow) { const declaredLeaves = [...pageRow[2].matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]).sort(); - const expectedLeaves = [...domain.leaves.map(([leaf]) => leaf), ...(domain.referenceDocuments ?? [])].sort(); + const expectedLeaves = [...domain.leaves.map((leaf) => leaf.path), ...(domain.referenceDocuments ?? [])].sort(); if (JSON.stringify(declaredLeaves) !== JSON.stringify(expectedLeaves)) valid = false; } } @@ -139,8 +143,8 @@ function checkIndex(domain) { if (!/^Out of scope:\s*\S/m.test(content)) failures.push(`${relative}: missing Out of scope declaration`); if (!/^Parent: \[[^\]]+\]\([^)]+\)/m.test(content)) failures.push(`${relative}: missing Parent navigation link`); if (!/^Next: \[[^\]]+\]\([^)]+\)/m.test(content)) failures.push(`${relative}: missing Next navigation link`); - for (const [leaf] of domain.leaves) { - const target = path.basename(leaf); + for (const leaf of domain.leaves) { + const target = path.basename(leaf.path); if (!content.match(new RegExp(`\\[[^\\]]+\\]\\(${target.replaceAll(".", "\\.")}\\)`))) { failures.push(`${relative}: missing leaf route ${target}`); } @@ -150,23 +154,36 @@ function checkIndex(domain) { } } -function checkLeaf(domain, relative, expectedSourcePath, titles) { +function checkLeaf(domain, leaf, titles) { + const { path: relative, provenance, sourcePath: expectedSourcePath } = leaf; const content = read(relative); if (!content) return; const metadata = parseFrontmatter(relative, content); - for (const field of ["type", "title", "description", "domain", "lifecycle", "source_repository", "source_path", "source_revision"]) { + const requiredFields = ["type", "title", "description", "domain", "lifecycle"]; + if (provenance === "external") requiredFields.push("source_repository", "source_path", "source_revision"); + if (provenance === "local") requiredFields.push("provenance_kind"); + for (const field of requiredFields) { if (!metadata[field]) failures.push(`${relative}: missing ${field}`); } const knownDomain = domains.some((candidate) => candidate.slug === metadata.domain); if (metadata.domain && !knownDomain) failures.push(`${relative}: unknown domain ${metadata.domain}`); else if (metadata.domain && metadata.domain !== domain.slug) failures.push(`${relative}: domain ${metadata.domain} does not match ${domain.slug}`); - if (metadata.lifecycle && metadata.lifecycle !== "experimental") failures.push(`${relative}: external adaptation lifecycle must be experimental`); - if (metadata.source_repository && metadata.source_repository !== repository) failures.push(`${relative}: unexpected source_repository ${metadata.source_repository}`); - if (metadata.source_path && metadata.source_path !== expectedSourcePath) failures.push(`${relative}: unexpected source_path ${metadata.source_path}`); - if (metadata.source_revision && !revisionPattern.test(metadata.source_revision)) { - failures.push(`${relative}: source_revision must be a full 40-character lowercase Git SHA`); - } else if (metadata.source_revision && metadata.source_revision !== revision) { - failures.push(`${relative}: unexpected source_revision ${metadata.source_revision}`); + if (metadata.lifecycle && metadata.lifecycle !== "experimental") { + failures.push(`${relative}: ${provenance === "local" ? "local leaf" : "external adaptation"} lifecycle must be experimental`); + } + if (provenance === "external") { + if (metadata.source_repository && metadata.source_repository !== repository) failures.push(`${relative}: unexpected source_repository ${metadata.source_repository}`); + if (metadata.source_path && metadata.source_path !== expectedSourcePath) failures.push(`${relative}: unexpected source_path ${metadata.source_path}`); + if (metadata.source_revision && !revisionPattern.test(metadata.source_revision)) { + failures.push(`${relative}: source_revision must be a full 40-character lowercase Git SHA`); + } else if (metadata.source_revision && metadata.source_revision !== revision) { + failures.push(`${relative}: unexpected source_revision ${metadata.source_revision}`); + } + } else if (provenance === "local") { + if (metadata.provenance_kind && metadata.provenance_kind !== "local") failures.push(`${relative}: local leaf provenance_kind must be local`); + for (const field of ["source_repository", "source_path", "source_revision"]) { + if (Object.hasOwn(metadata, field)) failures.push(`${relative}: local leaf must not declare ${field}`); + } } if (metadata.title) { if (titles.has(metadata.title)) failures.push(`${relative}: duplicate title ${metadata.title}`); @@ -214,24 +231,48 @@ function checkPromotionBoundary() { for (const clause of required) if (!content.includes(clause)) failures.push(`${relative}: missing promotion boundary ${clause}`); } -checkManifest(); -read("quality/claim-records/stylegallery-multidomain-scope.md"); -requireRootRoutes(); -const titles = new Set(); -let checkedLeaves = 0; -for (const domain of domains) { - checkIndex(domain); - for (const [relative, sourcePath] of domain.leaves) { - checkedLeaves += 1; - checkLeaf(domain, relative, sourcePath, titles); +function boundaryRegistry(registry) { + return registry.map((domain) => ({ + ...domain, + leaves: domain.leaves.map((leaf) => [leaf.path, leaf.sourcePath]), + })); +} + +export function validateDomains({ root: nextRoot = process.cwd(), domains: nextDomains = canonicalDomains } = {}) { + const previousRoot = root; + const previousDomains = domains; + const previousFailures = failures; + root = nextRoot; + domains = nextDomains; + failures = []; + checkManifest(); + read("quality/claim-records/stylegallery-multidomain-scope.md"); + requireRootRoutes(); + const titles = new Set(); + let checkedLeaves = 0; + for (const domain of domains) { + checkIndex(domain); + for (const leaf of domain.leaves) { + checkedLeaves += 1; + checkLeaf(domain, leaf, titles); + } } + failures.push(...collectDomainBoundaryFailures(root, boundaryRegistry(domains))); + checkReferenceDocuments(); + checkPromotionBoundary(); + + const result = { ok: failures.length === 0, checkedDomains: domains.length, checkedLeaves, failures: [...new Set(failures)] }; + root = previousRoot; + domains = previousDomains; + failures = previousFailures; + return result; +} + +const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)); +if (isMainModule) { + const result = validateDomains(); + if (process.argv.includes("--json")) console.log(JSON.stringify(result, null, 2)); + else if (result.ok) console.log(`ok: ${result.checkedDomains} domains, ${result.checkedLeaves} governed leaves`); + else console.error(result.failures.join("\n")); + process.exitCode = result.ok ? 0 : 1; } -failures.push(...collectDomainBoundaryFailures(root, domains)); -checkReferenceDocuments(); -checkPromotionBoundary(); - -const result = { ok: failures.length === 0, checkedDomains: domains.length, checkedLeaves, failures: [...new Set(failures)] }; -if (json) console.log(JSON.stringify(result, null, 2)); -else if (result.ok) console.log(`ok: ${result.checkedDomains} domains, ${result.checkedLeaves} governed leaves`); -else console.error(result.failures.join("\n")); -process.exitCode = result.ok ? 0 : 1; From e419477a1dcd8166f16b506597d92fa373576ba0 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:35:35 +0900 Subject: [PATCH 40/44] feat(evidence): report explicit freshness deadlines Plan-Task: 4 --- .../editorial/evidence/button.evidence.json | 90 ++--- .../terminal/evidence/button.evidence.json | 90 ++--- package.json | 1 + scripts/audit-evidence-freshness.mjs | 308 ++++++++++++++++++ scripts/test-audit-evidence-freshness.mjs | 177 ++++++++++ 5 files changed, 576 insertions(+), 90 deletions(-) create mode 100644 scripts/audit-evidence-freshness.mjs create mode 100644 scripts/test-audit-evidence-freshness.mjs diff --git a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json index bb6e292..ede64b0 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json @@ -103,9 +103,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -223,7 +223,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -335,9 +335,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -455,7 +455,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -567,9 +567,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -687,7 +687,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -801,9 +801,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -921,7 +921,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1032,9 +1032,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1152,7 +1152,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1263,9 +1263,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1383,7 +1383,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1496,9 +1496,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1616,7 +1616,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1729,9 +1729,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1849,7 +1849,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1962,9 +1962,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2082,7 +2082,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2197,9 +2197,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2317,7 +2317,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2431,9 +2431,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2551,7 +2551,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2665,9 +2665,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2785,7 +2785,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2901,9 +2901,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -3021,7 +3021,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3133,9 +3133,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -3253,7 +3253,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3365,9 +3365,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -3485,7 +3485,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, diff --git a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json index 3a7cee4..d28ddd3 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json @@ -103,9 +103,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -223,7 +223,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -335,9 +335,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -455,7 +455,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -567,9 +567,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -687,7 +687,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -801,9 +801,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -921,7 +921,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1032,9 +1032,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1152,7 +1152,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1263,9 +1263,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1383,7 +1383,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1496,9 +1496,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1616,7 +1616,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1729,9 +1729,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -1849,7 +1849,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1962,9 +1962,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2082,7 +2082,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2197,9 +2197,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2317,7 +2317,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2431,9 +2431,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2551,7 +2551,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2665,9 +2665,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -2785,7 +2785,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2901,9 +2901,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -3021,7 +3021,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3133,9 +3133,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -3253,7 +3253,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3365,9 +3365,9 @@ "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" }, { - "byte_length": 1432, + "byte_length": 1499, "path": "package.json", - "sha256": "sha256:80d8752faf5534a1f17193f8d70ae8a26939ec2ce68049c2b377ca4964aaab69" + "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" }, { "byte_length": 532, @@ -3485,7 +3485,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:cd2e62cb6dbc346c53600b47fe4d7d9552df40b5ed9e6973682129d3c61b989c" + "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" }, "started_at": "2026-07-14T06:33:47.214Z" }, diff --git a/package.json b/package.json index a56e3d6..5b15c51 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "node": ">=22" }, "scripts": { + "audit:evidence": "node scripts/audit-evidence-freshness.mjs", "build": "node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json", "create:component-state-session": "node scripts/create-component-state-session.mjs", "test": "node scripts/test-reference-adapters.mjs --json", diff --git a/scripts/audit-evidence-freshness.mjs b/scripts/audit-evidence-freshness.mjs new file mode 100644 index 0000000..a33f195 --- /dev/null +++ b/scripts/audit-evidence-freshness.mjs @@ -0,0 +1,308 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv2020 from "ajv/dist/2020.js"; +import { addDateTimeFormat, isRfc3339DateTime } from "./json-schema-formats.mjs"; +import { parseStrictJson } from "./strict-json.mjs"; +import { validateCalibration } from "./baseline-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const calibrationSchemaPath = path.join(repositoryRoot, "consumer-reference", "schema", "calibration-record.schema.json"); +const defaultPageSchemaPath = path.join(repositoryRoot, "consumer-reference", "schema", "page-evidence-manifest.schema.json"); +const PAGE_RECORD_KIND = "page_evidence_manifest"; +// This is an advisory warning window only. It never changes an explicit expiry or adds a TTL. +const DEFAULT_EXPIRING_WINDOW_DAYS = 7; + +function failure(code, sourcePath, message, details = undefined) { + return details === undefined ? { code, message, path: sourcePath } : { code, message, path: sourcePath, details }; +} + +function warning(code, sourcePath, status, thresholdDate, asOf, message) { + return { asOf, code, message, source_path: sourcePath, status, threshold_date: thresholdDate }; +} + +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function parseDateTime(value) { + return isRfc3339DateTime(value) ? Date.parse(value) : NaN; +} + +function isInside(base, target) { + const relative = path.relative(path.resolve(base), path.resolve(target)); + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); +} + +function displayPath(file, root) { + const absolute = path.resolve(file); + if (isInside(root, absolute)) return path.relative(root, absolute).split(path.sep).join("/") || "."; + return absolute; +} + +function readRecord(file, root, failures) { + const sourcePath = displayPath(file, root); + let stat; + try { + stat = fs.lstatSync(file); + } catch (error) { + failures.push(failure("freshness_record_unreadable", sourcePath, `record cannot be read: ${error instanceof Error ? error.message : String(error)}`)); + return undefined; + } + if (!stat.isFile() || stat.isSymbolicLink()) { + failures.push(failure("freshness_record_unreadable", sourcePath, "record must be a regular non-symlink file")); + return undefined; + } + if (stat.size > 1024 * 1024) { + failures.push(failure("freshness_record_unreadable", sourcePath, "record must not exceed 1 MiB")); + return undefined; + } + try { + return { sourcePath, value: parseStrictJson(fs.readFileSync(file, "utf8")) }; + } catch (error) { + failures.push(failure("freshness_record_invalid", sourcePath, error instanceof Error ? error.message : String(error))); + return undefined; + } +} + +function readSchema(schemaPath, sourcePath, failures) { + let stat; + try { + stat = fs.lstatSync(schemaPath); + } catch (error) { + failures.push(failure("freshness_schema_missing", sourcePath, `schema cannot be read: ${error instanceof Error ? error.message : String(error)}`)); + return undefined; + } + if (!stat.isFile() || stat.isSymbolicLink()) { + failures.push(failure("freshness_schema_invalid", sourcePath, "schema must be a regular non-symlink file")); + return undefined; + } + try { + return parseStrictJson(fs.readFileSync(schemaPath, "utf8")); + } catch (error) { + failures.push(failure("freshness_schema_invalid", sourcePath, error instanceof Error ? error.message : String(error))); + return undefined; + } +} + +function compileSchema(schema, schemaPath, sourcePath, failures, { strict = true } = {}) { + if (!isRecord(schema)) { + failures.push(failure("freshness_schema_invalid", sourcePath, "schema must be a JSON object")); + return undefined; + } + try { + const ajv = new Ajv2020({ allErrors: true, strict }); + addDateTimeFormat(ajv); + const schemaDirectory = path.dirname(defaultPageSchemaPath); + if (path.resolve(schemaPath) === path.resolve(defaultPageSchemaPath) && fs.existsSync(schemaDirectory)) { + for (const entry of fs.readdirSync(schemaDirectory, { withFileTypes: true }).filter((item) => item.isFile() && item.name.endsWith(".schema.json"))) { + const dependencyPath = path.join(schemaDirectory, entry.name); + if (path.resolve(dependencyPath) === path.resolve(schemaPath)) continue; + try { + const dependency = parseStrictJson(fs.readFileSync(dependencyPath, "utf8")); + if (dependency?.$id) ajv.addSchema(dependency, dependency.$id); + } catch { + // Only schemas reachable from the selected page schema affect this compile. + } + } + } + if (schema.$id) ajv.addSchema(schema, schema.$id); + else ajv.addSchema(schema, schemaPath); + return ajv.getSchema(schema.$id ?? schemaPath) ?? ajv.compile(schema); + } catch (error) { + failures.push(failure("freshness_schema_invalid", sourcePath, error instanceof Error ? error.message : String(error))); + return undefined; + } +} + +function schemaFailures(validate, value, sourcePath) { + if (validate(value)) return []; + return (validate.errors ?? []).map((error) => failure( + "freshness_record_invalid", + sourcePath, + `${error.instancePath || "/"} ${error.message}`, + )); +} + +function validateCalibrationRecord(value, sourcePath, failures) { + const schema = readSchema(calibrationSchemaPath, sourcePath, failures); + const validate = schema === undefined ? undefined : compileSchema(schema, calibrationSchemaPath, sourcePath, failures, { strict: false }); + if (validate) failures.push(...schemaFailures(validate, value, sourcePath)); + for (const finding of validateCalibration(value, sourcePath)) { + failures.push(failure("freshness_record_invalid", sourcePath, finding.message, { code: finding.code })); + } + if (!isRecord(value)) return undefined; + const external = value.committed_ci?.external_verification; + if (external === null || external === undefined) return { thresholdDate: null, verifiedAt: null }; + if (!isRecord(external) || !isRecord(external.artifact)) { + failures.push(failure("freshness_record_invalid", sourcePath, "calibration external verification must contain artifact metadata")); + return undefined; + } + const thresholdDate = external.artifact.expires_at; + const verifiedAt = external.verified_at; + if (!Number.isFinite(parseDateTime(thresholdDate)) || !Number.isFinite(parseDateTime(verifiedAt))) { + failures.push(failure("freshness_record_invalid", sourcePath, "calibration expiry and verification dates must be RFC 3339 date-time values")); + return undefined; + } + return { thresholdDate, verifiedAt }; +} + +function validatePageRecord(value, sourcePath, pageSchemaPath, failures) { + const schema = readSchema(pageSchemaPath, sourcePath, failures); + if (schema === undefined) return undefined; + const validate = compileSchema(schema, pageSchemaPath, sourcePath, failures, { strict: true }); + if (validate) failures.push(...schemaFailures(validate, value, sourcePath)); + if (!isRecord(value) || value.record_kind !== PAGE_RECORD_KIND) return undefined; + const thresholdDate = value.review_by; + if (!Number.isFinite(parseDateTime(thresholdDate))) { + failures.push(failure("freshness_record_invalid", sourcePath, "page evidence review_by must be an RFC 3339 date-time value")); + return undefined; + } + return { thresholdDate, verifiedAt: null }; +} + +function classify(entry, asOfMs, windowMs) { + if (entry.thresholdDate === null) return "fresh"; + const thresholdMs = Date.parse(entry.thresholdDate); + if (entry.kind === PAGE_RECORD_KIND) { + if (asOfMs >= thresholdMs) return "review_due"; + return thresholdMs - asOfMs <= windowMs ? "expiring" : "fresh"; + } + if (asOfMs >= thresholdMs) return "expired"; + return thresholdMs - asOfMs <= windowMs ? "expiring" : "fresh"; +} + +function isBlockingStatus(status) { + return status === "expired" || status === "review_due"; +} + +function auditOne({ file, asOf, asOfMs, mode, root, pageSchemaPath, expiringWindowMs }) { + const failures = []; + const sourcePath = displayPath(file, root); + const loaded = readRecord(file, root, failures); + if (!loaded) return { failures, warnings: [], record: null }; + const { value } = loaded; + if (!isRecord(value)) { + failures.push(failure("freshness_record_invalid", sourcePath, "record must be a JSON object")); + return { failures, warnings: [], record: null }; + } + let kind; + let dates; + if (value.record_kind === PAGE_RECORD_KIND) { + kind = PAGE_RECORD_KIND; + dates = validatePageRecord(value, sourcePath, pageSchemaPath, failures); + } else if (value.committed_ci !== undefined || value.required_runs !== undefined || value.runs !== undefined) { + kind = "calibration_record"; + dates = validateCalibrationRecord(value, sourcePath, failures); + } else { + failures.push(failure("freshness_record_invalid", sourcePath, "record_kind is not a supported evidence record")); + return { failures, warnings: [], record: null }; + } + if (dates === undefined || failures.length > 0) return { failures, warnings: [], record: null }; + const entry = { asOf, kind, source_path: sourcePath, threshold_date: dates.thresholdDate, verified_at: dates.verifiedAt }; + const status = classify({ kind, thresholdDate: dates.thresholdDate }, asOfMs, expiringWindowMs); + const result = { ...entry, status }; + const warnings = []; + if (status !== "fresh") { + const code = status === "expiring" ? "evidence_expiring" : status === "expired" ? "evidence_expired" : "evidence_review_due"; + const message = status === "expiring" + ? `evidence expiry is within the advisory window (${dates.thresholdDate})` + : status === "expired" + ? `evidence expired at ${dates.thresholdDate}` + : `evidence review is due at ${dates.thresholdDate}`; + warnings.push(warning(code, sourcePath, status, dates.thresholdDate, asOf, message)); + if (mode === "blocking" && isBlockingStatus(status)) failures.push(failure(code, sourcePath, message, { asOf, threshold_date: dates.thresholdDate })); + } + return { failures, record: result, warnings }; +} + +export function parseArguments(argv = process.argv.slice(2)) { + const options = { asOf: null, expiringWindowDays: DEFAULT_EXPIRING_WINDOW_DAYS, json: false, mode: "advisory", pageSchema: defaultPageSchemaPath, records: [] }; + const failures = []; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--json") { + options.json = true; + continue; + } + if (["--record", "--as-of", "--mode", "--page-schema", "--expiring-within-days"].includes(argument)) { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + failures.push(failure("argument_value_required", "", `${argument} requires a value`)); + continue; + } + index += 1; + if (argument === "--record") options.records.push(value); + else if (argument === "--as-of") options.asOf = value; + else if (argument === "--mode") options.mode = value; + else if (argument === "--page-schema") options.pageSchema = path.resolve(process.cwd(), value); + else options.expiringWindowDays = Number(value); + continue; + } + failures.push(failure("argument_unknown", "", `unsupported argument ${argument}`)); + } + if (options.records.length === 0) failures.push(failure("record_required", "", "at least one --record is required")); + if (options.asOf === null) failures.push(failure("as_of_required", "", "--as-of is required for deterministic freshness auditing")); + else if (!Number.isFinite(parseDateTime(options.asOf))) failures.push(failure("as_of_invalid", "", "--as-of must be an RFC 3339 date-time value")); + if (!["advisory", "blocking"].includes(options.mode)) failures.push(failure("mode_invalid", "", "--mode must be advisory or blocking")); + if (!Number.isFinite(options.expiringWindowDays) || options.expiringWindowDays < 0 || options.expiringWindowDays > 3650) failures.push(failure("expiring_window_invalid", "", "--expiring-within-days must be a number from 0 through 3650")); + return { failures, options }; +} + +export function auditEvidenceFreshness({ records, asOf, mode = "advisory", root = repositoryRoot, pageSchema = defaultPageSchemaPath, expiringWindowDays = DEFAULT_EXPIRING_WINDOW_DAYS }) { + const asOfMs = parseDateTime(asOf); + const failures = []; + const warnings = []; + const entries = []; + if (!Number.isFinite(asOfMs)) failures.push(failure("as_of_invalid", "", "asOf must be an RFC 3339 date-time value")); + for (const record of records) { + const file = path.resolve(root, record); + const result = auditOne({ asOf, asOfMs, expiringWindowMs: expiringWindowDays * 24 * 60 * 60 * 1000, file, mode, pageSchemaPath: pageSchema, root }); + failures.push(...result.failures); + warnings.push(...result.warnings); + if (result.record) entries.push(result.record); + } + const uniqueFailures = [...new Map(failures.map((item) => [`${item.code}:${item.path}:${item.message}`, item])).values()]; + const uniqueWarnings = [...new Map(warnings.map((item) => [`${item.code}:${item.source_path}:${item.threshold_date}`, item])).values()]; + return { + asOf, + expiring_window_days: expiringWindowDays, + failures: uniqueFailures, + mode, + ok: uniqueFailures.length === 0, + records: entries, + warnings: uniqueWarnings, + }; +} + +export function runCli(argv = process.argv.slice(2), root = repositoryRoot) { + const parsed = parseArguments(argv); + if (parsed.failures.length > 0) return { asOf: parsed.options.asOf, failures: parsed.failures, mode: parsed.options.mode, ok: false, records: [], warnings: [] }; + return auditEvidenceFreshness({ + asOf: parsed.options.asOf, + expiringWindowDays: parsed.options.expiringWindowDays, + mode: parsed.options.mode, + pageSchema: parsed.options.pageSchema, + records: parsed.options.records, + root, + }); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + const parsed = parseArguments(); + const report = parsed.failures.length > 0 + ? { asOf: parsed.options.asOf, failures: parsed.failures, mode: parsed.options.mode, ok: false, records: [], warnings: [] } + : runCli(); + if (parsed.options.json || parsed.failures.length > 0) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + else if (report.ok) process.stdout.write(`ok: audited ${report.records.length} evidence record(s)\n`); + else process.stderr.write(`${report.failures.map((item) => `${item.code}: ${item.message}`).join("\n")}\n`); + process.exitCode = report.ok ? 0 : 1; + } catch (error) { + const report = { failures: [failure("freshness_tool_error", "", error instanceof Error ? error.message : String(error))], ok: false, records: [], warnings: [] }; + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + process.exitCode = 1; + } +} diff --git a/scripts/test-audit-evidence-freshness.mjs b/scripts/test-audit-evidence-freshness.mjs new file mode 100644 index 0000000..3be4d26 --- /dev/null +++ b/scripts/test-audit-evidence-freshness.mjs @@ -0,0 +1,177 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { cleanupFixture, completeSession, initializeConsumer } from "./page-evidence-fixture.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const auditor = path.join(repositoryRoot, "scripts", "audit-evidence-freshness.mjs"); +const calibration = path.join(repositoryRoot, "consumer-reference", "baselines", "calibration.json"); +const selectedCase = process.argv.find((argument) => argument === "--case") ? process.argv[process.argv.indexOf("--case") + 1] : null; +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-freshness-")); + +function writeJson(file, value) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function run(args) { + const child = spawnSync(process.execPath, [auditor, ...args, "--json"], { cwd: repositoryRoot, encoding: "utf8" }); + let report; + try { + report = JSON.parse(child.stdout); + } catch (error) { + report = null; + } + return { child, report }; +} + +function result(name, expected, actual, ok) { + return { actual, expected, name, ok }; +} + +function pageSchema() { + return { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: "https://stylegallery.test/page-evidence-manifest.schema.json", + additionalProperties: false, + properties: { + record_kind: { const: "page_evidence_manifest" }, + review_by: { format: "date-time", type: "string" }, + schema_version: { const: "1.0" }, + }, + required: ["record_kind", "review_by", "schema_version"], + type: "object", + }; +} + +function pageFixture(file, reviewBy) { + writeJson(file, { record_kind: "page_evidence_manifest", review_by: reviewBy, schema_version: "1.0" }); +} + +function selected(cases) { + if (!selectedCase) return cases; + const match = cases.filter((testCase) => testCase.name === selectedCase); + if (match.length === 0) { + return [result(selectedCase, "known --case", { cases: cases.map((testCase) => testCase.name) }, false)]; + } + return match; +} + +const pageSchemaPath = path.join(tempRoot, "page-evidence-manifest.schema.json"); +writeJson(pageSchemaPath, pageSchema()); +const pagePreDeadline = path.join(tempRoot, "review-window.json"); +pageFixture(pagePreDeadline, "2026-07-27T00:00:00Z"); +const pagePastDeadline = path.join(tempRoot, "review-past-deadline.json"); +pageFixture(pagePastDeadline, "2026-07-20T00:00:00Z"); +const pageAtDeadline = path.join(tempRoot, "review-at-deadline.json"); +pageFixture(pageAtDeadline, "2026-07-21T00:00:00Z"); +const pageFresh = path.join(tempRoot, "review-fresh.json"); +pageFixture(pageFresh, "2026-08-20T00:00:00Z"); +const malformed = path.join(tempRoot, "malformed.json"); +fs.writeFileSync(malformed, '{"record_kind":"page_evidence_manifest","review_by":"not-a-date","schema_version":"1.0"}\n'); +const unknown = path.join(tempRoot, "unknown.json"); +writeJson(unknown, { record_kind: "unsupported_evidence_manifest", schema_version: "1.0" }); +const missing = path.join(tempRoot, "missing.json"); +const missingPageSchema = path.join(tempRoot, "missing-page-evidence-manifest.schema.json"); +const completedPageFixture = initializeConsumer(); +const completedPage = completeSession(completedPageFixture); +if (completedPage.validate?.status !== 0) throw new Error(`full page fixture failed: ${JSON.stringify(completedPage.validate?.report ?? completedPage.finalize?.report)}`); +const completeManifest = JSON.parse(fs.readFileSync(path.join(completedPage.session.artifactRoot, "page-evidence-manifest.json"), "utf8")); +const impossibleDateRecords = [ + ["impossible-session-started-at", (value) => { value.session.started_at = "2023-02-30T12:00:00Z"; }], + ["impossible-session-completed-at", (value) => { value.session.completed_at = "2023-02-30T12:00:00Z"; }], + ["impossible-manifest-completed-at", (value) => { value.completed_at = "2023-02-30T12:00:00Z"; }], + ["impossible-scenario-recorded-at", (value) => { value.scenarios[0].recorded_at = "2023-02-30T12:00:00Z"; }], +].map(([name, mutate]) => { + const value = structuredClone(completeManifest); + mutate(value); + const file = path.join(tempRoot, `${name}.json`); + writeJson(file, value); + return { file, name }; +}); + +const cases = [ + (() => { + const actual = run(["--record", pageFresh, "--page-schema", pageSchemaPath, "--as-of", "2023-02-30T12:00:00Z", "--mode", "advisory"]); + return result("impossible-as-of", "exit nonzero with as_of_invalid", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status }, actual.child.status !== 0 && actual.report?.failures?.some((item) => item.code === "as_of_invalid")); + })(), + ...impossibleDateRecords.map(({ file, name }) => (() => { + const actual = run(["--record", file, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result(name, "exit nonzero with freshness_record_invalid", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status }, actual.child.status !== 0 && actual.report?.failures?.some((item) => item.code === "freshness_record_invalid")); + })()), + (() => { + const actual = run(["--record", calibration, "--as-of", "2026-07-01T00:00:00Z", "--mode", "advisory"]); + return result("fresh-calibration", "exit 0 with status fresh and no warnings", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status === 0 && actual.report?.ok === true && actual.report.records[0]?.status === "fresh" && actual.report.warnings.length === 0); + })(), + (() => { + const actual = run(["--record", calibration, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("expiring-calibration-advisory", "exit 0 with expiring status and evidence_expiring warning", { codes: actual.report?.warnings?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status === 0 && actual.report?.ok === true && actual.report.records[0]?.status === "expiring" && actual.report.warnings.some((item) => item.code === "evidence_expiring")); + })(), + (() => { + const actual = run(["--record", calibration, "--as-of", "2026-07-28T00:00:00Z", "--mode", "advisory"]); + return result("expired-calibration-advisory", "exit 0 with expired status and warning", { codes: actual.report?.warnings?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status === 0 && actual.report?.ok === true && actual.report.records[0]?.status === "expired" && actual.report.warnings.some((item) => item.code === "evidence_expired")); + })(), + (() => { + const actual = run(["--record", pagePreDeadline, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("pre-deadline-page-advisory", "exit 0 with expiring status and evidence_expiring warning", { codes: actual.report?.warnings?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status === 0 && actual.report?.ok === true && actual.report.records[0]?.status === "expiring" && actual.report.warnings.some((item) => item.code === "evidence_expiring")); + })(), + (() => { + const actual = run(["--record", pagePreDeadline, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "blocking"]); + return result("pre-deadline-review-window-blocking", "exit 0 with expiring status, advisory evidence_expiring warning, and no blocking failure", { failureCodes: actual.report?.failures?.map((item) => item.code), warningCodes: actual.report?.warnings?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status === 0 && actual.report?.ok === true && actual.report.records[0]?.status === "expiring" && actual.report.warnings.some((item) => item.code === "evidence_expiring") && actual.report.failures.length === 0); + })(), + (() => { + const actual = run(["--record", pagePastDeadline, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("review-due-page-advisory", "exit 0 with review_due status and evidence_review_due warning", { codes: actual.report?.warnings?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status === 0 && actual.report?.ok === true && actual.report.records[0]?.status === "review_due" && actual.report.warnings.some((item) => item.code === "evidence_review_due")); + })(), + (() => { + const before = fs.readFileSync(calibration); + const actual = run(["--record", calibration, "--record", pagePreDeadline, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + const after = fs.readFileSync(calibration); + return result("repeated-record-and-source-immutability", "two records are audited and calibration bytes remain unchanged", { bytesUnchanged: before.equals(after), count: actual.report?.records?.length, status: actual.child.status }, actual.child.status === 0 && actual.report?.records?.length === 2 && before.equals(after)); + })(), + (() => { + const actual = run(["--record", pageFresh, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("fresh-page-review", "exit 0 with fresh page evidence", { status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status === 0 && actual.report?.records[0]?.status === "fresh"); + })(), + (() => { + const actual = run(["--record", malformed, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("malformed-advisory", "exit nonzero with freshness_record_invalid", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status }, actual.child.status !== 0 && actual.report?.ok === false && actual.report.failures.some((item) => item.code === "freshness_record_invalid")); + })(), + (() => { + const actual = run(["--record", unknown, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("unknown-record-kind", "exit nonzero with freshness_record_invalid", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status }, actual.child.status !== 0 && actual.report?.failures.some((item) => item.code === "freshness_record_invalid")); + })(), + (() => { + const actual = run(["--record", missing, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("unreadable-record", "exit nonzero with freshness_record_unreadable", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status }, actual.child.status !== 0 && actual.report?.failures.some((item) => item.code === "freshness_record_unreadable")); + })(), + (() => { + const actual = run(["--record", pagePreDeadline, "--page-schema", missingPageSchema, "--as-of", "2026-07-21T00:00:00Z", "--mode", "advisory"]); + return result("missing-page-schema", "exit nonzero with freshness_schema_missing", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status }, actual.child.status !== 0 && actual.report?.failures.some((item) => item.code === "freshness_schema_missing")); + })(), + (() => { + const actual = run(["--record", calibration, "--as-of", "2026-07-28T00:00:00Z", "--mode", "blocking"]); + return result("expired-calibration-blocking", "exit nonzero with evidence_expired", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status }, actual.child.status !== 0 && actual.report?.failures.some((item) => item.code === "evidence_expired")); + })(), + (() => { + const actual = run(["--record", pagePastDeadline, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "blocking"]); + return result("review-due-blocking", "exit nonzero with evidence_review_due", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status !== 0 && actual.report?.ok === false && actual.report.records[0]?.status === "review_due" && actual.report.failures.some((item) => item.code === "evidence_review_due")); + })(), + (() => { + const actual = run(["--record", pageAtDeadline, "--page-schema", pageSchemaPath, "--as-of", "2026-07-21T00:00:00Z", "--mode", "blocking"]); + return result("page-at-review-deadline-blocking", "exit nonzero with review_due/evidence_review_due at the deadline", { codes: actual.report?.failures?.map((item) => item.code), status: actual.child.status, statuses: actual.report?.records?.map((item) => item.status) }, actual.child.status !== 0 && actual.report?.ok === false && actual.report.records[0]?.status === "review_due" && actual.report.failures.some((item) => item.code === "evidence_review_due")); + })(), +]; + +try { + const report = { ok: selected(cases).every((testCase) => testCase.ok), results: selected(cases) }; + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + process.exitCode = report.ok ? 0 : 1; +} finally { + cleanupFixture(completedPageFixture); + fs.rmSync(tempRoot, { force: true, recursive: true }); +} From 8f999c266b7a7e78e7fcbb4f2a013018acd44b82 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:36:59 +0900 Subject: [PATCH 41/44] feat(conformance): validate consumer evidence end to end Plan-Task: 5 --- .../consumer-conformance/e2e-fixture.mjs | 282 +++++++++++++ .../consumer-conformance/manifest.json | 64 +++ .../consumer-conformance/runtime-proof.mjs | 32 ++ .../valid-migration-result.json | 18 + .../consumer-conformance/valid-migration.json | 29 +- .../consumer-conformance-record.schema.json | 47 ++- scripts/consumer-conformance-contract.mjs | 68 ++++ .../test-validate-consumer-conformance.mjs | 301 +++++++++++++- scripts/validate-consumer-conformance.mjs | 373 +++++++++++++++++- 9 files changed, 1181 insertions(+), 33 deletions(-) create mode 100644 consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs create mode 100644 consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs create mode 100644 consumer-reference/fixtures/consumer-conformance/valid-migration-result.json diff --git a/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs b/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs new file mode 100644 index 0000000..be7b5fa --- /dev/null +++ b/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs @@ -0,0 +1,282 @@ +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync, spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { canonicalSourceManifest, digest } from "../../../scripts/page-evidence-contract.mjs"; +import { + cleanupFixture, + finalizeSession, + initializeConsumer, + startSession, + writeJson, + writeRunner, +} from "../../../scripts/page-evidence-fixture.mjs"; + +const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const validator = path.join(styleGalleryRoot, "scripts", "validate-consumer-conformance.mjs"); + +function runValidator(root, record, { artifactRoot, priorManifest } = {}) { + const args = [validator, "--root", root, "--record", record]; + if (artifactRoot) args.push("--artifact-root", artifactRoot); + if (priorManifest) args.push("--prior-manifest", priorManifest); + args.push("--json"); + const child = spawnSync(process.execPath, args, { cwd: styleGalleryRoot, encoding: "utf8" }); + let output; + try { output = JSON.parse(child.stdout); } + catch { output = { failures: [], parse_error: child.stderr || child.stdout }; } + return { output, status: child.status }; +} + +function codes(child) { + return child?.report?.failures?.map((entry) => entry.code) + ?? child?.output?.failures?.map((entry) => entry.code) + ?? []; +} + +function outcome(name, expected, child, observe = {}) { + const actualCodes = codes(child); + const childOk = expected === "ok:true" + ? child?.status === 0 && (child.report?.ok === true || child.output?.ok === true) + : child?.status !== 0 && actualCodes.includes(expected); + const ok = childOk && !Object.values(observe).includes(false); + return { actual: { codes: actualCodes, status: child?.status, ...observe }, expected, name, ok }; +} + +function sourceManifest(fixture) { + const failures = []; + const source = canonicalSourceManifest(fixture.root, fixture.relevantSources, failures); + if (!source || failures.length > 0) throw new Error(`source fixture failed: ${JSON.stringify(failures)}`); + return source; +} + +function createSession(fixture, template, options = {}) { + const artifactName = options.artifactName ?? "migration"; + const scenarioId = options.scenarioId ?? "migration-round-trip"; + const session = { + artifactRoot: path.join(fixture.root, "evidence", artifactName), + handoffFile: path.join(fixture.root, "records", `${artifactName}-handoff.json`), + recordFile: path.join(fixture.root, "records", `${artifactName}.json`), + runId: options.runId ?? `run-${artifactName}`, + scenarioId, + sessionId: options.sessionId ?? `session-${artifactName}`, + }; + const value = structuredClone(template); + const styleGalleryRevision = execFileSync("git", ["rev-parse", "HEAD"], { cwd: styleGalleryRoot, encoding: "utf8" }).trim(); + value.id = `${artifactName}-migration`; + value.consumer = { relevant_sources: fixture.relevantSources, repository: fixture.repository, revision: fixture.revision }; + value.page_evidence = { manifest: `evidence/${artifactName}/page-evidence-manifest.json`, status: "applicable" }; + value.scenarios[0] = { + ...value.scenarios[0], + argv: ["node", "tests/page.spec.js"], + evidence_method: "browser", + id: scenarioId, + result_artifact: `evidence/${artifactName}/runner/${scenarioId}.json`, + run_id: session.runId, + session_id: session.sessionId, + source_digest: sourceManifest(fixture).sha256, + }; + for (const dimension of Object.values(value.migration_dimensions)) if (dimension.status === "applicable") dimension.scenario_ids = [scenarioId]; + for (const mapping of value.adoption_mappings) { + mapping.scenario_ids = [scenarioId]; + mapping.stylegallery.revision = styleGalleryRevision; + } + options.mutateRecord?.(value); + writeJson(session.recordFile, value); + writeJson(session.handoffFile, { consumer_conformance_record: `records/${artifactName}.json`, consumer_migration_readiness: "declared" }); + return session; +} + +function startedSession(fixture, template, options) { + const session = createSession(fixture, template, options); + const start = startSession(fixture, session); + if (start.status !== 0) throw new Error(`session start failed: ${JSON.stringify(start.report)}`); + return session; +} + +function isolated(template, run) { + const fixture = initializeConsumer(); + try { return run(fixture, template); } + finally { cleanupFixture(fixture); } +} + +export function initializeCompletedConsumer(template) { + const fixture = initializeConsumer(); + try { + const session = startedSession(fixture, template); + const finalized = finalizeSession(fixture, session, writeRunner(fixture, session)); + if (finalized.status !== 0) throw new Error(`session finalize failed: ${JSON.stringify(finalized.report)}`); + return { ...fixture, artifactRoot: session.artifactRoot, recordReference: "records/migration.json" }; + } catch (error) { + cleanupFixture(fixture); + throw error; + } +} + +export function cleanupCompletedConsumer(fixture) { + cleanupFixture(fixture); +} + +function mutatedCompletedPacket(template, { caseName, expected, mutateRecord }) { + return isolated(template, (fixture, source) => { + const session = startedSession(fixture, source, { artifactName: caseName }); + const finalized = finalizeSession(fixture, session, writeRunner(fixture, session)); + if (finalized.status !== 0) throw new Error(`session finalize failed: ${JSON.stringify(finalized.report)}`); + const value = JSON.parse(fs.readFileSync(session.recordFile, "utf8")); + mutateRecord(value); + writeJson(session.recordFile, value); + return outcome(caseName, expected, runValidator(fixture.root, `records/${caseName}.json`, { artifactRoot: session.artifactRoot })); + }); +} + +const cases = { + "end-to-end-consumer": (template) => isolated(template, (fixture, source) => { + const session = startedSession(fixture, source); + const finalized = finalizeSession(fixture, session, writeRunner(fixture, session)); + if (finalized.status !== 0) throw new Error(`session finalize failed: ${JSON.stringify(finalized.report)}`); + const handoff = JSON.parse(fs.readFileSync(session.handoffFile, "utf8")); + const child = runValidator(fixture.root, handoff.consumer_conformance_record, { artifactRoot: session.artifactRoot }); + return outcome("end-to-end-consumer", "ok:true", child, { + artifactRootOutsideStyleGallery: !session.artifactRoot.startsWith(styleGalleryRoot), + checkedDimensions: child.output.checkedDimensions, + checkedMappings: child.output.checkedMappings, + checkedPageArtifacts: child.output.checkedPageArtifacts, + checkedPageScenarios: child.output.checkedPageScenarios, + handoffLinked: handoff.consumer_migration_readiness === "declared" && handoff.consumer_conformance_record === "records/migration.json", + pageSessionId: child.output.pageSessionId, + }); + }), + "missing-mapping": (template) => isolated(template, (fixture, source) => { + const session = startedSession(fixture, source, { mutateRecord: (value) => { + const unit = { ...value.scenarios[0], evidence_method: "unit", id: "unit-migration", result_artifact: "evidence/results/unit-migration.json" }; + value.scenarios.push(unit); + value.adoption_mappings[0].scenario_ids = [unit.id]; + } }); + const finalized = finalizeSession(fixture, session, writeRunner(fixture, session)); + if (finalized.status !== 0) throw new Error(`session finalize failed: ${JSON.stringify(finalized.report)}`); + return outcome("missing-mapping", "page_evidence_adoption_mapping_missing", runValidator(fixture.root, "records/migration.json", { artifactRoot: session.artifactRoot })); + }), + "applicable-without-runtime": (template) => isolated(template, (fixture, source) => { + const session = createSession(fixture, source, { mutateRecord: (value) => { value.scenarios[0].evidence_method = "unit"; } }); + fs.mkdirSync(session.artifactRoot, { recursive: true }); + return outcome("applicable-without-runtime", "page_evidence_runtime_scenario_required", runValidator(fixture.root, "records/migration.json", { artifactRoot: session.artifactRoot })); + }), + "dirty-source": (template) => isolated(template, (fixture, source) => { + const session = createSession(fixture, source); + fs.appendFileSync(path.join(fixture.root, fixture.relevantSources[0]), "// dirty\n"); + return outcome("dirty-source", "page_evidence_source_dirty", startSession(fixture, session)); + }), + "failed-runner": (template) => isolated(template, (fixture, source) => { + const session = startedSession(fixture, source); + return outcome("failed-runner", "page_evidence_runner_failed", finalizeSession(fixture, session, writeRunner(fixture, session, { status: "failed" }))); + }), + "cross-session-reuse": (template) => isolated(template, (fixture, source) => { + const target = startedSession(fixture, source, { artifactName: "target" }); + const alternate = startedSession(fixture, source, { artifactName: "alternate" }); + const targetRunner = writeRunner(fixture, target); + const alternateRunner = writeRunner(fixture, alternate); + fs.copyFileSync(alternateRunner.runnerFile, targetRunner.runnerFile); + fs.copyFileSync(path.join(alternate.artifactRoot, alternateRunner.artifactReference), path.join(target.artifactRoot, targetRunner.artifactReference)); + return outcome("cross-session-reuse", "page_evidence_session_mismatch", finalizeSession(fixture, target, targetRunner)); + }), + "unmanifested-artifact": (template) => isolated(template, (fixture, source) => { + const session = startedSession(fixture, source); + const runner = writeRunner(fixture, session); + fs.writeFileSync(path.join(session.artifactRoot, "rogue.png"), "rogue"); + return outcome("unmanifested-artifact", "page_evidence_artifact_unmanifested", finalizeSession(fixture, session, runner)); + }), + "reuse-source-drift": (template) => isolated(template, (fixture, source) => { + const prior = startedSession(fixture, source, { artifactName: "prior" }); + const priorFinalized = finalizeSession(fixture, prior, writeRunner(fixture, prior)); + if (priorFinalized.status !== 0) throw new Error(`prior finalize failed: ${JSON.stringify(priorFinalized.report)}`); + fs.appendFileSync(path.join(fixture.root, fixture.relevantSources[0]), "// next revision\n"); + execFileSync("git", ["add", fixture.relevantSources[0]], { cwd: fixture.root }); + execFileSync("git", ["commit", "-m", "next fixture revision"], { cwd: fixture.root, stdio: "ignore" }); + fixture.revision = execFileSync("git", ["rev-parse", "HEAD"], { cwd: fixture.root, encoding: "utf8" }).trim(); + const current = startedSession(fixture, source, { artifactName: "current" }); + const priorManifest = path.join(prior.artifactRoot, "page-evidence-manifest.json"); + const evidence = { + kind: "reused", + prior_manifest_sha256: digest(fs.readFileSync(priorManifest)), + prior_run_id: prior.runId, + prior_scenario_id: prior.scenarioId, + prior_session_id: prior.sessionId, + }; + const runner = writeRunner(fixture, current, { evidence, writeArtifact: false }); + return outcome("reuse-source-drift", "page_evidence_reuse_source_mismatch", finalizeSession(fixture, current, runner, priorManifest)); + }), + "source-set-mismatch": (template) => mutatedCompletedPacket(template, { + caseName: "source-set-mismatch", + expected: "page_evidence_source_set_mismatch", + mutateRecord: (value) => { value.consumer.relevant_sources = value.consumer.relevant_sources.slice(0, 1); }, + }), + "repository-mismatch": (template) => mutatedCompletedPacket(template, { + caseName: "repository-mismatch", + expected: "page_evidence_repository_mismatch", + mutateRecord: (value) => { value.consumer.repository = "other/consumer"; }, + }), + "revision-mismatch": (template) => mutatedCompletedPacket(template, { + caseName: "revision-mismatch", + expected: "page_evidence_revision_mismatch", + mutateRecord: (value) => { value.consumer.revision = "0".repeat(40); }, + }), + "source-digest-mismatch": (template) => mutatedCompletedPacket(template, { + caseName: "source-digest-mismatch", + expected: "page_evidence_source_mismatch", + mutateRecord: (value) => { value.scenarios[0].source_digest = "0".repeat(64); }, + }), + "intent-scenario-mismatch": (template) => mutatedCompletedPacket(template, { + caseName: "intent-scenario-mismatch", + expected: "page_evidence_intent_scenario_mismatch", + mutateRecord: (value) => { + const additional = { ...value.scenarios[0], id: "secondary-browser", result_artifact: "evidence/intent-scenario-mismatch/runner/secondary-browser.json" }; + value.scenarios.push(additional); + value.migration_dimensions.route_parity.scenario_ids.push(additional.id); + value.adoption_mappings[0].scenario_ids.push(additional.id); + }, + }), + "review-by-missing": (template) => isolated(template, (fixture, source) => { + const session = startedSession(fixture, source, { artifactName: "review-by-missing" }); + const finalized = finalizeSession(fixture, session, writeRunner(fixture, session)); + if (finalized.status !== 0) throw new Error(`session finalize failed: ${JSON.stringify(finalized.report)}`); + const manifestFile = path.join(session.artifactRoot, "page-evidence-manifest.json"); + const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8")); + delete manifest.review_by; + writeJson(manifestFile, manifest); + return outcome("review-by-missing", "page_evidence_manifest_schema_invalid", runValidator(fixture.root, "records/review-by-missing.json", { artifactRoot: session.artifactRoot })); + }), + "manifest-network-url": (template) => isolated(template, (fixture, source) => { + const session = createSession(fixture, source, { artifactName: "manifest-network-url", mutateRecord: (value) => { + value.page_evidence.manifest = "https://example.com/page-evidence-manifest.json"; + } }); + fs.mkdirSync(session.artifactRoot, { recursive: true }); + const child = runValidator(fixture.root, "records/manifest-network-url.json", { artifactRoot: session.artifactRoot }); + return outcome("manifest-network-url", "consumer_conformance_schema_invalid", child, { + noRuntimeExecution: child.output.checkedRuntimeCommands === 0, + }); + }), + "manifest-symlink": (template) => isolated(template, (fixture, source) => { + const session = startedSession(fixture, source, { artifactName: "manifest-symlink" }); + const finalized = finalizeSession(fixture, session, writeRunner(fixture, session)); + if (finalized.status !== 0) throw new Error(`session finalize failed: ${JSON.stringify(finalized.report)}`); + const manifest = path.join(session.artifactRoot, "page-evidence-manifest.json"); + const redirected = path.join(session.artifactRoot, "redirected-manifest.json"); + fs.renameSync(manifest, redirected); + fs.symlinkSync(redirected, manifest); + return outcome("manifest-symlink", "page_evidence_manifest_symlink", runValidator(fixture.root, "records/manifest-symlink.json", { artifactRoot: session.artifactRoot })); + }), + "artifact-root-redirect": (template) => isolated(template, (fixture, source) => { + const session = startedSession(fixture, source, { artifactName: "artifact-root-redirect" }); + const finalized = finalizeSession(fixture, session, writeRunner(fixture, session)); + if (finalized.status !== 0) throw new Error(`session finalize failed: ${JSON.stringify(finalized.report)}`); + const redirected = `${session.artifactRoot}-redirected`; + fs.renameSync(session.artifactRoot, redirected); + fs.symlinkSync(redirected, session.artifactRoot); + return outcome("artifact-root-redirect", "page_evidence_artifact_symlink", runValidator(fixture.root, "records/artifact-root-redirect.json", { artifactRoot: session.artifactRoot })); + }), +}; + +export const endToEndCaseNames = Object.freeze(Object.keys(cases)); + +export function runEndToEndCase(name, template) { + return cases[name](template); +} diff --git a/consumer-reference/fixtures/consumer-conformance/manifest.json b/consumer-reference/fixtures/consumer-conformance/manifest.json index a8da42f..e8f1c97 100644 --- a/consumer-reference/fixtures/consumer-conformance/manifest.json +++ b/consumer-reference/fixtures/consumer-conformance/manifest.json @@ -25,9 +25,73 @@ "expected_code": "adoption_stylegallery_revision_unpinned", "name": "unpinned-stylegallery-revision" }, + { + "expected_code": "adoption_stylegallery_revision_unknown", + "name": "unknown-stylegallery-revision" + }, + { + "expected_code": "adoption_stylegallery_path_missing", + "name": "missing-stylegallery-path" + }, + { + "expected_code": "adoption_stylegallery_anchor_missing", + "name": "missing-stylegallery-anchor" + }, { "expected_code": "adoption_debt_incomplete", "name": "debt-without-lifecycle" + }, + { + "expected_code": "consumer_source_missing", + "name": "missing-source" + }, + { + "expected_code": "consumer_source_symlink", + "name": "symlink-source" + }, + { + "expected_code": "runtime_command_checkout_symlink", + "name": "checkout-symlink" + }, + { + "expected_code": "runtime_result_artifact_missing", + "name": "missing-result-artifact" + }, + { + "expected_code": "runtime_result_artifact_symlink", + "name": "symlink-result-artifact" + }, + { + "expected_code": "runtime_result_artifact_json_invalid", + "name": "invalid-result-artifact-json" + }, + { + "expected_code": "runtime_result_artifact_mismatch", + "name": "fabricated-result-artifact" + }, + { + "expected_code": "consumer_repository_mismatch", + "name": "fabricated-repository" + }, + { + "expected_code": "consumer_revision_unknown", + "name": "fabricated-revision" + }, + { + "expected_code": "runtime_source_digest_mismatch", + "name": "fabricated-source-digest" + }, + { + "expected_code": "runtime_command_failed", + "name": "nonzero-command" + }, + { + "expected_code": "runtime_command_result_missing", + "name": "forged-noop-result" + }, + { + "expected_code": "consumer_repository_mismatch", + "name": "github-repository-spoof" } ], "schema_record": "consumer-reference/schema/consumer-conformance-record.schema.json", diff --git a/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs b/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs new file mode 100644 index 0000000..0e59e47 --- /dev/null +++ b/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const resultReference = process.env.STYLEGALLERY_RESULT_ARTIFACT; +const contextValue = process.env.STYLEGALLERY_RUNTIME_CONTEXT; +assert.ok(resultReference && contextValue, "the validator must provide the result path and runtime context"); +assert.equal(path.posix.normalize(resultReference), resultReference); +assert.ok(!path.posix.isAbsolute(resultReference) && !resultReference.split("/").includes("..")); + +if (process.argv.includes("--lock-worktree")) { + const locked = spawnSync("git", ["worktree", "lock", "."], { encoding: "utf8" }); + assert.equal(locked.status, 0, locked.stderr); +} + +const context = JSON.parse(contextValue); +const persisted = JSON.parse(JSON.stringify({ drawer: "open", route: "/gallery", selection: null })); +assert.deepEqual(persisted, { drawer: "open", route: "/gallery", selection: null }); + +const output = { + ...context, + record_kind: "consumer_migration_scenario_result", + recorded_at: "2026-07-21T00:00:00Z", + schema_version: "1.0", + status: "passed", +}; +const resultFile = path.resolve(process.cwd(), resultReference); +fs.mkdirSync(path.dirname(resultFile), { recursive: true }); +fs.writeFileSync(resultFile, `${JSON.stringify(output, null, 2)}\n`, { flag: "wx" }); diff --git a/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json b/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json new file mode 100644 index 0000000..0aacaf8 --- /dev/null +++ b/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json @@ -0,0 +1,18 @@ +{ + "argv": [ + "node", + "consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs" + ], + "evidence_method": "integration", + "exit_code": 0, + "record_kind": "consumer_migration_scenario_result", + "recorded_at": "2026-07-21T00:00:00Z", + "repository": "changeroa/StyleGallery", + "revision": "84bf420d568f68021117f8142e6d982b4966de2f", + "run_id": "run-20260721-001", + "scenario_id": "migration-round-trip", + "schema_version": "1.0", + "session_id": "session-20260721-001", + "source_digest": "6d90893774e2228ffe9ae0286f5b3392b103ea1bf5e571fcde2f7a861d76372c", + "status": "passed" +} diff --git a/consumer-reference/fixtures/consumer-conformance/valid-migration.json b/consumer-reference/fixtures/consumer-conformance/valid-migration.json index 6ed4d72..8fe28c0 100644 --- a/consumer-reference/fixtures/consumer-conformance/valid-migration.json +++ b/consumer-reference/fixtures/consumer-conformance/valid-migration.json @@ -16,9 +16,9 @@ "migration-round-trip" ], "stylegallery": { - "anchor": "#implementation-handoff", - "path": "recipes/app-shell/index.md", - "revision": "2222222222222222222222222222222222222222" + "anchor": "#recommended-pattern-stack", + "path": "recipes/dashboard.md", + "revision": "66601da6e81c69674edf5c1b9b1b0862d4597fe9" } } ], @@ -29,11 +29,13 @@ }, "consumer": { "relevant_sources": [ - "src/app.js", - "src/styles.css" + "consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs", + "consumer-reference/schema/consumer-conformance-record.schema.json", + "scripts/consumer-conformance-contract.mjs", + "scripts/validate-consumer-conformance.mjs" ], - "repository": "example/consumer-web", - "revision": "1111111111111111111111111111111111111111" + "repository": "changeroa/StyleGallery", + "revision": "84bf420d568f68021117f8142e6d982b4966de2f" }, "id": "example-consumer-migration", "migration_dimensions": { @@ -120,17 +122,20 @@ "assertions": [ "The drawer state and persisted route data match after a reload." ], - "command": "npm run test:migration -- --reporter=json", - "evidence_method": "browser", + "argv": [ + "node", + "consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs" + ], + "evidence_method": "integration", "exit_code": 0, "id": "migration-round-trip", "observable_actions": [ - "Open the gallery route, toggle the drawer, save, and reload." + "Run the synthetic migration fixture and inspect its machine-readable result." ], - "result_artifact": "evidence/results/migration-round-trip.json", + "result_artifact": "consumer-reference/fixtures/consumer-conformance/valid-migration-result.json", "run_id": "run-20260721-001", "session_id": "session-20260721-001", - "source_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "source_digest": "6d90893774e2228ffe9ae0286f5b3392b103ea1bf5e571fcde2f7a861d76372c" } ], "schema_version": "1.0" diff --git a/consumer-reference/schema/consumer-conformance-record.schema.json b/consumer-reference/schema/consumer-conformance-record.schema.json index a0e2735..8bd39ec 100644 --- a/consumer-reference/schema/consumer-conformance-record.schema.json +++ b/consumer-reference/schema/consumer-conformance-record.schema.json @@ -149,6 +149,46 @@ "pattern": "^(?!/)(?!.*//)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?![A-Za-z][A-Za-z0-9+.-]*:)(?!.*\\/$)[^\\\\?#]+$", "type": "string" }, + "argv": { + "items": { "minLength": 1, "type": "string" }, + "maxItems": 64, + "minItems": 1, + "type": "array" + }, + "runtimeResult": { + "additionalProperties": false, + "properties": { + "argv": { "$ref": "#/$defs/argv" }, + "evidence_method": { "enum": ["unit", "integration", "browser"] }, + "exit_code": { "const": 0 }, + "record_kind": { "const": "consumer_migration_scenario_result" }, + "recorded_at": { "format": "date-time", "type": "string" }, + "repository": { "minLength": 3, "pattern": "^\\S+/\\S+$", "type": "string" }, + "revision": { "pattern": "^[a-f0-9]{40}$", "type": "string" }, + "run_id": { "minLength": 1, "pattern": "^\\S+$", "type": "string" }, + "scenario_id": { "$ref": "#/$defs/slug" }, + "schema_version": { "const": "1.0" }, + "session_id": { "minLength": 1, "pattern": "^\\S+$", "type": "string" }, + "source_digest": { "$ref": "#/$defs/sha256" }, + "status": { "const": "passed" } + }, + "required": [ + "argv", + "evidence_method", + "exit_code", + "record_kind", + "recorded_at", + "repository", + "revision", + "run_id", + "scenario_id", + "schema_version", + "session_id", + "source_digest", + "status" + ], + "type": "object" + }, "scenario": { "additionalProperties": false, "properties": { @@ -157,7 +197,7 @@ "minItems": 1, "type": "array" }, - "command": { "minLength": 1, "type": "string" }, + "argv": { "$ref": "#/$defs/argv" }, "evidence_method": { "enum": ["unit", "integration", "browser"] }, "exit_code": { "const": 0 }, "id": { "$ref": "#/$defs/slug" }, @@ -173,7 +213,7 @@ }, "required": [ "assertions", - "command", + "argv", "evidence_method", "exit_code", "id", @@ -227,6 +267,7 @@ "properties": { "adoption_mappings": { "items": { "$ref": "#/$defs/adoptionMapping" }, + "maxItems": 256, "minItems": 1, "type": "array" }, @@ -245,6 +286,7 @@ "properties": { "relevant_sources": { "items": { "$ref": "#/$defs/repositoryPath" }, + "maxItems": 1024, "minItems": 1, "type": "array", "uniqueItems": true @@ -301,6 +343,7 @@ "record_kind": { "const": "consumer_migration_conformance" }, "scenarios": { "items": { "$ref": "#/$defs/scenario" }, + "maxItems": 256, "minItems": 1, "type": "array" }, diff --git a/scripts/consumer-conformance-contract.mjs b/scripts/consumer-conformance-contract.mjs index 5899b12..fc98f55 100644 --- a/scripts/consumer-conformance-contract.mjs +++ b/scripts/consumer-conformance-contract.mjs @@ -31,6 +31,14 @@ function uniqueFindings(findings) { return [...new Map(findings.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()]; } +function sameStringSet(left, right) { + return Array.isArray(left) + && Array.isArray(right) + && left.length === right.length + && new Set(left).size === left.length + && left.every((entry) => right.includes(entry)); +} + export function isNormalizedRepositoryPath(value, { jsonOnly = false } = {}) { if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("//") || value.includes("?") || value.includes("#")) return false; if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return false; @@ -84,6 +92,7 @@ export function validateConsumerConformanceSemantics(value, recordPath) { if (scenarioIds.length !== scenarioSet.size) add("runtime_scenario_id_duplicate", "runtime scenario IDs must be unique"); for (const scenario of scenarios) { if (!RUNTIME_EVIDENCE_METHODS.includes(scenario.evidence_method)) add("runtime_evidence_method_invalid", "runtime evidence must use unit, integration, or browser execution"); + if (["unit", "integration"].includes(scenario.evidence_method) && scenario.argv?.[0] !== "node") add("runtime_command_executable_invalid", "unit and integration evidence must use the governed Node runtime"); if (scenario.exit_code !== 0) add("runtime_evidence_exit_nonzero", `scenario ${scenario.id ?? ""} must record exit code zero`); if (!isNormalizedRepositoryPath(scenario.result_artifact, { jsonOnly: true })) add("runtime_result_artifact_invalid", `scenario ${scenario.id ?? ""} requires a normalized JSON result artifact`); } @@ -128,3 +137,62 @@ export function validateConsumerConformanceSemantics(value, recordPath) { } return uniqueFindings(findings); } + +export function validateConsumerPageIntentSemantics(value, recordPath) { + if (!isPlainObject(value) || value.page_evidence?.status !== "applicable") return []; + const findings = []; + const add = (code, message) => findings.push(finding(code, message, recordPath)); + const scenarios = Array.isArray(value.scenarios) ? value.scenarios.filter(isPlainObject) : []; + const browserIds = scenarios.filter((scenario) => scenario.evidence_method === "browser").map((scenario) => scenario.id); + if (browserIds.length === 0) { + add("page_evidence_runtime_scenario_required", "applicable page evidence requires at least one browser runtime scenario"); + return findings; + } + const mappingIds = new Set((Array.isArray(value.adoption_mappings) ? value.adoption_mappings : []) + .flatMap((mapping) => Array.isArray(mapping?.scenario_ids) ? mapping.scenario_ids : [])); + for (const scenarioId of browserIds) { + if (!mappingIds.has(scenarioId)) add("page_evidence_adoption_mapping_missing", `browser scenario ${scenarioId} requires an adoption mapping`); + } + return uniqueFindings(findings); +} + +export function validateConsumerPageEvidenceSemantics(value, manifest, recordPath) { + if (!isPlainObject(value) || !isPlainObject(manifest) || value.page_evidence?.status !== "applicable") return []; + const findings = []; + const add = (code, message) => findings.push(finding(code, message, recordPath)); + const scenarios = Array.isArray(value.scenarios) ? value.scenarios.filter(isPlainObject) : []; + const scenarioById = new Map(scenarios.map((scenario) => [scenario.id, scenario])); + const browserScenarios = scenarios.filter((scenario) => scenario.evidence_method === "browser"); + const browserIds = browserScenarios.map((scenario) => scenario.id); + const intendedIds = manifest.session?.intended_scenario_ids; + if (!sameStringSet(browserIds, intendedIds)) add("page_evidence_intent_scenario_mismatch", "page session intent must exactly match the conformance browser scenario catalog"); + + const sourcePaths = manifest.session?.source?.files?.map((entry) => entry.path); + if (!sameStringSet(value.consumer?.relevant_sources, sourcePaths)) add("page_evidence_source_set_mismatch", "conformance relevant sources must exactly match the page session source set"); + if (value.consumer?.repository !== manifest.repository || value.consumer?.repository !== manifest.session?.repository || value.consumer?.repository !== manifest.run?.repository) { + add("page_evidence_repository_mismatch", "conformance, page session, manifest, and run repositories must agree"); + } + if (value.consumer?.revision !== manifest.revision || value.consumer?.revision !== manifest.session?.revision || value.consumer?.revision !== manifest.run?.revision) { + add("page_evidence_revision_mismatch", "conformance, page session, manifest, and run revisions must agree"); + } + if (typeof manifest.review_by !== "string" || manifest.review_by.length === 0) add("page_evidence_review_by_required", "page evidence manifest requires review_by"); + + const pageScenarioById = new Map((Array.isArray(manifest.scenarios) ? manifest.scenarios : []).map((scenario) => [scenario.id, scenario])); + const manifestDirectory = value.page_evidence.manifest.split("/").slice(0, -1).join("/"); + for (const scenario of browserScenarios) { + if (scenario.source_digest !== manifest.session?.source?.sha256) add("page_evidence_source_mismatch", `browser scenario ${scenario.id} source digest must equal the page session source digest`); + if (scenario.session_id !== manifest.session?.session_id) add("page_evidence_session_mismatch", `browser scenario ${scenario.id} session identity must equal the page session`); + if (scenario.run_id !== manifest.run?.id) add("page_evidence_run_mismatch", `browser scenario ${scenario.id} run identity must equal the page run`); + const runnerPath = pageScenarioById.get(scenario.id)?.runner_result?.path; + if (scenario.result_artifact !== (runnerPath ? `${manifestDirectory}/${runnerPath}` : undefined)) add("page_evidence_result_artifact_mismatch", `browser scenario ${scenario.id} result artifact must equal its page runner result`); + } + + const dimensions = isPlainObject(value.migration_dimensions) ? Object.values(value.migration_dimensions) : []; + for (const scenarioId of dimensions.flatMap((dimension) => dimension?.status === "applicable" && Array.isArray(dimension.scenario_ids) ? dimension.scenario_ids : [])) { + if (scenarioById.get(scenarioId)?.evidence_method === "browser" && !intendedIds?.includes(scenarioId)) add("page_evidence_dimension_scenario_missing", `browser scenario ${scenarioId} used by an applicable dimension is absent from page intent`); + } + for (const scenarioId of (Array.isArray(value.adoption_mappings) ? value.adoption_mappings : []).flatMap((mapping) => Array.isArray(mapping?.scenario_ids) ? mapping.scenario_ids : [])) { + if (scenarioById.get(scenarioId)?.evidence_method === "browser" && !intendedIds?.includes(scenarioId)) add("page_evidence_adoption_scenario_missing", `browser scenario ${scenarioId} used by an adoption mapping is absent from page intent`); + } + return uniqueFindings(findings); +} diff --git a/scripts/test-validate-consumer-conformance.mjs b/scripts/test-validate-consumer-conformance.mjs index 492d872..2217ced 100644 --- a/scripts/test-validate-consumer-conformance.mjs +++ b/scripts/test-validate-consumer-conformance.mjs @@ -3,22 +3,46 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { spawnSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { + endToEndCaseNames, + runEndToEndCase, +} from "../consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs"; +import { MIGRATION_DIMENSIONS, validateConsumerConformanceSemantics } from "./consumer-conformance-contract.mjs"; +import { canonicalSourceManifest } from "./page-evidence-contract.mjs"; import { parseStrictJson } from "./strict-json.mjs"; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const fixtureRoot = path.join(repositoryRoot, "consumer-reference", "fixtures", "consumer-conformance"); const validator = path.join(repositoryRoot, "scripts", "validate-consumer-conformance.mjs"); -const expectedCases = Object.freeze([ +const contractCases = Object.freeze([ ["omitted-dimension", "migration_dimension_required"], ["applicable-without-scenario", "migration_dimension_scenario_required"], ["source-regex-runtime-proof", "runtime_evidence_method_invalid"], ["orphan-adoption-scenario", "adoption_scenario_unknown"], ["malformed-local-target", "adoption_consumer_target_invalid"], ["unpinned-stylegallery-revision", "adoption_stylegallery_revision_unpinned"], + ["unknown-stylegallery-revision", "adoption_stylegallery_revision_unknown"], + ["missing-stylegallery-path", "adoption_stylegallery_path_missing"], + ["missing-stylegallery-anchor", "adoption_stylegallery_anchor_missing"], ["debt-without-lifecycle", "adoption_debt_incomplete"], ]); +const runtimeCases = Object.freeze([ + ["missing-source", "consumer_source_missing"], + ["symlink-source", "consumer_source_symlink"], + ["checkout-symlink", "runtime_command_checkout_symlink"], + ["missing-result-artifact", "runtime_result_artifact_missing"], + ["symlink-result-artifact", "runtime_result_artifact_symlink"], + ["invalid-result-artifact-json", "runtime_result_artifact_json_invalid"], + ["fabricated-result-artifact", "runtime_result_artifact_mismatch"], + ["fabricated-repository", "consumer_repository_mismatch"], + ["fabricated-revision", "consumer_revision_unknown"], + ["fabricated-source-digest", "runtime_source_digest_mismatch"], + ["nonzero-command", "runtime_command_failed"], + ["forged-noop-result", "runtime_command_result_missing"], + ["github-repository-spoof", "consumer_repository_mismatch", { GITHUB_REPOSITORY: "example/spoof" }], +]); function parseArguments(argv) { const options = { caseName: undefined, json: false }; @@ -48,10 +72,10 @@ function validateInventory(manifest) { const encodedCases = Array.isArray(manifest.invalid_cases) ? manifest.invalid_cases.map((entry) => Object.keys(entry).length === 2 ? `${entry.name}:${entry.expected_code}` : "") : []; - const expectedEncoded = expectedCases.map(([name, code]) => `${name}:${code}`); + const expectedEncoded = [...contractCases, ...runtimeCases].map(([name, code]) => `${name}:${code}`); const entries = fs.readdirSync(fixtureRoot, { withFileTypes: true }); - const closedFiles = entries.every((entry) => entry.isFile() && path.extname(entry.name) === ".json") - && sameSet(entries.map((entry) => entry.name), ["manifest.json", "valid-migration.json"]); + const closedFiles = entries.every((entry) => entry.isFile()) + && sameSet(entries.map((entry) => entry.name), ["e2e-fixture.mjs", "manifest.json", "runtime-proof.mjs", "valid-migration-result.json", "valid-migration.json"]); return Object.keys(manifest).length === expectedKeys.length && sameSet(Object.keys(manifest), expectedKeys) && manifest.id === "consumer-conformance-fixture-inventory" @@ -63,6 +87,118 @@ function validateInventory(manifest) { && closedFiles; } +function git(root, ...args) { + return execFileSync("git", ["-c", `safe.directory=${root}`, ...args], { cwd: root, encoding: "utf8" }).trim(); +} + +function writeFile(root, reference, content) { + const file = path.join(root, reference); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); +} + +function runtimeResult(value, overrides = {}) { + const scenario = value.scenarios[0]; + return { + argv: scenario.argv, + evidence_method: scenario.evidence_method, + exit_code: scenario.exit_code, + record_kind: "consumer_migration_scenario_result", + recorded_at: "2026-07-21T00:00:00Z", + repository: value.consumer.repository, + revision: value.consumer.revision, + run_id: scenario.run_id, + scenario_id: scenario.id, + schema_version: "1.0", + session_id: scenario.session_id, + source_digest: scenario.source_digest, + status: "passed", + ...overrides, + }; +} + +function initializeRuntimeFixture(source) { + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-runtime-"))); + execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "ignore" }); + git(root, "config", "user.email", "fixture@example.com"); + git(root, "config", "user.name", "StyleGallery fixture"); + git(root, "remote", "add", "origin", "https://github.com/example/consumer-web.git"); + writeFile(root, "src/app.js", "export const app = true;\n"); + writeFile(root, "src/styles.css", ".app { display: block; }\n"); + writeFile(root, "tests/migration.mjs", `import fs from "node:fs";\nimport path from "node:path";\nimport { spawnSync } from "node:child_process";\nif (process.argv.includes("--lock-worktree")) { const locked = spawnSync("git", ["worktree", "lock", "."]); if (locked.status !== 0) process.exit(18); }\nconst reference = process.env.STYLEGALLERY_RESULT_ARTIFACT;\nconst context = JSON.parse(process.env.STYLEGALLERY_RUNTIME_CONTEXT);\nconst file = path.resolve(process.cwd(), reference);\nfs.mkdirSync(path.dirname(file), { recursive: true });\nif (process.argv.includes("--oversized-result")) { const descriptor = fs.openSync(file, "wx"); fs.ftruncateSync(descriptor, 67108865); fs.closeSync(descriptor); process.exit(0); }\nfs.writeFileSync(file, JSON.stringify({ ...context, record_kind: "consumer_migration_scenario_result", recorded_at: "2026-07-21T00:00:00Z", schema_version: "1.0", status: "passed" }, null, 2) + "\\n", { flag: "wx" });\n`); + writeFile(root, "tests/failing.mjs", "process.exit(17);\n"); + writeFile(root, "tests/hanging.mjs", "setInterval(() => {}, 1000);\n"); + writeFile(root, "tests/noop.mjs", "// Intentionally produces no result artifact.\n"); + git(root, "add", "src/app.js", "src/styles.css", "tests/migration.mjs", "tests/failing.mjs", "tests/hanging.mjs", "tests/noop.mjs"); + git(root, "commit", "-m", "fixture sources"); + const cleanRevision = git(root, "rev-parse", "HEAD"); + fs.symlinkSync("app.js", path.join(root, "src/linked.js")); + git(root, "add", "src/linked.js"); + git(root, "commit", "-m", "fixture symlink source"); + const symlinkRevision = git(root, "rev-parse", "HEAD"); + git(root, "reset", "--hard", cleanRevision); + + const value = structuredClone(source); + value.consumer = { + relevant_sources: ["src/app.js", "src/styles.css", "tests/migration.mjs"], + repository: "example/consumer-web", + revision: git(root, "rev-parse", "HEAD"), + }; + const styleGalleryRevision = git(repositoryRoot, "rev-parse", "HEAD"); + for (const mapping of value.adoption_mappings) mapping.stylegallery.revision = styleGalleryRevision; + const sourceFailures = []; + const manifest = canonicalSourceManifest(root, value.consumer.relevant_sources, sourceFailures); + if (!manifest || sourceFailures.length > 0) throw new Error(`runtime fixture source manifest failed: ${JSON.stringify(sourceFailures)}`); + value.scenarios[0] = { + ...value.scenarios[0], + argv: ["node", "tests/migration.mjs"], + evidence_method: "integration", + result_artifact: "evidence/results/migration-round-trip.json", + source_digest: manifest.sha256, + }; + writeFile(root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + writeFile(root, "evidence/results/fabricated.json", `${JSON.stringify(runtimeResult(value, { scenario_id: "fabricated-scenario" }), null, 2)}\n`); + const failing = structuredClone(value); + failing.scenarios[0].argv = ["node", "tests/failing.mjs"]; + failing.scenarios[0].result_artifact = "evidence/results/nonzero-command.json"; + writeFile(root, failing.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(failing), null, 2)}\n`); + const noop = structuredClone(value); + noop.scenarios[0].argv = ["node", "tests/noop.mjs"]; + noop.scenarios[0].result_artifact = "evidence/results/forged-noop-result.json"; + writeFile(root, noop.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(noop), null, 2)}\n`); + writeFile(root, "evidence/results/invalid.json", "{not json}\n"); + fs.symlinkSync("migration-round-trip.json", path.join(root, "evidence/results/redirect.json")); + return { root, symlinkRevision, value }; +} + +function mutatedRuntimeRecord(name, source, fixture) { + const value = structuredClone(source); + if (name === "missing-source") value.consumer.relevant_sources[0] = "src/missing.js"; + else if (name === "symlink-source") { + value.consumer.relevant_sources[0] = "src/linked.js"; + value.consumer.revision = fixture.symlinkRevision; + } + else if (name === "checkout-symlink") value.consumer.revision = fixture.symlinkRevision; + else if (name === "missing-result-artifact") value.scenarios[0].result_artifact = "evidence/results/missing.json"; + else if (name === "symlink-result-artifact") value.scenarios[0].result_artifact = "evidence/results/redirect.json"; + else if (name === "invalid-result-artifact-json") value.scenarios[0].result_artifact = "evidence/results/invalid.json"; + else if (name === "fabricated-result-artifact") value.scenarios[0].result_artifact = "evidence/results/fabricated.json"; + else if (name === "fabricated-repository") value.consumer.repository = "fabricated/consumer"; + else if (name === "fabricated-revision") value.consumer.revision = "0".repeat(40); + else if (name === "fabricated-source-digest") value.scenarios[0].source_digest = "0".repeat(64); + else if (name === "nonzero-command") { + value.scenarios[0].argv = ["node", "tests/failing.mjs"]; + value.scenarios[0].result_artifact = "evidence/results/nonzero-command.json"; + } + else if (name === "forged-noop-result") { + value.scenarios[0].argv = ["node", "tests/noop.mjs"]; + value.scenarios[0].result_artifact = "evidence/results/forged-noop-result.json"; + } + else if (name === "github-repository-spoof") value.consumer.repository = "example/spoof"; + else throw new Error(`unknown runtime fixture case ${name}`); + return value; +} + function mutatedRecord(name, source) { const value = structuredClone(source); if (name === "omitted-dimension") delete value.migration_dimensions.exact_time_boundary; @@ -71,15 +207,19 @@ function mutatedRecord(name, source) { else if (name === "orphan-adoption-scenario") value.adoption_mappings[0].scenario_ids = ["missing-scenario"]; else if (name === "malformed-local-target") value.adoption_mappings[0].consumer_target = { identity: "app shell", kind: "selector" }; else if (name === "unpinned-stylegallery-revision") value.adoption_mappings[0].stylegallery.revision = "abc123"; + else if (name === "unknown-stylegallery-revision") value.adoption_mappings[0].stylegallery.revision = "0".repeat(40); + else if (name === "missing-stylegallery-path") value.adoption_mappings[0].stylegallery.path = "recipes/missing.md"; + else if (name === "missing-stylegallery-anchor") value.adoption_mappings[0].stylegallery.anchor = "#missing-anchor"; else if (name === "debt-without-lifecycle") value.adoption_mappings[0].debt = [{ summary: "Replace the temporary spacing bridge." }]; else throw new Error(`unknown fixture case ${name}`); return value; } -function runValidator(root, record) { +function runValidator(root, record, env = {}) { const child = spawnSync(process.execPath, [validator, "--root", root, "--record", record, "--json"], { cwd: repositoryRoot, encoding: "utf8", + env: { ...process.env, ...env }, }); let output; try { @@ -93,31 +233,168 @@ function runValidator(root, record) { const options = parseArguments(process.argv); const tempRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-conformance-"))); const results = []; +let runtimeFixture; try { const manifest = readJson(path.join(fixtureRoot, "manifest.json")); const inventoryOk = validateInventory(manifest) && fs.existsSync(path.join(repositoryRoot, manifest.schema_record)); results.push({ actual: inventoryOk, expected: true, name: "closed-fixture-inventory", ok: inventoryOk }); const valid = readJson(path.join(fixtureRoot, "valid-migration.json")); const selected = options.caseName ?? "all"; + for (const name of endToEndCaseNames) { + if (selected === "all" || selected === name) results.push(runEndToEndCase(name, valid)); + } if (selected === "all" || selected === "valid-migration") { - const child = runValidator(repositoryRoot, "consumer-reference/fixtures/consumer-conformance/valid-migration.json"); - const ok = child.status === 0 && child.output.ok === true && child.output.checkedDimensions === 13 && child.output.checkedMappings === 1; - results.push({ actual: { checkedDimensions: child.output.checkedDimensions, checkedMappings: child.output.checkedMappings, codes: child.output.failures?.map((entry) => entry.code) ?? [], status: child.status }, expected: "valid record, 13 dimensions, one mapping, exit:0", name: "valid-migration", ok }); + const codes = validateConsumerConformanceSemantics(valid, "valid-migration.json").map((entry) => entry.code); + const checkedDimensions = Object.keys(valid.migration_dimensions ?? {}).length; + const checkedMappings = valid.adoption_mappings?.length ?? 0; + const ok = codes.length === 0 && checkedDimensions === MIGRATION_DIMENSIONS.length && checkedMappings === 1; + results.push({ actual: { checkedDimensions, checkedMappings, codes }, expected: "valid static template, 13 dimensions, one mapping", name: "valid-migration", ok }); + } + if (selected === "all" || selected === "stale-template-rebound") { + const stale = structuredClone(valid); + stale.adoption_mappings[0].stylegallery.revision = "0".repeat(40); + const result = runEndToEndCase("end-to-end-consumer", stale); + results.push({ ...result, expected: "dynamic record rebound to the current StyleGallery checkout", name: "stale-template-rebound" }); } - for (const [name, expectedCode] of expectedCases) { + const contractTemplate = structuredClone(valid); + const styleGalleryRevision = git(repositoryRoot, "rev-parse", "HEAD"); + for (const mapping of contractTemplate.adoption_mappings) mapping.stylegallery.revision = styleGalleryRevision; + for (const [name, expectedCode] of contractCases) { if (selected !== "all" && selected !== name) continue; const record = path.join(tempRoot, `${name}.json`); - fs.writeFileSync(record, `${JSON.stringify(mutatedRecord(name, valid), null, 2)}\n`); + fs.writeFileSync(record, `${JSON.stringify(mutatedRecord(name, contractTemplate), null, 2)}\n`); const child = runValidator(tempRoot, path.basename(record)); const codes = child.output.failures?.map((entry) => entry.code) ?? []; const ok = child.status !== 0 && codes.includes(expectedCode); results.push({ actual: { codes, status: child.status }, expected: expectedCode, name, ok }); } - if (selected !== "all" && selected !== "valid-migration" && !expectedCases.some(([name]) => name === selected)) { + const runtimeTemplate = structuredClone(valid); + runtimeTemplate.adoption_mappings[0].stylegallery.revision = "0".repeat(40); + runtimeFixture = initializeRuntimeFixture(runtimeTemplate); + if (selected === "all" || selected === "valid-runtime-proof") { + const record = "records/valid-runtime-proof.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(runtimeFixture.value, null, 2)}\n`); + const child = runValidator(runtimeFixture.root, record); + const ok = child.status === 0 && child.output.ok === true && child.output.checkedScenarios === 1; + results.push({ actual: { codes: child.output.failures?.map((entry) => entry.code) ?? [], status: child.status }, expected: "runtime-bound record and exit:0", name: "valid-runtime-proof", ok }); + } + for (const [name, expectedCode, env] of runtimeCases) { + if (selected !== "all" && selected !== name) continue; + const record = `records/${name}.json`; + writeFile(runtimeFixture.root, record, `${JSON.stringify(mutatedRuntimeRecord(name, runtimeFixture.value, runtimeFixture), null, 2)}\n`); + const child = runValidator(runtimeFixture.root, record, env); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes(expectedCode); + results.push({ actual: { codes, status: child.status }, expected: expectedCode, name, ok }); + } + if (selected === "all" || selected === "child-process-denied-cleanup") { + const value = structuredClone(runtimeFixture.value); + value.scenarios[0].argv = ["node", "tests/migration.mjs", "--lock-worktree"]; + value.scenarios[0].result_artifact = "evidence/results/locked-worktree-cleanup.json"; + writeFile(runtimeFixture.root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + const record = "records/locked-worktree-cleanup.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(value, null, 2)}\n`); + const before = git(runtimeFixture.root, "worktree", "list", "--porcelain"); + const child = runValidator(runtimeFixture.root, record); + const after = git(runtimeFixture.root, "worktree", "list", "--porcelain"); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes("runtime_command_failed") && before === after; + results.push({ actual: { codes, registeredWorktreesUnchanged: before === after, status: child.status }, expected: "child process denied and no registered temporary worktree", name: "child-process-denied-cleanup", ok }); + } + if (selected === "all" || selected === "timed-out-command-cleanup") { + const value = structuredClone(runtimeFixture.value); + value.scenarios[0].argv = ["node", "tests/hanging.mjs"]; + value.scenarios[0].result_artifact = "evidence/results/timed-out-command.json"; + writeFile(runtimeFixture.root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + const record = "records/timed-out-command.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(value, null, 2)}\n`); + const before = git(runtimeFixture.root, "worktree", "list", "--porcelain"); + const child = runValidator(runtimeFixture.root, record, { STYLEGALLERY_RUNTIME_TIMEOUT_MS: "100" }); + const after = git(runtimeFixture.root, "worktree", "list", "--porcelain"); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes("runtime_command_failed") && before === after; + results.push({ actual: { codes, registeredWorktreesUnchanged: before === after, status: child.status }, expected: "timeout and no registered temporary worktree", name: "timed-out-command-cleanup", ok }); + } + if (selected === "all" || selected === "permission-override-denied") { + const value = structuredClone(runtimeFixture.value); + value.scenarios[0].argv = ["node", "--allow-child-process", "tests/migration.mjs"]; + value.scenarios[0].result_artifact = "evidence/results/permission-override.json"; + writeFile(runtimeFixture.root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + const record = "records/permission-override.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(value, null, 2)}\n`); + const child = runValidator(runtimeFixture.root, record); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes("runtime_command_argv_unsafe") && child.output.checkedRuntimeCommands === 0; + results.push({ actual: { checkedRuntimeCommands: child.output.checkedRuntimeCommands, codes, status: child.status }, expected: "permission override rejected before execution", name: "permission-override-denied", ok }); + } + if (selected === "all" || selected === "permission-disable-denied") { + const value = structuredClone(runtimeFixture.value); + value.scenarios[0].argv = ["node", "--no-permission=false", "tests/migration.mjs"]; + value.scenarios[0].result_artifact = "evidence/results/permission-disable.json"; + writeFile(runtimeFixture.root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + const record = "records/permission-disable.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(value, null, 2)}\n`); + const child = runValidator(runtimeFixture.root, record); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes("runtime_command_argv_unsafe") && child.output.checkedRuntimeCommands === 0; + results.push({ actual: { checkedRuntimeCommands: child.output.checkedRuntimeCommands, codes, status: child.status }, expected: "permission disable rejected before execution", name: "permission-disable-denied", ok }); + } + if (selected === "all" || selected === "permission-underscore-denied") { + const value = structuredClone(runtimeFixture.value); + value.scenarios[0].argv = ["node", "--allow_child_process", "tests/migration.mjs"]; + value.scenarios[0].result_artifact = "evidence/results/permission-underscore.json"; + writeFile(runtimeFixture.root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + const record = "records/permission-underscore.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(value, null, 2)}\n`); + const child = runValidator(runtimeFixture.root, record); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes("runtime_command_argv_unsafe") && child.output.checkedRuntimeCommands === 0; + results.push({ actual: { checkedRuntimeCommands: child.output.checkedRuntimeCommands, codes, status: child.status }, expected: "underscore permission override rejected before execution", name: "permission-underscore-denied", ok }); + } + if (selected === "all" || selected === "permission-path-shaped-denied") { + const value = structuredClone(runtimeFixture.value); + value.scenarios[0].argv = ["node", "--allow-fs-read=/tmp/outside.mjs", "tests/migration.mjs"]; + value.scenarios[0].result_artifact = "evidence/results/permission-path-shaped.json"; + writeFile(runtimeFixture.root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + const record = "records/permission-path-shaped.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(value, null, 2)}\n`); + const child = runValidator(runtimeFixture.root, record); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes("runtime_command_argv_unsafe") && child.output.checkedRuntimeCommands === 0; + results.push({ actual: { checkedRuntimeCommands: child.output.checkedRuntimeCommands, codes, status: child.status }, expected: "path-shaped permission option rejected before execution", name: "permission-path-shaped-denied", ok }); + } + if (selected === "all" || selected === "oversized-command-result") { + const value = structuredClone(runtimeFixture.value); + value.scenarios[0].argv = ["node", "tests/migration.mjs", "--oversized-result"]; + value.scenarios[0].result_artifact = "evidence/results/oversized-command-result.json"; + writeFile(runtimeFixture.root, value.scenarios[0].result_artifact, `${JSON.stringify(runtimeResult(value), null, 2)}\n`); + const record = "records/oversized-command-result.json"; + writeFile(runtimeFixture.root, record, `${JSON.stringify(value, null, 2)}\n`); + const child = runValidator(runtimeFixture.root, record); + const codes = child.output.failures?.map((entry) => entry.code) ?? []; + const ok = child.status !== 0 && codes.includes("runtime_command_result_missing") && child.output.checkedRuntimeCommands === 0; + results.push({ actual: { checkedRuntimeCommands: child.output.checkedRuntimeCommands, codes, status: child.status }, expected: "oversized command result rejected before read", name: "oversized-command-result", ok }); + } + const knownCase = selected === "all" + || selected === "valid-migration" + || selected === "valid-runtime-proof" + || selected === "stale-template-rebound" + || selected === "child-process-denied-cleanup" + || selected === "timed-out-command-cleanup" + || selected === "permission-override-denied" + || selected === "permission-disable-denied" + || selected === "permission-underscore-denied" + || selected === "permission-path-shaped-denied" + || selected === "oversized-command-result" + || endToEndCaseNames.includes(selected) + || contractCases.some(([name]) => name === selected) + || runtimeCases.some(([name]) => name === selected); + if (!knownCase) { results.push({ actual: selected, expected: "known case", name: "case-selection", ok: false }); } } finally { fs.rmSync(tempRoot, { force: true, recursive: true }); + if (runtimeFixture) fs.rmSync(runtimeFixture.root, { force: true, recursive: true }); } const result = { ok: results.every((entry) => entry.ok), results }; diff --git a/scripts/validate-consumer-conformance.mjs b/scripts/validate-consumer-conformance.mjs index f58705c..488c8e6 100644 --- a/scripts/validate-consumer-conformance.mjs +++ b/scripts/validate-consumer-conformance.mjs @@ -1,7 +1,10 @@ #!/usr/bin/env node import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { isDeepStrictEqual } from "node:util"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { @@ -9,28 +12,47 @@ import { consumerConformanceSchemaFindings, isNormalizedRepositoryPath, validateConsumerConformanceSemantics, + validateConsumerPageEvidenceSemantics, + validateConsumerPageIntentSemantics, } from "./consumer-conformance-contract.mjs"; import { parseStrictJson } from "./strict-json.mjs"; import { addDateTimeFormat } from "./json-schema-formats.mjs"; +import { + addSchemaFindings, + compilePageEvidenceSchemas, + digest, + gitIdentity, + normalizeReference, + PAGE_EVIDENCE_MANIFEST, + readJsonFile, + resolveContained, +} from "./page-evidence-contract.mjs"; +import { MAX_ARTIFACT_FILE_BYTES } from "./page-artifact-metadata.mjs"; const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const schemaPath = path.join(styleGalleryRoot, "consumer-reference", "schema", "consumer-conformance-record.schema.json"); +const pageSchemaRoot = path.join(styleGalleryRoot, "consumer-reference", "schema"); +const pageValidator = path.join(styleGalleryRoot, "scripts", "validate-page-evidence.mjs"); +const runtimeTimeout = Math.min(120_000, Math.max(100, Number.parseInt(process.env.STYLEGALLERY_RUNTIME_TIMEOUT_MS ?? "120000", 10) || 120_000)); function finding(code, message, recordPath) { return { code, message, path: recordPath }; } function parseArguments(argv) { - const options = { json: false, record: undefined, root: process.cwd() }; + const options = { artifactRoot: undefined, json: false, priorManifest: undefined, record: undefined, root: process.cwd() }; const failures = []; for (let index = 2; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--json") options.json = true; - else if (argument === "--record" || argument === "--root") { + else if (["--artifact-root", "--prior-manifest", "--record", "--root"].includes(argument)) { const value = argv[index + 1]; if (!value || value.startsWith("--")) failures.push(finding("argument_value_required", `${argument} requires a value`, "")); else { - options[argument === "--record" ? "record" : "root"] = value; + if (argument === "--artifact-root") options.artifactRoot = value; + if (argument === "--prior-manifest") options.priorManifest = value; + if (argument === "--record") options.record = value; + if (argument === "--root") options.root = value; index += 1; } } else failures.push(finding("argument_unknown", `unsupported argument ${argument}`, "")); @@ -67,11 +89,12 @@ function containedRecord(rootArgument, recordArgument, failures) { failures.push(finding("consumer_conformance_record_path_invalid", "consumer conformance record must be a contained regular file", recordArgument)); return undefined; } - return { recordPath: relative.split(path.sep).join("/"), resolved }; + return { recordPath: relative.split(path.sep).join("/"), resolved, root }; } function readStrictJson(file, code, displayPath, failures) { try { + if (fs.lstatSync(file).size > MAX_ARTIFACT_FILE_BYTES) throw new Error(`file exceeds ${MAX_ARTIFACT_FILE_BYTES} bytes`); return parseStrictJson(fs.readFileSync(file, "utf8")); } catch (error) { failures.push(finding(code, error instanceof Error ? error.message : String(error), displayPath)); @@ -83,33 +106,369 @@ function uniqueFindings(findings) { return [...new Map(findings.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()]; } +function runGit(root, args, encoding = "utf8") { + return spawnSync("git", ["-c", `safe.directory=${path.resolve(root)}`, ...args], { cwd: root, encoding }); +} + +function firstFilesystemRedirect(root) { + const pending = [root]; + while (pending.length > 0) { + const directory = pending.pop(); + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const target = path.join(directory, entry.name); + const stat = fs.lstatSync(target); + if (stat.isSymbolicLink()) return path.relative(root, target).split(path.sep).join("/"); + if (stat.isDirectory()) pending.push(target); + } + } + return undefined; +} + +function executePinnedScenario(root, revision, scenario, expectedResult, failures) { + if (!Array.isArray(scenario?.argv) || scenario.argv.length === 0 || scenario.argv.some((argument) => typeof argument !== "string" || argument.length === 0)) return false; + const [executable, ...args] = scenario.argv; + if (executable !== "node") { + failures.push(finding("runtime_command_executable_invalid", `scenario ${scenario.id ?? ""} must use the governed Node runtime`, scenario.id ?? "")); + return undefined; + } + const entrypoint = args[0]; + if (!isNormalizedRepositoryPath(entrypoint ?? "") || entrypoint.startsWith("-") || !/\.(?:c?js|mjs)$/.test(entrypoint)) { + failures.push(finding("runtime_command_argv_unsafe", `scenario ${scenario.id ?? ""} may not override the governed Node permission boundary`, scenario.id ?? "")); + return undefined; + } + const temporaryRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-command-"))); + const checkout = path.join(temporaryRoot, "checkout"); + let worktreeAdded = false; + try { + const add = runGit(root, ["worktree", "add", "--detach", checkout, revision]); + if (add.status !== 0) { + failures.push(finding("runtime_command_checkout_failed", `scenario ${scenario.id ?? ""} could not check out the pinned revision`, scenario.id ?? "")); + return false; + } + worktreeAdded = true; + const redirect = firstFilesystemRedirect(checkout); + if (redirect) { + failures.push(finding("runtime_command_checkout_symlink", `scenario ${scenario.id ?? ""} checkout contains filesystem redirect ${redirect}`, redirect)); + return undefined; + } + const resultFile = path.resolve(checkout, scenario.result_artifact); + fs.rmSync(resultFile, { force: true }); + const child = spawnSync(process.execPath, ["--permission", `--allow-fs-read=${checkout}`, `--allow-fs-write=${checkout}`, ...args], { + cwd: checkout, + env: { + CI: "1", + LANG: "C", + LC_ALL: "C", + PATH: process.env.PATH ?? "", + STYLEGALLERY_RESULT_ARTIFACT: scenario.result_artifact, + STYLEGALLERY_RUNTIME_CONTEXT: JSON.stringify(expectedResult), + }, + killSignal: "SIGKILL", + shell: false, + stdio: "ignore", + timeout: runtimeTimeout, + }); + if (child.error || child.status !== 0) { + const outcome = child.error?.code === "ETIMEDOUT" ? "timed out" : child.signal ? `received ${child.signal}` : `exited ${child.status ?? "without a status"}`; + failures.push(finding("runtime_command_failed", `scenario ${scenario.id ?? ""} ${outcome} at the pinned revision`, scenario.id ?? "")); + return undefined; + } + try { + const stat = fs.lstatSync(resultFile); + if (stat.size > MAX_ARTIFACT_FILE_BYTES) throw new Error(`result exceeds ${MAX_ARTIFACT_FILE_BYTES} bytes`); + if (!stat.isFile() || stat.isSymbolicLink() || !fs.realpathSync(resultFile).startsWith(`${fs.realpathSync(checkout)}${path.sep}`)) throw new Error("result is not a contained regular file"); + return parseStrictJson(fs.readFileSync(resultFile, "utf8")); + } catch (error) { + failures.push(finding("runtime_command_result_missing", `scenario ${scenario.id ?? ""} did not produce a valid result artifact: ${error instanceof Error ? error.message : String(error)}`, scenario.result_artifact)); + return undefined; + } + } finally { + if (worktreeAdded) { + const removed = runGit(root, ["worktree", "remove", "--force", "--force", checkout]); + if (removed.status !== 0) { + try { fs.rmSync(checkout, { force: true, recursive: true }); } catch {} + runGit(root, ["worktree", "prune", "--expire", "now"]); + const registered = runGit(root, ["worktree", "list", "--porcelain"]); + if (registered.status !== 0 || registered.stdout.includes(checkout)) failures.push(finding("runtime_command_cleanup_failed", `scenario ${scenario.id ?? ""} worktree cleanup failed`, scenario.id ?? "")); + } + } + fs.rmSync(temporaryRoot, { force: true, recursive: true }); + } +} + +function remoteIdentity(remote) { + const cleaned = remote.trim().replace(/\.git$/, ""); + const scp = cleaned.match(/^[^@]+@[^:]+:(.+)$/); + if (scp) return scp[1]; + try { return new URL(cleaned).pathname.replace(/^\//, ""); } + catch { return undefined; } +} + +function pinnedSourceManifest(root, revision, references, failures) { + if (!Array.isArray(references) || references.length === 0 || new Set(references).size !== references.length) return undefined; + const revisionCheck = runGit(root, ["cat-file", "-e", `${revision}^{commit}`]); + if (revisionCheck.status !== 0) { + failures.push(finding("consumer_revision_unknown", "consumer revision must resolve to a commit in the repository", revision)); + return undefined; + } + const files = []; + for (const reference of [...references].sort()) { + if (!isNormalizedRepositoryPath(reference)) continue; + const tree = runGit(root, ["ls-tree", "-z", revision, "--", reference], "buffer"); + const entry = tree.status === 0 ? tree.stdout.toString("utf8").split("\0").find((line) => line.endsWith(`\t${reference}`)) : undefined; + if (!entry) { + failures.push(finding("consumer_source_missing", "relevant source is missing from the pinned revision", reference)); + continue; + } + const [metadata] = entry.split("\t", 1); + const [mode, type] = metadata.split(" "); + if (mode === "120000") { + failures.push(finding("consumer_source_symlink", "relevant source must not be a symbolic link", reference)); + continue; + } + if (type !== "blob") { + failures.push(finding("consumer_source_type_invalid", "relevant source must be a regular file", reference)); + continue; + } + const blob = runGit(root, ["show", `${revision}:${reference}`], "buffer"); + if (blob.status !== 0) { + failures.push(finding("consumer_source_unreadable", "relevant source could not be read from the pinned revision", reference)); + continue; + } + files.push({ byte_length: blob.stdout.length, path: reference, sha256: digest(blob.stdout) }); + } + return files.length === references.length ? { files, sha256: digest(Buffer.from(JSON.stringify(files))) } : undefined; +} + +function markdownAnchors(bytes) { + const occurrences = new Map(); + return bytes.toString("utf8").split("\n").flatMap((line) => { + const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/)?.[1]; + if (!heading) return []; + const base = heading + .replace(/`([^`]*)`/g, "$1") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .toLowerCase() + .replace(/[^a-z0-9 _-]/g, "") + .trim() + .replace(/\s+/g, "-"); + const occurrence = occurrences.get(base) ?? 0; + occurrences.set(base, occurrence + 1); + return [occurrence === 0 ? `#${base}` : `#${base}-${occurrence}`]; + }); +} + +function validateStyleGallerySources(value, recordPath, failures) { + const mappings = Array.isArray(value?.adoption_mappings) ? value.adoption_mappings : []; + let checked = 0; + for (const [index, mapping] of mappings.entries()) { + const source = mapping?.stylegallery; + if (!source || !/^[a-f0-9]{40}$/.test(source.revision ?? "") || !isNormalizedRepositoryPath(source.path)) continue; + const revision = runGit(styleGalleryRoot, ["cat-file", "-e", `${source.revision}^{commit}`]); + if (revision.status !== 0) { + failures.push(finding("adoption_stylegallery_revision_unknown", `adoption mapping ${index} revision does not resolve in StyleGallery`, recordPath)); + continue; + } + const tree = runGit(styleGalleryRoot, ["ls-tree", "-z", source.revision, "--", source.path], "buffer"); + const entry = tree.status === 0 ? tree.stdout.toString("utf8").split("\0").find((line) => line.endsWith(`\t${source.path}`)) : undefined; + if (!entry) { + failures.push(finding("adoption_stylegallery_path_missing", `adoption mapping ${index} path is missing from its pinned StyleGallery revision`, source.path)); + continue; + } + const [metadata] = entry.split("\t", 1); + const [mode, type] = metadata.split(" "); + if (mode === "120000" || type !== "blob") { + failures.push(finding("adoption_stylegallery_path_invalid", `adoption mapping ${index} path must be a regular StyleGallery file`, source.path)); + continue; + } + const blob = runGit(styleGalleryRoot, ["show", `${source.revision}:${source.path}`], "buffer"); + if (blob.status !== 0) { + failures.push(finding("adoption_stylegallery_path_unreadable", `adoption mapping ${index} path could not be read from StyleGallery`, source.path)); + continue; + } + if (!markdownAnchors(blob.stdout).includes(source.anchor)) { + failures.push(finding("adoption_stylegallery_anchor_missing", `adoption mapping ${index} anchor is missing from its pinned StyleGallery file`, `${source.path}${source.anchor}`)); + continue; + } + checked += 1; + } + return checked; +} + +function validateRuntimeEvidence({ recordPath, root, validateResult, value }, failures) { + const consumer = value?.consumer; + const scenarios = Array.isArray(value?.scenarios) ? value.scenarios : []; + if (!consumer || typeof consumer.repository !== "string" || typeof consumer.revision !== "string" || !Array.isArray(consumer.relevant_sources)) return 0; + + const gitFailures = []; + const identity = gitIdentity(root, gitFailures); + for (const issue of gitFailures) failures.push(finding("consumer_git_invalid", issue.message, issue.path)); + if (identity) { + const remote = runGit(root, ["config", "--get", "remote.origin.url"]); + const repositories = new Set([identity.repository, remote.status === 0 ? remoteIdentity(remote.stdout) : undefined].filter(Boolean)); + if (repositories.size === 0) failures.push(finding("consumer_repository_unverifiable", "consumer repository requires a verifiable Git remote identity", recordPath)); + else if (!repositories.has(consumer.repository)) failures.push(finding("consumer_repository_mismatch", "consumer repository differs from the Git repository identity", recordPath)); + } + + const source = pinnedSourceManifest(root, consumer.revision, consumer.relevant_sources, failures); + if (source) { + for (const scenario of scenarios) { + if (scenario?.source_digest !== source.sha256) failures.push(finding("runtime_source_digest_mismatch", `scenario ${scenario?.id ?? ""} source digest differs from the pinned relevant sources`, recordPath)); + } + } + + let checked = 0; + let executed = 0; + for (const scenario of scenarios) { + if (!isNormalizedRepositoryPath(scenario?.result_artifact, { jsonOnly: true })) continue; + const result = readJsonFile({ prefix: "runtime_result_artifact", reference: scenario.result_artifact, root }, failures); + if (!result) continue; + checked += 1; + if (scenario.evidence_method === "browser" && value.page_evidence?.status === "applicable") continue; + if (!validateResult(result.value)) { + for (const error of validateResult.errors ?? []) failures.push(finding("runtime_result_artifact_schema_invalid", `${error.instancePath || "/"} ${error.message}`, scenario.result_artifact)); + continue; + } + const expected = { + argv: scenario.argv, + evidence_method: scenario.evidence_method, + exit_code: scenario.exit_code, + repository: consumer.repository, + revision: consumer.revision, + run_id: scenario.run_id, + scenario_id: scenario.id, + session_id: scenario.session_id, + source_digest: scenario.source_digest, + }; + if (Object.entries(expected).some(([key, expectedValue]) => Array.isArray(expectedValue) + ? !Array.isArray(result.value[key]) || expectedValue.length !== result.value[key].length || expectedValue.some((entry, index) => result.value[key][index] !== entry) + : result.value[key] !== expectedValue)) { + failures.push(finding("runtime_result_artifact_mismatch", `scenario ${scenario.id ?? ""} result artifact identity differs from the conformance record`, scenario.result_artifact)); + } + const executedResult = identity && source ? executePinnedScenario(root, consumer.revision, scenario, expected, failures) : undefined; + if (executedResult !== undefined) { + executed += 1; + if (!validateResult(executedResult)) { + for (const error of validateResult.errors ?? []) failures.push(finding("runtime_command_result_invalid", `${error.instancePath || "/"} ${error.message}`, scenario.result_artifact)); + } else if (!isDeepStrictEqual(executedResult, result.value)) { + failures.push(finding("runtime_result_artifact_execution_mismatch", `scenario ${scenario.id ?? ""} stored result differs from the result produced at the pinned revision`, scenario.result_artifact)); + } + } + } + return { checked, executed }; +} + +function runPageValidator({ artifactRoot, manifest, priorManifest, root }, failures) { + const args = [pageValidator, "--root", root, "--artifact-root", artifactRoot, "--manifest", manifest]; + if (priorManifest) args.push("--prior-manifest", priorManifest); + args.push("--json"); + const child = spawnSync(process.execPath, args, { cwd: styleGalleryRoot, encoding: "utf8" }); + let report; + try { + report = parseStrictJson(child.stdout); + } catch (error) { + failures.push(finding("page_evidence_validator_output_invalid", error instanceof Error ? error.message : String(error), manifest)); + return undefined; + } + if (Array.isArray(report?.failures)) failures.push(...report.failures); + if (child.status !== 0 && report?.failures?.length === 0) failures.push(finding("page_evidence_validator_failed", "page evidence validator exited without a finding", manifest)); + return report; +} + +function validatePageEvidence({ artifactRootArgument, priorManifestArgument, recordPath, root, value }, failures) { + if (value?.page_evidence?.status !== "applicable") return undefined; + const intentFindings = validateConsumerPageIntentSemantics(value, recordPath); + failures.push(...intentFindings); + if (intentFindings.some((entry) => entry.code === "page_evidence_runtime_scenario_required")) return undefined; + if (!artifactRootArgument) { + failures.push(finding("page_evidence_artifact_root_required", "applicable page evidence requires --artifact-root", value.page_evidence.manifest ?? "")); + return undefined; + } + const artifactReference = normalizeReference(root, artifactRootArgument); + if (!artifactReference) { + failures.push(finding("page_evidence_artifact_escape", "artifact root must be a normalized path inside the consumer root", artifactRootArgument)); + return undefined; + } + const artifact = resolveContained({ expectedType: "directory", prefix: "page_evidence_artifact", reference: artifactReference, root }, failures); + const expectedManifest = `${artifactReference}/${PAGE_EVIDENCE_MANIFEST}`; + if (value.page_evidence.manifest !== expectedManifest) { + failures.push(finding("page_evidence_manifest_path_mismatch", `page evidence manifest must equal ${expectedManifest}`, value.page_evidence.manifest)); + return undefined; + } + const manifest = readJsonFile({ prefix: "page_evidence_manifest", reference: expectedManifest, root }, failures); + if (!artifact || !manifest) return undefined; + let schemas; + try { + schemas = compilePageEvidenceSchemas(pageSchemaRoot); + } catch (error) { + failures.push(finding("page_evidence_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema")); + return undefined; + } + if (!addSchemaFindings(schemas.manifest, manifest.value, manifest.reference, "page_evidence_manifest_schema_invalid", failures)) return undefined; + failures.push(...validateConsumerPageEvidenceSemantics(value, manifest.value, recordPath)); + + let priorManifest; + if (priorManifestArgument) { + const reference = normalizeReference(root, priorManifestArgument); + const prior = reference ? resolveContained({ expectedType: "file", prefix: "page_evidence_reuse_manifest", reference, root }, failures) : undefined; + if (!reference) failures.push(finding("page_evidence_reuse_manifest_escape", "prior manifest must be a normalized path inside the consumer root", priorManifestArgument)); + priorManifest = prior?.file; + } + if (priorManifestArgument && !priorManifest) return undefined; + return runPageValidator({ artifactRoot: artifact.file, manifest: manifest.file, priorManifest, root }, failures); +} + const { failures: argumentFailures, options } = parseArguments(process.argv); const failures = [...argumentFailures]; const record = options.record ? containedRecord(options.root, options.record, failures) : undefined; const schema = readStrictJson(schemaPath, "consumer_conformance_schema_invalid", "consumer-reference/schema/consumer-conformance-record.schema.json", failures); const value = record ? readStrictJson(record.resolved, "consumer_conformance_json_invalid", record.recordPath, failures) : undefined; +let checkedRuntimeArtifacts = 0; +let checkedRuntimeCommands = 0; +let checkedStyleGallerySources = 0; +let recordSchemaValid = false; if (schema && value !== undefined) { try { const ajv = new Ajv2020({ allErrors: true, strict: true }); addDateTimeFormat(ajv); - const validate = ajv.compile(schema); - if (!validate(value)) failures.push(...consumerConformanceSchemaFindings(validate.errors, record.recordPath)); - failures.push(...validateConsumerConformanceSemantics(value, record.recordPath)); + ajv.addSchema(schema); + const validate = ajv.getSchema(schema.$id); + const validateResult = ajv.compile({ $ref: `${schema.$id}#/$defs/runtimeResult` }); + const schemaValid = validate(value); + recordSchemaValid = schemaValid; + if (!schemaValid) failures.push(...consumerConformanceSchemaFindings(validate.errors, record.recordPath)); + else { + failures.push(...validateConsumerConformanceSemantics(value, record.recordPath)); + checkedStyleGallerySources = validateStyleGallerySources(value, record.recordPath, failures); + const runtimeEvidence = validateRuntimeEvidence({ recordPath: record.recordPath, root: record.root, validateResult, value }, failures); + checkedRuntimeArtifacts = runtimeEvidence.checked; + checkedRuntimeCommands = runtimeEvidence.executed; + } } catch (error) { failures.push(finding("consumer_conformance_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema/consumer-conformance-record.schema.json")); } } +const pageReport = record && value !== undefined && recordSchemaValid + ? validatePageEvidence({ artifactRootArgument: options.artifactRoot, priorManifestArgument: options.priorManifest, recordPath: record.recordPath, root: record.root, value }, failures) + : undefined; + const uniqueFailures = uniqueFindings(failures); const result = { checkedDimensions: value && typeof value.migration_dimensions === "object" && value.migration_dimensions !== null ? MIGRATION_DIMENSIONS.filter((name) => Object.hasOwn(value.migration_dimensions, name)).length : 0, checkedMappings: Array.isArray(value?.adoption_mappings) ? value.adoption_mappings.length : 0, + checkedRuntimeArtifacts, + checkedRuntimeCommands, checkedScenarios: Array.isArray(value?.scenarios) ? value.scenarios.length : 0, + checkedStyleGallerySources, + checkedPageArtifacts: pageReport?.artifactCount ?? 0, + checkedPageScenarios: pageReport?.scenarioCount ?? 0, failures: uniqueFailures, ok: uniqueFailures.length === 0, + pageSessionId: pageReport?.sessionId ?? null, record: record?.recordPath ?? options.record ?? null, }; if (options.json) console.log(JSON.stringify(result, null, 2)); From 86605001fb92c1fc987037ba0c0e67f7b09cb1be Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:38:15 +0900 Subject: [PATCH 42/44] feat(qa): execute consumer conformance matrix Plan-Task: 6 --- .../editorial/evidence/button.evidence.json | 210 ++++---- .../terminal/evidence/button.evidence.json | 210 ++++---- package-lock.json | 24 + package.json | 2 + scripts/run-consumer-page-evidence-ci.mjs | 170 +++++++ .../test-consumer-conformance-sentinel.mjs | 165 +++++++ tests/consumer-conformance.spec.mjs | 235 +++++++++ .../consumer-conformance-scenarios.mjs | 80 ++++ tests/helpers/render-consumer-conformance.mjs | 453 ++++++++++++++++++ 9 files changed, 1339 insertions(+), 210 deletions(-) create mode 100644 scripts/run-consumer-page-evidence-ci.mjs create mode 100644 scripts/test-consumer-conformance-sentinel.mjs create mode 100644 tests/consumer-conformance.spec.mjs create mode 100644 tests/fixtures/consumer-conformance-scenarios.mjs create mode 100644 tests/helpers/render-consumer-conformance.mjs diff --git a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json index ede64b0..d631fdb 100644 --- a/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json @@ -17,7 +17,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -45,7 +45,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -98,14 +98,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -223,7 +223,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -249,7 +249,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -277,7 +277,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -330,14 +330,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -455,7 +455,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -481,7 +481,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -509,7 +509,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -562,14 +562,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -687,7 +687,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -715,7 +715,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -743,7 +743,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -796,14 +796,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -921,7 +921,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -946,7 +946,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -974,7 +974,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1027,14 +1027,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1152,7 +1152,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1177,7 +1177,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1205,7 +1205,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1258,14 +1258,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1383,7 +1383,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1410,7 +1410,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1438,7 +1438,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1491,14 +1491,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1616,7 +1616,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1643,7 +1643,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1671,7 +1671,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1724,14 +1724,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1849,7 +1849,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1876,7 +1876,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1904,7 +1904,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1957,14 +1957,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2082,7 +2082,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2111,7 +2111,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2139,7 +2139,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2192,14 +2192,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2317,7 +2317,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2345,7 +2345,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2373,7 +2373,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2426,14 +2426,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2551,7 +2551,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2579,7 +2579,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2607,7 +2607,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2660,14 +2660,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2785,7 +2785,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2815,7 +2815,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2843,7 +2843,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2896,14 +2896,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -3021,7 +3021,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3047,7 +3047,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3075,7 +3075,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3128,14 +3128,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -3253,7 +3253,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3279,7 +3279,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3307,7 +3307,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3360,14 +3360,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -3485,7 +3485,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, diff --git a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json index d28ddd3..f9faa17 100644 --- a/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +++ b/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json @@ -17,7 +17,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -45,7 +45,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -98,14 +98,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -223,7 +223,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -249,7 +249,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -277,7 +277,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -330,14 +330,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -455,7 +455,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -481,7 +481,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -509,7 +509,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -562,14 +562,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -687,7 +687,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -715,7 +715,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -743,7 +743,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -796,14 +796,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -921,7 +921,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -946,7 +946,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -974,7 +974,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1027,14 +1027,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1152,7 +1152,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1177,7 +1177,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1205,7 +1205,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1258,14 +1258,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1383,7 +1383,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1410,7 +1410,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1438,7 +1438,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1491,14 +1491,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1616,7 +1616,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1643,7 +1643,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1671,7 +1671,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1724,14 +1724,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -1849,7 +1849,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -1876,7 +1876,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1904,7 +1904,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -1957,14 +1957,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2082,7 +2082,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2111,7 +2111,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2139,7 +2139,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2192,14 +2192,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2317,7 +2317,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2345,7 +2345,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2373,7 +2373,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2426,14 +2426,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2551,7 +2551,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2579,7 +2579,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2607,7 +2607,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2660,14 +2660,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -2785,7 +2785,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -2815,7 +2815,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2843,7 +2843,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -2896,14 +2896,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -3021,7 +3021,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3047,7 +3047,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3075,7 +3075,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3128,14 +3128,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -3253,7 +3253,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, @@ -3279,7 +3279,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3307,7 +3307,7 @@ "browser_revision": "chromium-1228", "container_image": "local-host:25.5.0", "kind": "browser", - "lockfile_sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e", + "lockfile_sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80", "node": "v22.23.1", "platform": "darwin/arm64", "playwright": "1.61.0", @@ -3360,14 +3360,14 @@ "sha256": "sha256:5fe9bf91ec1198441169dc1aa0773c9f4d278b2e345d111c453c34bd0c575d49" }, { - "byte_length": 59586, + "byte_length": 60458, "path": "package-lock.json", - "sha256": "sha256:fb93b381972057a0db6c4f17f1441479698164c0fd7b8724d396621810df4e6e" + "sha256": "sha256:d3f737b3aee4dffe053129c197ecc38393cd45123f0986d068655df34177ce80" }, { - "byte_length": 1499, + "byte_length": 1625, "path": "package.json", - "sha256": "sha256:3afe1140ad596222b433665f2a0f52ed59b3df53bf973280f614ed2404978d7d" + "sha256": "sha256:0ce4b2d065b7b3ebdcf6ab818939bea8e1595b99515b00fbc72586b12a056146" }, { "byte_length": 532, @@ -3485,7 +3485,7 @@ "sha256": "sha256:51c09abb671b615d075ba74b8578efc500fc000d1e39a62d9fc20a76a27a5a46" } ], - "sha256": "sha256:3862a824f12e470343fbcdefd9bf4cabed580484e558384083c304cb9fde85bd" + "sha256": "sha256:dfc234bdeae8e90ac33cc5724893b9235a0898c85d5aff7cea349da354e53406" }, "started_at": "2026-07-14T06:33:47.214Z" }, diff --git a/package-lock.json b/package-lock.json index e3d927c..c66f779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "style-dictionary": "5.5.0" }, "devDependencies": { + "@axe-core/playwright": "4.12.1", "@playwright/test": "1.61.0", "ajv": "8.20.0", "ajv-formats": "3.0.1" @@ -17,6 +18,19 @@ "node": ">=22" } }, + "node_modules/@axe-core/playwright": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.12.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@bundled-es-modules/deepmerge": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@bundled-es-modules/deepmerge/-/deepmerge-4.3.2.tgz", @@ -556,6 +570,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", diff --git a/package.json b/package.json index 5b15c51..2f0dc34 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "node scripts/build-reference-artifacts.mjs --adapter style-dictionary --fail-on-warning --json", "create:component-state-session": "node scripts/create-component-state-session.mjs", "test": "node scripts/test-reference-adapters.mjs --json", + "test:consumer-conformance": "playwright test tests/consumer-conformance.spec.mjs", "test:component-state": "node scripts/test-component-state-source-contract.mjs && node scripts/test-validate-component-state.mjs --json && node scripts/test-validate-component-state-artifacts.mjs && node scripts/test-generate-consumer-reference-evidence.mjs --json", "test:component-state:runtime": "playwright test tests/component-state-evidence.spec.mjs --project=chromium", "test:component-state:runtime-negative": "node scripts/test-component-state-sentinel.mjs", @@ -18,6 +19,7 @@ "validate:component-state": "node scripts/validate-component-state.mjs --json && node scripts/generate-consumer-reference-evidence.mjs --check --json" }, "devDependencies": { + "@axe-core/playwright": "4.12.1", "@playwright/test": "1.61.0", "ajv": "8.20.0", "ajv-formats": "3.0.1" diff --git a/scripts/run-consumer-page-evidence-ci.mjs b/scripts/run-consumer-page-evidence-ci.mjs new file mode 100644 index 0000000..3e921a7 --- /dev/null +++ b/scripts/run-consumer-page-evidence-ci.mjs @@ -0,0 +1,170 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { canonicalSourceManifest } from "./page-evidence-contract.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const workspaceRoot = process.cwd(); +const temporaryRoot = path.join(workspaceRoot, ".tmp"); +const syntheticBranch = "main"; +const syntheticRepository = "example/stylegallery-page-evidence-ci"; +const sourcePaths = [ + "playwright.config.mjs", + "tests/consumer-conformance.spec.mjs", + "tests/fixtures/consumer-conformance-scenarios.mjs", + "tests/helpers/render-consumer-conformance.mjs", +]; + +function trustedTemporaryRoot() { + const resolvedWorkspace = path.resolve(workspaceRoot); + if (fs.realpathSync(resolvedWorkspace) !== resolvedWorkspace) throw new Error("workspace root must not be a filesystem redirect"); + if (!fs.existsSync(temporaryRoot)) fs.mkdirSync(temporaryRoot); + const stat = fs.lstatSync(temporaryRoot); + if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(temporaryRoot) !== temporaryRoot) throw new Error(".tmp must be a real directory inside the workspace"); + return temporaryRoot; +} + +function containedPath({ base, candidate, label }) { + if (base === temporaryRoot) trustedTemporaryRoot(); + const resolved = path.resolve(workspaceRoot, candidate); + const relative = path.relative(base, resolved); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(label + " must be inside " + base); + return resolved; +} + +function run({ args, command, cwd }) { + const child = spawnSync(command, args, { cwd, encoding: "utf8" }); + if (child.status !== 0) throw new Error(command + " " + args.join(" ") + " failed (" + child.status + ")\n" + child.stdout + child.stderr); + return child.stdout.trim(); +} + +function writeJson(file, value) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", { flag: "wx" }); +} + +function parseArguments() { + const command = process.argv[2]; + const values = {}; + for (let index = 3; index < process.argv.length; index += 2) { + const name = process.argv[index]; + const value = process.argv[index + 1]; + if (!name?.startsWith("--") || !value || value.startsWith("--")) throw new Error("invalid argument pair at " + (name ?? "")); + values[name.slice(2)] = value; + } + if (!["prepare", "verify-session", "package", "cleanup"].includes(command)) throw new Error("first argument must be prepare, verify-session, package, or cleanup"); + return { command, values }; +} + +function prepare(values) { + const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" }); + const recordFile = containedPath({ base: consumerRoot, candidate: values.record, label: "--record" }); + const artifactRoot = containedPath({ base: consumerRoot, candidate: values["artifact-root"], label: "--artifact-root" }); + if (fs.existsSync(consumerRoot)) throw new Error("--consumer-root must not already exist"); + if (!fs.statSync(path.join(repositoryRoot, "node_modules")).isDirectory()) throw new Error("node_modules must exist before preparing the consumer"); + + fs.mkdirSync(consumerRoot, { recursive: true }); + run({ command: "git", args: ["init", "-b", syntheticBranch], cwd: consumerRoot }); + run({ command: "git", args: ["config", "user.email", "page-evidence-ci@example.invalid"], cwd: consumerRoot }); + run({ command: "git", args: ["config", "user.name", "StyleGallery page-evidence CI"], cwd: consumerRoot }); + run({ command: "git", args: ["remote", "add", "origin", `https://github.com/${syntheticRepository}.git`], cwd: consumerRoot }); + + for (const reference of sourcePaths) { + const destination = path.join(consumerRoot, reference); + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(path.join(repositoryRoot, reference), destination, fs.constants.COPYFILE_EXCL); + } + fs.symlinkSync(path.join(repositoryRoot, "node_modules"), path.join(consumerRoot, "node_modules"), "dir"); + run({ command: "git", args: ["add", ...sourcePaths], cwd: consumerRoot }); + run({ command: "git", args: ["commit", "-m", "test: prepare consumer page evidence"], cwd: consumerRoot }); + + const revision = run({ command: "git", args: ["rev-parse", "HEAD"], cwd: consumerRoot }); + const sourceFailures = []; + const source = canonicalSourceManifest(consumerRoot, sourcePaths, sourceFailures); + if (!source || sourceFailures.length > 0) throw new Error("source manifest failed: " + JSON.stringify(sourceFailures)); + const runId = process.env.GITHUB_RUN_ID ? "github-" + process.env.GITHUB_RUN_ID : "local-" + revision.slice(0, 12); + const sessionId = "page-evidence-" + crypto.randomUUID(); + const scenarioId = "responsive-layout"; + const reference = (file) => path.relative(consumerRoot, file).split(path.sep).join("/"); + + writeJson(recordFile, { + claim_boundary: "Synthetic CI evidence for the copied consumer-conformance probe; not product certification.", + consumer: { + relevant_sources: sourcePaths, + repository: syntheticRepository, + revision, + }, + id: "stylegallery-consumer-page-evidence-ci", + page_evidence: { + manifest: reference(path.join(artifactRoot, "page-evidence-manifest.json")), + status: "applicable", + }, + record_kind: "consumer_migration_conformance", + scenarios: [{ + assertions: ["The selected responsive layout case passes its browser assertions."], + argv: ["npx", "playwright", "test", "tests/consumer-conformance.spec.mjs", "--project=chromium", "--grep", "state-w1024-focus"], + evidence_method: "browser", + exit_code: 0, + id: scenarioId, + observable_actions: ["Render and capture state-w1024-focus in Chromium."], + result_artifact: reference(path.join(artifactRoot, "runner", scenarioId + ".json")), + run_id: runId, + session_id: sessionId, + source_digest: source.sha256, + }], + schema_version: "1.0", + }); + return { artifactRoot, consumerRoot, recordFile, revision, sourceSha256: source.sha256 }; +} + +function verifySession(values) { + const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" }); + const artifactRoot = containedPath({ base: consumerRoot, candidate: values["artifact-root"], label: "--artifact-root" }); + const receiptFile = path.join(artifactRoot, "page-evidence-session.json"); + const receipt = JSON.parse(fs.readFileSync(receiptFile, "utf8")); + const expected = { + branch: syntheticBranch, + repository: syntheticRepository, + revision: run({ command: "git", args: ["rev-parse", "HEAD"], cwd: consumerRoot }), + }; + for (const [field, value] of Object.entries(expected)) { + if (receipt[field] !== value) throw new Error(`page-evidence receipt ${field} must be ${value}, received ${receipt[field]}`); + } + return { ...expected, receiptFile }; +} + +function packagePacket(values) { + const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" }); + const artifactRoot = containedPath({ base: consumerRoot, candidate: values["artifact-root"], label: "--artifact-root" }); + const output = containedPath({ base: temporaryRoot, candidate: values.output, label: "--output" }); + const outputRelativeToConsumer = path.relative(consumerRoot, output); + if (!outputRelativeToConsumer.startsWith("..") && !path.isAbsolute(outputRelativeToConsumer)) throw new Error("--output must be outside --consumer-root"); + if (!fs.statSync(artifactRoot).isDirectory()) throw new Error("--artifact-root must be a directory"); + if (fs.existsSync(output)) throw new Error("--output must not already exist"); + fs.mkdirSync(path.dirname(output), { recursive: true }); + fs.cpSync(artifactRoot, output, { errorOnExist: true, force: false, recursive: true }); + return { output }; +} + +function cleanup(values) { + const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" }); + if (fs.existsSync(consumerRoot)) { + const stat = fs.lstatSync(consumerRoot); + if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(consumerRoot) !== consumerRoot) throw new Error("--consumer-root must be a real directory before cleanup"); + } + fs.rmSync(consumerRoot, { force: true, recursive: true }); + return { consumerRoot }; +} + +try { + const { command, values } = parseArguments(); + const result = command === "prepare" ? prepare(values) : command === "verify-session" ? verifySession(values) : command === "package" ? packagePacket(values) : cleanup(values); + process.stdout.write(JSON.stringify({ command, ok: true, ...result }, null, 2) + "\n"); +} catch (error) { + process.stderr.write((error instanceof Error ? error.message : String(error)) + "\n"); + process.exitCode = 1; +} diff --git a/scripts/test-consumer-conformance-sentinel.mjs b/scripts/test-consumer-conformance-sentinel.mjs new file mode 100644 index 0000000..f4e0a30 --- /dev/null +++ b/scripts/test-consumer-conformance-sentinel.mjs @@ -0,0 +1,165 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const availableCases = new Map([ + ["overflow", { expected: "document_no_horizontal_overflow", grep: "@sentinel-overflow", mutation: "overflow" }], + ["clipped-focus", { expected: "focus_geometry_visible", grep: "@sentinel-clipped-focus", mutation: "clipped-focus" }], + ["low-contrast", { expected: "color-contrast", grep: "@sentinel-low-contrast", mutation: "low-contrast" }], + ["dialog-focus-leak", { expected: "dialog_focus_trap", grep: "@sentinel-dialog-focus-leak", mutation: "dialog-focus-leak" }], + ["console-error", { expected: "console_error_free", grep: "@sentinel-console-error", mutation: "console-error" }], + ["page-evidence-symlink", { expected: "page_evidence_output_symlink", pageEvidence: "symlink" }], + ["page-evidence-existing-output", { expected: "page_evidence_output_exists", pageEvidence: "existing-output" }], + ["ci-cleanup-symlink", { ciCleanup: true, expected: ".tmp must be a real directory" }], +]); +const selected = []; +let json = false; +const argumentFailures = []; + +for (let index = 2; index < process.argv.length; index += 1) { + const argument = process.argv[index]; + if (argument === "--json") json = true; + else if (argument === "--case") { + const name = process.argv[index + 1]; + if (!name || name.startsWith("--")) argumentFailures.push("--case requires a value"); + else { + selected.push(name); + index += 1; + } + } else argumentFailures.push(`unsupported argument ${argument}`); +} + +const names = selected.length > 0 ? [...new Set(selected)] : [...availableCases.keys()]; +for (const name of names) if (!availableCases.has(name)) argumentFailures.push(`unknown case ${name}`); + +function runPlaywright(grep, environment) { + return spawnSync( + path.join(repositoryRoot, "node_modules", ".bin", "playwright"), + ["test", "tests/consumer-conformance.spec.mjs", "--project=chromium", "--reporter=line", "--grep", grep], + { + cwd: repositoryRoot, + encoding: "utf8", + env: { ...process.env, ...environment }, + maxBuffer: 4 * 1024 * 1024, + }, + ); +} + +function runMutationControl(name, control) { + const child = runPlaywright(control.grep, { CONSUMER_CONFORMANCE_MUTATION: control.mutation }); + const output = `${child.stdout}\n${child.stderr}`; + return { + actual: { named: output.includes(control.expected), status: child.status }, + expected: control.expected, + name, + ok: child.status !== 0 && output.includes(control.expected), + }; +} + +function writeReceipt(root) { + const receipt = { + attempt: 1, + intended_scenario_ids: ["responsive-layout"], + nonce: "a".repeat(64), + repository: "changeroa/StyleGallery", + revision: "b".repeat(40), + run_id: "consumer-conformance-sentinel-run", + session_id: "consumer-conformance-sentinel-session", + source: { sha256: "c".repeat(64) }, + }; + fs.writeFileSync(path.join(root, "page-evidence-session.json"), `${JSON.stringify(receipt)}\n`, { flag: "wx" }); +} + +function runPageEvidenceControl(name, control) { + const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-page-evidence-sentinel-")); + const observations = []; + try { + for (const outputKind of ["captures", "runner"]) { + const caseRoot = path.join(temporaryRoot, outputKind); + const artifactRoot = path.join(caseRoot, "artifacts"); + const outside = path.join(caseRoot, "outside"); + fs.mkdirSync(artifactRoot, { recursive: true }); + fs.mkdirSync(outside, { recursive: true }); + writeReceipt(artifactRoot); + + const filename = outputKind === "captures" + ? "responsive-layout-state-w1024-focus.png" + : "responsive-layout.json"; + const outputPath = path.join(artifactRoot, outputKind, filename); + const outsidePath = path.join(outside, filename); + const marker = "do-not-overwrite"; + if (control.pageEvidence === "symlink") fs.symlinkSync(outside, path.join(artifactRoot, outputKind), "dir"); + else { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, marker, { flag: "wx" }); + } + + const child = runPlaywright("state-w1024-focus", { + CONSUMER_CONFORMANCE_CAPTURE_DIR: "", + CONSUMER_CONFORMANCE_MUTATION: "none", + PAGE_EVIDENCE_ARTIFACT_ROOT: artifactRoot, + PAGE_EVIDENCE_CASE_ID: "state-w1024-focus", + PAGE_EVIDENCE_SCENARIO_ID: "responsive-layout", + }); + const output = `${child.stdout}\n${child.stderr}`; + observations.push({ + named: output.includes(control.expected), + output_kind: outputKind, + outside_write: fs.existsSync(outsidePath), + preserved: control.pageEvidence === "existing-output" ? fs.readFileSync(outputPath, "utf8") === marker : true, + status: child.status, + }); + } + } finally { + fs.rmSync(temporaryRoot, { force: true, recursive: true }); + } + + return { + actual: { observations }, + expected: control.expected, + name, + ok: observations.every((observation) => observation.status !== 0 && observation.named && !observation.outside_write && observation.preserved), + }; +} + +function runCiCleanupControl(name, control) { + const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-ci-cleanup-sentinel-")); + const outside = fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-ci-cleanup-outside-")); + const marker = path.join(outside, "consumer", "preserve.txt"); + try { + fs.mkdirSync(path.dirname(marker), { recursive: true }); + fs.writeFileSync(marker, "preserve\n", { flag: "wx" }); + fs.symlinkSync(outside, path.join(workspace, ".tmp"), "dir"); + const child = spawnSync(process.execPath, [path.join(repositoryRoot, "scripts", "run-consumer-page-evidence-ci.mjs"), "cleanup", "--consumer-root", ".tmp/consumer"], { cwd: workspace, encoding: "utf8" }); + const output = `${child.stdout}\n${child.stderr}`; + return { + actual: { markerPreserved: fs.readFileSync(marker, "utf8") === "preserve\n", named: output.includes(control.expected), status: child.status }, + expected: control.expected, + name, + ok: child.status !== 0 && output.includes(control.expected) && fs.readFileSync(marker, "utf8") === "preserve\n", + }; + } finally { + fs.rmSync(workspace, { force: true, recursive: true }); + fs.rmSync(outside, { force: true, recursive: true }); + } +} + +const results = argumentFailures.length === 0 ? names.map((name) => { + const control = availableCases.get(name); + return control.pageEvidence ? runPageEvidenceControl(name, control) : control.ciCleanup ? runCiCleanupControl(name, control) : runMutationControl(name, control); +}) : []; + +const failures = [ + ...argumentFailures.map((message) => `argument_invalid:${message}`), + ...results.filter((result) => !result.ok).map((result) => `missing_semantic:${result.name}:${result.expected}`), +]; +const report = { failures, ok: failures.length === 0, results }; +if (json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +else if (report.ok) process.stdout.write(`consumer conformance sentinel passed ${results.length} mutation controls\n`); +else process.stderr.write(`${failures.join("\n")}\n`); +if (!report.ok) process.exitCode = 1; diff --git a/tests/consumer-conformance.spec.mjs b/tests/consumer-conformance.spec.mjs new file mode 100644 index 0000000..414b29f --- /dev/null +++ b/tests/consumer-conformance.spec.mjs @@ -0,0 +1,235 @@ +import { expect, test } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; +import { + layoutCases, + overlayCases, + stateCases, + zoomCases, +} from "./fixtures/consumer-conformance-scenarios.mjs"; +import { + capturePageEvidence, + captureVisualQa, + collectContrastViolations, + focusGeometry, + observeRuntimeErrors, + pageGeometry, + renderConsumerConformance, + setChromiumPageScale, +} from "./helpers/render-consumer-conformance.mjs"; + +const mutation = process.env.CONSUMER_CONFORMANCE_MUTATION ?? "none"; +const WCAG_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]; + +function sentinelTag(caseId) { + const tags = { + "dialog-w375": "@sentinel-dialog-focus-leak", + "layout-w320-full-unbroken": "@sentinel-overflow", + "state-w320-loading": "@sentinel-console-error", + "state-w375-default": "@sentinel-low-contrast", + "state-w375-focus": "@sentinel-clipped-focus", + }; + return tags[caseId] ? ` ${tags[caseId]}` : ""; +} + +async function expectGeometry(page) { + const geometry = await pageGeometry(page); + expect(geometry.document.scrollWidth, "document_no_horizontal_overflow").toBeLessThanOrEqual(geometry.document.clientWidth); + for (const owner of geometry.scrollOwners) { + expect(owner.scrollWidth, `scroll_owner_no_horizontal_overflow:${owner.id}`).toBeLessThanOrEqual(owner.clientWidth); + expect(["auto", "scroll"], `scroll_owner_declared:${owner.id}`).toContain(owner.overflowY); + } + for (const essential of geometry.essentials) { + expect(essential.width > 0 && essential.height > 0, `essential_nonzero:${essential.id}`).toBe(true); + expect(essential.intersectsViewport, `essential_observable:${essential.id}`).toBe(true); + expect(essential.fullyVisibleInViewport && essential.fullyVisibleInScrollOwner, `essential_fully_observable:${essential.id}`).toBe(true); + } +} + +async function expectContrast(page, roots = ["body"]) { + const violations = await collectContrastViolations(page, roots); + expect(violations, "color-contrast").toEqual([]); +} + +async function expectAxeClean(page, roots = ["body"]) { + const violations = []; + for (const root of roots) { + const result = await new AxeBuilder({ page }).include(root).withTags(WCAG_TAGS).analyze(); + violations.push(...result.violations.map((violation) => ({ + id: violation.id, + impact: violation.impact, + nodes: violation.nodes.map((node) => node.target), + root, + }))); + } + expect(violations, "axe_declared_wcag_tags").toEqual([]); +} + +async function expectRuntimeClean(errors) { + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(errors, "console_error_free").toEqual([]); +} + +async function expectProtectedPhrases(page, phrases = []) { + if (phrases.length === 0) return; + const lineCounts = await page.locator(".probe-content").evaluate((element, protectedPhrases) => protectedPhrases.map((phrase) => { + const text = element.textContent ?? ""; + const start = text.indexOf(phrase); + if (start < 0 || !element.firstChild) return 0; + const range = document.createRange(); + range.setStart(element.firstChild, start); + range.setEnd(element.firstChild, start + phrase.length); + return [...range.getClientRects()].filter((rect) => rect.width > 0 && rect.height > 0).length; + }), phrases); + expect(lineCounts, "cjk_semantic_phrase_single_line").toEqual(phrases.map(() => 1)); +} + +for (const scenario of layoutCases()) { + test(`layout ${scenario.caseId}${sentinelTag(scenario.caseId)}`, async ({ page }) => { + const errors = observeRuntimeErrors(page); + await page.setViewportSize(scenario.viewport); + await renderConsumerConformance(page, { kind: "layout", mutation, ...scenario }); + await expect(page.locator("main"), "main_landmark_present").toHaveCount(1); + await expect(page.getByRole("heading", { level: 1, name: "Consumer conformance probe" }), "heading_semantics").toBeVisible(); + await expect(page.getByRole("button", { name: scenario.content.label }), "content_action_semantics").toMatchAriaSnapshot(`- button "${scenario.content.label}"`); + await expectGeometry(page); + await expectContrast(page); + await expectAxeClean(page); + await expectProtectedPhrases(page, scenario.content.protectedPhrases); + await expectRuntimeClean(errors); + await captureVisualQa(page, { caseId: scenario.caseId }); + await capturePageEvidence(page, { caseId: scenario.caseId, pageScaleFactor: 1 }); + }); +} + +for (const scenario of stateCases()) { + test(`state ${scenario.caseId}${sentinelTag(scenario.caseId)}`, async ({ page }) => { + const errors = observeRuntimeErrors(page); + await page.setViewportSize(scenario.viewport); + await renderConsumerConformance(page, { kind: "state", mutation, ...scenario }); + const control = page.getByRole("button", { name: "Submit migration" }); + let visualCaptured = false; + await expect(control, "state_button_semantics").toBeVisible(); + + if (scenario.state === "hover") { + await control.hover(); + await expect(control, "hover_state_observed").toHaveAttribute("data-observed-state", "hover"); + } else if (scenario.state === "focus") { + await page.keyboard.press("Tab"); + await expect(control, "focus_state_observed").toBeFocused(); + const geometry = await focusGeometry(control); + expect(geometry.visible && !geometry.clippedBy, "focus_geometry_visible").toBe(true); + } else if (scenario.state === "active") { + const box = await control.boundingBox(); + expect(box, "active_control_box").toBeTruthy(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await expect(control, "active_state_observed").toHaveAttribute("data-observed-state", "active"); + await captureVisualQa(page, { caseId: scenario.caseId }); + visualCaptured = true; + await page.mouse.up(); + } else if (scenario.state === "disabled") { + await expect(control, "disabled_state_semantics").toBeDisabled(); + } else if (scenario.state === "error") { + await expect(control, "error_state_semantics").toHaveAttribute("aria-invalid", "true"); + await expect(page.getByRole("alert"), "error_message_semantics").toHaveText("Migration could not be saved."); + } else if (scenario.state === "loading") { + await expect(control, "loading_state_semantics").toHaveAttribute("aria-busy", "true"); + await expect(page.getByRole("status"), "loading_status_semantics").toHaveText("Saving migration"); + } else { + await expect(control, "default_state_semantics").toHaveAttribute("data-observed-state", "default"); + } + + await expectGeometry(page); + await expectContrast(page); + await expectAxeClean(page); + await expectRuntimeClean(errors); + if (!visualCaptured) await captureVisualQa(page, { caseId: scenario.caseId }); + await capturePageEvidence(page, { caseId: scenario.caseId, pageScaleFactor: 1 }); + }); +} + +for (const scenario of overlayCases()) { + test(`overlay ${scenario.caseId}${sentinelTag(scenario.caseId)}`, async ({ page }) => { + const errors = observeRuntimeErrors(page); + await page.setViewportSize(scenario.viewport); + await renderConsumerConformance(page, { kind: "overlay", mutation, ...scenario }); + const opener = page.getByRole("button", { name: `Open ${scenario.overlay.label}` }); + const overlay = page.getByRole("dialog", { name: scenario.overlay.label }); + const first = overlay.getByRole("button", { name: `First ${scenario.overlay.id} action` }); + const last = overlay.getByRole("button", { name: `Close ${scenario.overlay.label}` }); + const outside = page.locator("#outside-control"); + + await expect(overlay, "overlay_closed_initially").toBeHidden(); + await opener.click(); + await expect(overlay, "overlay_open_semantics").toBeVisible(); + await expect(overlay, "overlay_aria_snapshot").toMatchAriaSnapshot(` + - dialog "${scenario.overlay.label}": + - heading "${scenario.overlay.label}" [level=2] + - button "First ${scenario.overlay.id} action" + - link "${scenario.overlay.label} details" + - button "Close ${scenario.overlay.label}" + `); + await expect(first, "overlay_initial_focus").toBeFocused(); + await page.keyboard.press("Shift+Tab"); + await expect(last, "dialog_focus_trap").toBeFocused(); + await page.keyboard.press("Tab"); + await expect(first, "dialog_focus_trap").toBeFocused(); + const outsideBox = await outside.boundingBox(); + expect(outsideBox, "dialog_background_control_box").toBeTruthy(); + await page.mouse.click(outsideBox.x + outsideBox.width / 2, outsideBox.y + outsideBox.height / 2); + await expect(first, "dialog_pointer_focus_containment").toBeFocused(); + await outside.evaluate((element) => element.focus()); + await expect(first, "dialog_programmatic_focus_containment").toBeFocused(); + await expectContrast(page, ["body", "[data-overlay]:not([hidden])"]); + await expectAxeClean(page, ["body", "[data-overlay]:not([hidden])"]); + await captureVisualQa(page, { caseId: scenario.caseId, suffix: "open" }); + await page.keyboard.press("Escape"); + await expect(overlay, "overlay_escape_close").toBeHidden(); + await expect(opener, "overlay_escape_focus_return").toBeFocused(); + + await opener.click(); + await last.click(); + await expect(overlay, "overlay_button_close").toBeHidden(); + await expect(opener, "overlay_button_focus_return").toBeFocused(); + await expectGeometry(page); + await expectRuntimeClean(errors); + await capturePageEvidence(page, { caseId: scenario.caseId, pageScaleFactor: 1 }); + }); +} + +for (const scenario of zoomCases()) { + test(`zoom ${scenario.caseId}`, async ({ page }) => { + const errors = observeRuntimeErrors(page); + await page.setViewportSize({ height: scenario.viewport.height, width: scenario.viewport.width }); + await renderConsumerConformance(page, { + caseId: scenario.caseId, + container: { id: "medium", inlineSize: "30rem" }, + content: { body: "Zoomed content remains readable and operable.", id: "zoom", label: "Zoom probe" }, + kind: "layout", + mutation, + viewport: scenario.viewport, + }); + await setChromiumPageScale(page, scenario.viewport.pageScaleFactor); + const scale = await page.evaluate(() => window.visualViewport?.scale); + expect(scale, "chromium_page_scale_probe").toBe(scenario.viewport.pageScaleFactor); + await page.keyboard.press("Tab"); + const control = page.getByRole("button", { name: "Zoom probe" }); + await expect(control, "zoom_focus_observable").toBeFocused(); + const essential = await control.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const viewport = window.visualViewport; + const style = getComputedStyle(element); + const focusExtent = (Number.parseFloat(style.outlineWidth) || 0) + Math.max(0, Number.parseFloat(style.outlineOffset) || 0); + return rect.top - focusExtent >= viewport.offsetTop + && rect.left - focusExtent >= viewport.offsetLeft + && rect.bottom + focusExtent <= viewport.offsetTop + viewport.height + && rect.right + focusExtent <= viewport.offsetLeft + viewport.width; + }); + expect(essential, "zoom_essential_observable").toBe(true); + await expectContrast(page); + await expectAxeClean(page); + await expectRuntimeClean(errors); + await captureVisualQa(page, { caseId: scenario.caseId, suffix: "chromium-page-scale-probe" }); + await capturePageEvidence(page, { caseId: scenario.caseId, pageScaleFactor: scenario.viewport.pageScaleFactor, probe: "chromium_page_scale_probe" }); + }); +} diff --git a/tests/fixtures/consumer-conformance-scenarios.mjs b/tests/fixtures/consumer-conformance-scenarios.mjs new file mode 100644 index 0000000..4d9d5a5 --- /dev/null +++ b/tests/fixtures/consumer-conformance-scenarios.mjs @@ -0,0 +1,80 @@ +export const VIEWPORTS = Object.freeze([ + Object.freeze({ height: 720, width: 320 }), + Object.freeze({ height: 812, width: 375 }), + Object.freeze({ height: 768, width: 768 }), + Object.freeze({ height: 768, width: 1024 }), + Object.freeze({ height: 900, width: 1440 }), +]); + +export const CONTAINERS = Object.freeze([ + Object.freeze({ id: "tight", inlineSize: "240px" }), + Object.freeze({ id: "medium", inlineSize: "32rem" }), + Object.freeze({ id: "roomy", inlineSize: "54rem" }), + Object.freeze({ id: "full", inlineSize: "100%" }), +]); + +export const CONTENTS = Object.freeze([ + Object.freeze({ body: "", id: "empty", label: "Empty content probe" }), + Object.freeze({ body: "A deliberately long navigation label that must wrap without widening its container", id: "long-label", label: "Long label probe" }), + Object.freeze({ + body: "Migration evidence remains consumer-owned. This intentionally long paragraph exercises line wrapping, intrinsic sizing, and stable reading order without claiming that a synthetic fixture certifies a product.", + id: "long-paragraph", + label: "Long paragraph probe", + }), + Object.freeze({ + body: "소비자가\u00a0증거를\u00a0실행합니다. 소비자가\u00a0결과를\u00a0검증합니다. 긴\u00a0문장도\u00a0잘\u00a0줄바꿈됩니다. 의미도\u00a0온전히\u00a0유지됩니다.", + id: "cjk", + label: "CJK content probe", + protectedPhrases: Object.freeze([ + "소비자가\u00a0증거를\u00a0실행합니다.", + "소비자가\u00a0결과를\u00a0검증합니다.", + "긴\u00a0문장도\u00a0잘\u00a0줄바꿈됩니다.", + "의미도\u00a0온전히\u00a0유지됩니다.", + ]), + }), + Object.freeze({ body: `unbroken-${"consumerconformance".repeat(12)}`, id: "unbroken", label: "Unbroken content probe" }), +]); + +export const STATES = Object.freeze(["default", "hover", "focus", "active", "disabled", "error", "loading"]); + +export const OVERLAYS = Object.freeze([ + Object.freeze({ id: "drawer", label: "Navigation drawer" }), + Object.freeze({ id: "dialog", label: "Migration confirmation" }), +]); + +export const ZOOM_CASES = Object.freeze([ + Object.freeze({ height: 768, pageScaleFactor: 2, width: 1024 }), + Object.freeze({ height: 900, pageScaleFactor: 2, width: 1440 }), +]); + +export function layoutCases() { + return VIEWPORTS.flatMap((viewport) => CONTAINERS.flatMap((container) => CONTENTS.map((content) => ({ + caseId: `layout-w${viewport.width}-${container.id}-${content.id}`, + container, + content, + viewport, + })))); +} + +export function stateCases() { + return VIEWPORTS.flatMap((viewport) => STATES.map((state) => ({ + caseId: `state-w${viewport.width}-${state}`, + state, + viewport, + }))); +} + +export function overlayCases() { + return VIEWPORTS.flatMap((viewport) => OVERLAYS.map((overlay) => ({ + caseId: `${overlay.id}-w${viewport.width}`, + overlay, + viewport, + }))); +} + +export function zoomCases() { + return ZOOM_CASES.map((viewport) => ({ + caseId: `zoom-w${viewport.width}-scale${viewport.pageScaleFactor}`, + viewport, + })); +} diff --git a/tests/helpers/render-consumer-conformance.mjs b/tests/helpers/render-consumer-conformance.mjs new file mode 100644 index 0000000..9eb4bce --- /dev/null +++ b/tests/helpers/render-consumer-conformance.mjs @@ -0,0 +1,453 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; + +const DEFAULT_EVIDENCE_CASE = "state-w1024-focus"; +const identityPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const scenarioPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function commonStyles(mutation) { + const text = mutation === "low-contrast" ? "rgb(156 163 175)" : "rgb(31 41 55)"; + const overflow = mutation === "overflow" ? "body::after{content:'';display:block;inline-size:200vw;block-size:1px}" : ""; + const clippedFocus = mutation === "clipped-focus" ? ".focus-frame{overflow:hidden;padding:0}" : ""; + return ` + :root{ + --canvas:rgb(255 255 255); + --error-canvas:rgb(254 242 242); + --error-text:rgb(153 27 27); + --focus:rgb(180 83 9); + --muted:rgb(75 85 99); + --primary:rgb(29 78 216); + --primary-active:rgb(30 58 138); + --primary-hover:rgb(30 64 175); + --surface:rgb(248 250 252); + --text:${text}; + } + *,*::before,*::after{box-sizing:border-box} + html,body{margin:0;min-block-size:100%} + body{background:var(--canvas);color:var(--text);font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.5;padding:16px} + button,a{font:inherit} + button{background:var(--primary);border:2px solid var(--primary);color:rgb(255 255 255);cursor:pointer;min-block-size:44px;padding:8px 16px} + button:hover{background:var(--primary-hover)} + button:active{background:var(--primary-active)} + button:disabled{background:rgb(71 85 105);border-color:rgb(71 85 105);cursor:not-allowed} + button:focus-visible,a:focus-visible{outline:3px solid var(--focus);outline-offset:3px} + h1,h2,p{margin-block:0} + h1{font-size:clamp(1.5rem,4vw,2rem);line-height:1.2} + main{display:grid;gap:16px;inline-size:100%;margin-inline:auto;max-inline-size:72rem;min-inline-size:0} + .probe-container{background:var(--surface);display:grid;gap:12px;inline-size:min(100%,var(--container-inline-size));max-block-size:calc(100vh - 96px);min-inline-size:0;overflow:auto;padding:16px} + .probe-content{min-inline-size:0;overflow-wrap:anywhere;word-break:normal} + .probe-content[data-content-kind="cjk"]{word-break:keep-all} + .focus-frame{inline-size:min(100%,32rem);padding:8px} + .focus-frame>button{inline-size:100%} + .state-error{background:var(--error-canvas);color:var(--error-text);padding:8px} + .state-status{color:var(--muted);padding:8px} + .overlay-backdrop{background:rgb(15 23 42 / .35);inset:0;position:fixed;z-index:1} + .overlay-shell{background:var(--surface);display:grid;gap:12px;inset:16px;max-block-size:calc(100vh - 32px);overflow:auto;padding:24px;position:fixed;z-index:2} + .overlay-shell[data-kind="drawer"]{inset-inline-start:auto;inline-size:min(22rem,calc(100vw - 32px))} + .overlay-shell[data-kind="dialog"]{inset:50% auto auto 50%;inline-size:min(32rem,calc(100vw - 32px));transform:translate(-50%,-50%)} + .overlay-actions{display:grid;gap:12px} + .overlay-actions>a{color:rgb(29 78 216);padding:8px} + [hidden]{display:none!important} + ${overflow} + ${clippedFocus} + `; +} + +function layoutMarkup(options) { + const content = options.content; + return ` +
+

Consumer conformance probe

+
+

Container probe

+

${escapeHtml(content.body)}

+ +
+
+ `; +} + +function stateMarkup(options) { + const state = options.state; + const attributes = [ + state === "disabled" ? "disabled" : "", + state === "error" ? 'aria-describedby="migration-error" aria-invalid="true"' : "", + state === "loading" ? 'aria-busy="true" aria-describedby="migration-status"' : "", + ].filter(Boolean).join(" "); + const message = state === "error" + ? '' + : state === "loading" + ? '

Saving migration

' + : ""; + return ` +
+

Consumer conformance probe

+
+
+ +
+ ${message} +
+
+ `; +} + +function overlayMarkup(options) { + const overlay = options.overlay; + return ` +
+

Consumer conformance probe

+ + + + +
+ `; +} + +function runtimeScript(options) { + const runtime = JSON.stringify({ kind: options.kind, mutation: options.mutation }).replaceAll("<", "\\u003c"); + return ` + const runtime = ${runtime}; + const stateControl = document.getElementById("state-control"); + if (stateControl) { + const intended = stateControl.dataset.intendedState; + if (intended === "hover") stateControl.addEventListener("pointerenter", () => { stateControl.dataset.observedState = "hover"; }); + if (intended === "focus") stateControl.addEventListener("focus", () => { stateControl.dataset.observedState = "focus"; }); + if (intended === "active") { + stateControl.addEventListener("pointerdown", () => { stateControl.dataset.observedState = "active"; }); + stateControl.addEventListener("pointerup", () => { stateControl.dataset.observedState = "default"; }); + } + } + const opener = document.getElementById("overlay-opener"); + const overlay = document.getElementById("consumer-overlay"); + const backdrop = document.getElementById("overlay-backdrop"); + const first = document.getElementById("overlay-first"); + const close = document.getElementById("overlay-close"); + if (opener && overlay && backdrop && first && close) { + const background = [...document.querySelectorAll("#consumer-main > :not(#consumer-overlay):not(#overlay-backdrop)")]; + const containFocus = runtime.mutation !== "dialog-focus-leak"; + const closeOverlay = () => { + overlay.hidden = true; + backdrop.hidden = true; + for (const element of background) element.inert = false; + opener.setAttribute("aria-expanded", "false"); + opener.focus(); + }; + opener.setAttribute("aria-controls", "consumer-overlay"); + opener.setAttribute("aria-expanded", "false"); + opener.addEventListener("click", () => { + overlay.hidden = false; + if (containFocus) { + backdrop.hidden = false; + for (const element of background) element.inert = true; + } + opener.setAttribute("aria-expanded", "true"); + first.focus(); + }); + close.addEventListener("click", closeOverlay); + document.addEventListener("pointerdown", (event) => { + if (!containFocus || overlay.hidden) return; + const interactive = event.target.closest('button,a[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'); + if (!overlay.contains(event.target) || !interactive) event.preventDefault(); + }, true); + document.addEventListener("focusin", (event) => { + if (containFocus && !overlay.hidden && !overlay.contains(event.target)) first.focus(); + }); + document.addEventListener("keydown", (event) => { + if (overlay.hidden) return; + if (event.key === "Escape") { event.preventDefault(); closeOverlay(); return; } + if (event.key !== "Tab" || !containFocus) return; + const focusable = [...overlay.querySelectorAll('button:not([disabled]),a[href]')]; + if (event.shiftKey && document.activeElement === focusable[0]) { + event.preventDefault(); + focusable.at(-1).focus(); + } else if (!event.shiftKey && document.activeElement === focusable.at(-1)) { + event.preventDefault(); + focusable[0].focus(); + } + }); + } + if (runtime.mutation === "console-error") console.error("deliberate consumer conformance mutation"); + `; +} + +export async function renderConsumerConformance(page, options) { + const markup = options.kind === "layout" + ? layoutMarkup(options) + : options.kind === "state" + ? stateMarkup(options) + : overlayMarkup(options); + await page.setContent(`Consumer conformance probe${markup}`); +} + +export function observeRuntimeErrors(page) { + const errors = []; + page.on("console", (message) => { + if (message.type() === "error") errors.push(`console:${message.text()}`); + }); + page.on("pageerror", (error) => errors.push(`page:${error.message}`)); + return errors; +} + +export async function pageGeometry(page) { + return page.evaluate(() => { + const viewport = { height: window.innerHeight, width: window.innerWidth }; + const observation = (element) => { + const rect = element.getBoundingClientRect(); + const scrollOwner = element.closest("[data-scroll-owner]"); + const ownerRect = scrollOwner?.getBoundingClientRect(); + return { + fullyVisibleInScrollOwner: !ownerRect || (rect.left >= ownerRect.left && rect.right <= ownerRect.right && rect.top >= ownerRect.top && rect.bottom <= ownerRect.bottom), + fullyVisibleInViewport: rect.left >= 0 && rect.right <= viewport.width && rect.top >= 0 && rect.bottom <= viewport.height, + height: rect.height, + id: element.id || element.dataset.essential || element.dataset.scrollOwner || element.tagName.toLowerCase(), + intersectsViewport: rect.bottom > 0 && rect.right > 0 && rect.top < viewport.height && rect.left < viewport.width, + width: rect.width, + }; + }; + return { + document: { clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth }, + essentials: [...document.querySelectorAll("[data-essential]")].map(observation), + scrollOwners: [...document.querySelectorAll("[data-scroll-owner]")].map((element) => ({ + clientWidth: element.clientWidth, + id: element.dataset.scrollOwner, + overflowY: getComputedStyle(element).overflowY, + scrollWidth: element.scrollWidth, + })), + }; + }); +} + +export async function focusGeometry(locator) { + return locator.evaluate((element) => { + const style = getComputedStyle(element); + const rect = element.getBoundingClientRect(); + const outlineWidth = Number.parseFloat(style.outlineWidth) || 0; + const outlineOffset = Number.parseFloat(style.outlineOffset) || 0; + const extent = outlineWidth + Math.max(0, outlineOffset); + const ring = { bottom: rect.bottom + extent, left: rect.left - extent, right: rect.right + extent, top: rect.top - extent }; + let clippedBy = null; + for (let ancestor = element.parentElement; ancestor && !clippedBy; ancestor = ancestor.parentElement) { + const ancestorStyle = getComputedStyle(ancestor); + if (![ancestorStyle.overflow, ancestorStyle.overflowX, ancestorStyle.overflowY].some((value) => value === "hidden" || value === "clip")) continue; + const bounds = ancestor.getBoundingClientRect(); + if (ring.left < bounds.left || ring.right > bounds.right || ring.top < bounds.top || ring.bottom > bounds.bottom) clippedBy = ancestor.id || ancestor.className || ancestor.tagName.toLowerCase(); + } + const visible = document.activeElement === element + && outlineWidth >= 2 + && style.outlineStyle !== "none" + && ring.left >= 0 + && ring.top >= 0 + && ring.right <= window.innerWidth + && ring.bottom <= window.innerHeight; + return { clippedBy, outlineOffset, outlineWidth, ring, visible }; + }); +} + +export async function collectContrastViolations(page, rootSelectors) { + return page.evaluate((selectors) => { + function parseColor(value) { + const match = value.match(/^rgba?\((.*)\)$/); + if (!match) return null; + const parts = match[1].trim().split(/[\s,\/]+/).filter(Boolean).map(Number); + if (parts.length < 3 || parts.slice(0, 3).some(Number.isNaN)) return null; + return { alpha: Number.isFinite(parts[3]) ? parts[3] : 1, channels: parts.slice(0, 3) }; + } + function composite(foreground, background) { + const alpha = foreground.alpha; + return foreground.channels.map((channel, index) => channel * alpha + background[index] * (1 - alpha)); + } + function backgroundFor(element) { + const ancestors = []; + for (let current = element; current; current = current.parentElement) ancestors.push(current); + let background = [255, 255, 255]; + for (const ancestor of ancestors.reverse()) { + const parsed = parseColor(getComputedStyle(ancestor).backgroundColor); + if (parsed && parsed.alpha > 0) background = composite(parsed, background); + } + return background; + } + function luminance(channels) { + const linear = channels.map((value) => { + const channel = value / 255; + return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]; + } + function ratio(foreground, background) { + const foregroundLuminance = luminance(foreground); + const backgroundLuminance = luminance(background); + return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05); + } + + const roots = selectors.flatMap((selector) => [...document.querySelectorAll(selector)]); + const elements = new Set(roots.flatMap((root) => [root, ...root.querySelectorAll("*")])); + const violations = []; + for (const element of elements) { + const hasDirectText = [...element.childNodes].some((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0); + if (!hasDirectText) continue; + const style = getComputedStyle(element); + const rect = element.getBoundingClientRect(); + if (style.display === "none" || style.visibility === "hidden" || rect.width === 0 || rect.height === 0) continue; + const foreground = parseColor(style.color); + if (!foreground) continue; + const background = backgroundFor(element); + const actualForeground = composite(foreground, background); + const actualRatio = ratio(actualForeground, background); + if (actualRatio < 4.5) violations.push({ + actual: Number(actualRatio.toFixed(2)), + element: `${element.tagName.toLowerCase()}${element.id ? `#${element.id}` : ""}`, + required: 4.5, + text: element.textContent.trim().slice(0, 80), + }); + } + return violations; + }, rootSelectors); +} + +export async function setChromiumPageScale(page, pageScaleFactor) { + const session = await page.context().newCDPSession(page); + try { + await session.send("Emulation.setPageScaleFactor", { pageScaleFactor }); + } finally { + await session.detach(); + } +} + +export async function captureVisualQa(page, { caseId, suffix = "rendered" } = {}) { + const configuredRoot = process.env.CONSUMER_CONFORMANCE_CAPTURE_DIR; + if (!configuredRoot) return; + const root = path.resolve(configuredRoot); + const safeCase = String(caseId).replaceAll(/[^a-z0-9-]/g, "-"); + const safeSuffix = String(suffix).replaceAll(/[^a-z0-9-]/g, "-"); + if (!safeCase || !safeSuffix) throw new Error("consumer conformance capture requires a safe case and suffix"); + const file = path.join(root, `${safeCase}-${safeSuffix}.png`); + if (!file.startsWith(`${root}${path.sep}`)) throw new Error("consumer conformance capture escaped its root"); + fs.mkdirSync(root, { recursive: true }); + if (fs.existsSync(file)) throw new Error(`consumer conformance capture already exists: ${file}`); + await page.screenshot({ animations: "disabled", caret: "hide", path: file }); +} + +function resolveEvidencePath(root, reference) { + if (typeof reference !== "string" || reference.length === 0 || path.isAbsolute(reference) || reference.includes("\\") || reference.split("/").some((segment) => segment === ".." || segment === "." || segment.length === 0)) { + throw new Error(`invalid PAGE_EVIDENCE path: ${reference}`); + } + const resolved = path.resolve(root, reference); + if (!resolved.startsWith(`${root}${path.sep}`)) throw new Error(`PAGE_EVIDENCE path escapes artifact root: ${reference}`); + return resolved; +} + +function pathEntry(file) { + try { + return fs.lstatSync(file); + } catch (error) { + if (error?.code === "ENOENT") return undefined; + throw error; + } +} + +function prepareEvidenceOutput(root, reference) { + const resolved = resolveEvidencePath(root, reference); + const rootEntry = pathEntry(root); + if (!rootEntry?.isDirectory() || rootEntry.isSymbolicLink()) throw new Error("page_evidence_artifact_root_invalid: artifact root must be a real directory"); + const canonicalRoot = fs.realpathSync(root); + const parentReference = path.posix.dirname(reference); + let directory = root; + + for (const segment of parentReference === "." ? [] : parentReference.split("/")) { + directory = path.join(directory, segment); + let entry = pathEntry(directory); + if (!entry) { + fs.mkdirSync(directory); + entry = pathEntry(directory); + } + if (entry.isSymbolicLink()) throw new Error(`page_evidence_output_symlink: ${reference}`); + if (!entry.isDirectory()) throw new Error(`page_evidence_output_parent_invalid: ${reference}`); + const canonicalDirectory = fs.realpathSync(directory); + if (canonicalDirectory !== canonicalRoot && !canonicalDirectory.startsWith(`${canonicalRoot}${path.sep}`)) throw new Error(`page_evidence_output_escape: ${reference}`); + } + + if (pathEntry(resolved)) throw new Error(`page_evidence_output_exists: ${reference}`); + return resolved; +} + +export async function capturePageEvidence(page, { caseId, pageScaleFactor, probe } = {}) { + const configuredRoot = process.env.PAGE_EVIDENCE_ARTIFACT_ROOT ?? process.env.PAGE_EVIDENCE_ARTIFACT_DIR; + if (!configuredRoot) return; + const selectedCase = process.env.PAGE_EVIDENCE_CASE_ID ?? DEFAULT_EVIDENCE_CASE; + if (caseId !== selectedCase) return; + + const root = path.resolve(configuredRoot); + const receiptFile = path.resolve(process.env.PAGE_EVIDENCE_SESSION_RECEIPT ?? path.join(root, "page-evidence-session.json")); + const receiptBytes = fs.readFileSync(receiptFile); + const receipt = JSON.parse(receiptBytes); + const intended = receipt.intended_scenario_ids; + const scenarioId = process.env.PAGE_EVIDENCE_SCENARIO_ID ?? (intended?.length === 1 ? intended[0] : undefined); + if (!scenarioPattern.test(scenarioId ?? "") || !intended?.includes(scenarioId)) throw new Error("PAGE_EVIDENCE_SCENARIO_ID must select one intended receipt scenario"); + if (!identityPattern.test(receipt.run_id ?? "") || !identityPattern.test(receipt.session_id ?? "")) throw new Error("PAGE_EVIDENCE receipt identities are invalid"); + + const safeCase = caseId.replaceAll(/[^a-z0-9-]/g, "-"); + const probeSlug = probe ? `-${probe.replaceAll(/[^a-z0-9-]/gi, "-").toLowerCase()}` : ""; + const screenshotReference = `captures/${scenarioId}-${safeCase}${probeSlug}.png`; + const runnerReference = process.env.PAGE_EVIDENCE_RUNNER_RESULT ?? `runner/${scenarioId}.json`; + fs.mkdirSync(root, { recursive: true }); + const screenshotFile = prepareEvidenceOutput(root, screenshotReference); + const runnerFile = prepareEvidenceOutput(root, runnerReference); + if (screenshotFile === runnerFile) throw new Error("page_evidence_output_collision: screenshot and runner paths must differ"); + await page.screenshot({ animations: "disabled", caret: "hide", path: screenshotFile }); + + const semanticEnvironment = await page.evaluate(({ scale }) => ({ + color_scheme: matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light", + locale: navigator.language, + page_scale_factor: scale, + reduced_motion: matchMedia("(prefers-reduced-motion: reduce)").matches ? "reduce" : "no-preference", + viewport: { height: window.innerHeight, width: window.innerWidth }, + }), { scale: pageScaleFactor }); + const browserVersion = page.context().browser()?.version() ?? "unknown"; + const runner = { + evidence: { artifacts: [{ media_type: "image/png", path: screenshotReference }], kind: "captured" }, + nonce: receipt.nonce, + receipt_sha256: crypto.createHash("sha256").update(receiptBytes).digest("hex"), + recorded_at: new Date().toISOString(), + record_kind: "page_evidence_runner_result", + repository: receipt.repository, + revision: receipt.revision, + run: { + attempt: receipt.attempt, + id: receipt.run_id, + repository: receipt.repository, + revision: receipt.revision, + source: process.env.GITHUB_ACTIONS === "true" ? "github_actions" : "local", + }, + scenario_id: scenarioId, + schema_version: "1.0", + semantic_environment: { + browser: `Chromium ${browserVersion}`, + browser_revision: process.env.PAGE_EVIDENCE_BROWSER_REVISION ?? browserVersion, + platform: `${process.platform}/${process.arch}`, + ...semanticEnvironment, + }, + session_id: receipt.session_id, + source_sha256: receipt.source.sha256, + status: "passed", + }; + fs.writeFileSync(runnerFile, `${JSON.stringify(runner, null, 2)}\n`, { flag: "wx" }); +} From 9ccda1d40c2c4a84cba448de325af6c34b4d06e0 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:38:57 +0900 Subject: [PATCH 43/44] docs(migration): publish consumer readiness method Plan-Task: 7 --- DOMAINS.md | 4 +- README.md | 2 + consumer-reference/contract.md | 13 ++ consumer-reference/index.md | 4 +- .../consumer-migration-readiness.md | 124 ++++++++++++++++++ design-engineering/index.md | 3 +- guides/documentation-mode-taxonomy.md | 4 + guides/layout-brief.md | 1 + guides/webpage-generation-workflow.md | 2 + index.md | 2 + quality/evidence/consumer-migration.md | 42 ++++++ quality/evidence/executable-evidence.md | 3 + quality/evidence/index.md | 1 + quality/gates/consumer-migration-evidence.md | 53 ++++++++ quality/gates/harmony-evaluation.md | 2 + quality/gates/index.md | 1 + quality/index.md | 3 + .../test-validate-consumer-conformance.mjs | 23 ++++ scripts/test-validate-consumer-reference.mjs | 70 +++++++++- scripts/test-validate-domains.mjs | 13 +- scripts/test-validate-ia.mjs | 29 +++- scripts/test-validate-webpage-workflow.mjs | 50 +++++++ scripts/validate-consumer-reference.mjs | 97 ++++++++++---- scripts/validate-domains.mjs | 1 + scripts/validate-ia.mjs | 10 ++ scripts/validate-webpage-workflow.mjs | 40 ++++++ 26 files changed, 551 insertions(+), 46 deletions(-) create mode 100644 design-engineering/consumer-migration-readiness.md create mode 100644 quality/evidence/consumer-migration.md create mode 100644 quality/gates/consumer-migration-evidence.md diff --git a/DOMAINS.md b/DOMAINS.md index 79b7312..38a31ab 100644 --- a/DOMAINS.md +++ b/DOMAINS.md @@ -25,7 +25,7 @@ This manifest is the source of truth for top-level StyleGallery domains. A domai | --- | --- | --- | | Layout | `layout/index.md` | Existing `GUIDE.md`, Layout-specific `guides/*.md`, and `recipes/*.md`; generated `patterns/**/*.md` and `CATALOG.md` remain at current paths. Shared `quality/**/*.md` infrastructure governs every domain without becoming a Layout leaf. | | Motion | `motion/index.md` | `motion/vocabulary.md`, `motion/review-workflow.md`, `motion/practice-reference.md` | -| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md`, `design-engineering/reference-profiles/index.md`, `design-engineering/reference-profiles/governed-local/index.md`, `design-engineering/reference-profiles/external-adaptation/index.md` | +| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md`, `design-engineering/consumer-migration-readiness.md`, `design-engineering/reference-profiles/index.md`, `design-engineering/reference-profiles/governed-local/index.md`, `design-engineering/reference-profiles/external-adaptation/index.md` | | Platform Guides | `platform-guides/index.md` | `platform-guides/apple-interaction.md` | ## Shared Non-Domain Infrastructure @@ -43,6 +43,8 @@ All five new domain leaves are independent method rewrites inspired by [emilkowa - If recognizable upstream expression is added later, preserve the full upstream MIT notice and record the copied material separately before merge. - Tracked repository documents must not depend on `.omo/`; stable upstream blob links and tracked repository contracts carry contributor-facing provenance. +`design-engineering/consumer-migration-readiness.md` is the separately declared StyleGallery-local leaf. It uses `provenance_kind: local`, carries no external source fields, and remains experimental. Its presence does not change the external-adaptation inventory or make its consumer-local method universal policy. + ## Promotion And Staleness External adaptations begin `experimental`. Promotion to `stable` requires a demonstrated reader or review task, evidence that the document improves that task, a named review owner, and no unresolved provenance or platform-version debt. Remove or revise a page when its source revision changes materially, a platform claim becomes stale, a local quality gate contradicts it, or a reader test shows the route is misleading. diff --git a/README.md b/README.md index 5b6a224..ea2924c 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Each common task has one primary route. Use secondary links only after the prima | `check whether a layout or design claim is admissible` | [Quality Gates](quality/index.md) | It routes claims to gates and evidence boundaries. | | `prove repository checks and evidence coverage` | [Executable Evidence Coverage](quality/evidence/executable-evidence.md) | It maps validators, fixtures, CI commands, and their boundaries. | | `declare consumer reference applicability` | [Consumer Reference](consumer-reference/index.md) | It provides the required handoff field without moving consumer values into Layout. | +| `prove an existing consumer migration` | [Consumer Migration Readiness](design-engineering/consumer-migration-readiness.md) | It requires thirteen explicit behavior classifications, runtime proof, adoption mappings, and source-bound page evidence when applicable. | | `change generated patterns, catalog, or governance policy` | [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md) | It identifies source files, generated artifacts, validators, lifecycle state, and review ownership. | | `run findability QA` | [Tree-Test Findability QA](quality/index.md#tree-test-findability-qa) | It tests whether task routes are discoverable, not just linked. | @@ -90,6 +91,7 @@ Each common task has one primary route. Use secondary links only after the prima - Use [Layout Pattern Catalog](CATALOG.md) when you already know the spatial problem. - Use [Quality Gates](quality/index.md) when a claim needs principle-backed evidence, visual QA boundaries, accessibility precedence, or design rationale. - Use [Consumer Reference](consumer-reference/index.md) when an implementation handoff must declare one repository-local JSON reference or a sentence explaining non-applicability. +- Use [Consumer Migration Readiness](design-engineering/consumer-migration-readiness.md) only for a migration that declares a consumer-local conformance record; ordinary handoffs keep the existing `not_applicable` path. - Use [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md) before changing generated artifacts, validators, lifecycle state, or ownership policy. ## Layout Domain Principles diff --git a/consumer-reference/contract.md b/consumer-reference/contract.md index 99ad61a..91fe712 100644 --- a/consumer-reference/contract.md +++ b/consumer-reference/contract.md @@ -27,6 +27,17 @@ Consumer reference reason: This handoff does not require consumer-specific visua `not_applicable` requires a sentence reason. A declared handoff points to exactly one canonical record and does not duplicate its owner, maturity, support, scope, or artifact data. +### Migration-Only Extension + +An ordinary implementation handoff uses only the shape above. When an existing consumer is being migrated and explicitly elects the experimental [Consumer Migration Readiness](../design-engineering/consumer-migration-readiness.md) method, add: + +```txt +Consumer migration conformance: declared +Consumer migration conformance record: path/to/consumer-conformance.json +``` + +This field is migration-only. Do not add it to generic blank handoffs, and do not replace their `Consumer reference: not_applicable` declaration. The conformance record is consumer-owned, follows `schema/consumer-conformance-record.schema.json`, and resolves relative to the consumer repository root. The consumer-reference validator resolves and executes every declared migration conformance record; it propagates conformance findings and, for applicable page evidence, derives the artifact root from the record's manifest path. Ordinary handoffs without this declaration do not execute the migration validator. + ## Repository-Local Record Boundary `declared` accepts only a normalized POSIX repository-relative path ending in `.json`. The path must exist beneath the current repository root and contain valid JSON. Absolute paths, URI schemes, network paths, query or fragment redirects, `..` segments, non-normalized paths, filesystem redirects, and symlink escapes are rejected. @@ -42,6 +53,8 @@ External consumers keep their canonical record in their own repository and apply - Repository-owned fixtures disclose `fixture_independence: "related"`; they do not count as independent consumers. - Current ownership truth is `owner.enforcement: "placeholder"` and `review_independence: "single_account"`. Boolean aliases are not accepted. +Every runtime scenario binds to regular files at the pinned consumer revision and to a strict JSON result receipt. The validator resolves the declared consumer repository and revision, recomputes each source digest from Git blobs, and checks the receipt against the scenario and consumer identity even when page evidence is `not_applicable`. Every adopted StyleGallery source must likewise resolve to a real path and heading anchor at its pinned StyleGallery revision. + ## Ownership And Dependency Consumer-reference infrastructure is owned by repository governance and validation review, currently represented by the same `@changeroa` placeholder account. This is one account, not independent review. diff --git a/consumer-reference/index.md b/consumer-reference/index.md index 25597c0..3f7d38b 100644 --- a/consumer-reference/index.md +++ b/consumer-reference/index.md @@ -13,6 +13,8 @@ Out of scope: profiles, themes, components, browser runtime, decorative styling, ## Documents - [Receiver contract](contract.md) - Required handoff shape, record-path boundary, lifecycle fields, ownership truth, and dependency direction. +- [Consumer migration conformance schema](schema/consumer-conformance-record.schema.json) - Closed consumer-local record with thirteen explicit migration dimensions, runtime scenarios, adoption mappings, and optional page evidence. +- [Page evidence session schema](schema/page-evidence-session.schema.json) and [manifest schema](schema/page-evidence-manifest.schema.json) - Source-bound capture lifecycle for an applicable migration page. - [Consumer reference item schema](schema/item.schema.json) - Machine-readable shape for the current receiver item. - [Promotion RFC schema](schema/promotion-rfc.schema.json) - Closed JSON shape for deferred or rejected invariant-promotion proposals. - [Shared-experimental promotion policy](policies/shared-experimental.json) - Count scope, normative bypass, lifecycle, and zero-promotion claim boundary. @@ -24,4 +26,4 @@ Out of scope: profiles, themes, components, browser runtime, decorative styling, ## IA Navigation Parent: [StyleGallery](../index.md). -Next: [Consumer Reference Receiver Contract](contract.md). +Next: [Consumer Migration Readiness](../design-engineering/consumer-migration-readiness.md). diff --git a/design-engineering/consumer-migration-readiness.md b/design-engineering/consumer-migration-readiness.md new file mode 100644 index 0000000..cff2143 --- /dev/null +++ b/design-engineering/consumer-migration-readiness.md @@ -0,0 +1,124 @@ +--- +type: Domain Guide +title: Consumer Migration Readiness +description: Evidence-bound method for deciding whether a consumer migration preserves its declared behavior. +domain: design-engineering +lifecycle: experimental +provenance_kind: local +--- + +# Consumer Migration Readiness + +Use this method only when an existing consumer is being migrated and its behavior must remain demonstrably equivalent. It turns migration intent into a consumer-owned conformance record and executable page evidence without making StyleGallery the owner of product code or visual values. + +## Repository Boundary + +The consuming repository owns its conformance record, source paths, runtime scenarios, evidence artifacts, adoption mappings, deviations, debt, and approval decision. StyleGallery supplies an experimental method, closed schemas, and validators. It does not certify the consumer, define product defaults, or turn a local adoption into a shared standard. + +Ordinary implementation handoffs remain unchanged and may use `Consumer reference: not_applicable` with a sentence reason. Only a migration handoff that elects this method adds `Consumer migration conformance: declared` and one normalized repository-relative JSON record path. + +## Reusable Method + +1. Inventory the old and new consumer behavior before changing implementation. +2. Create one consumer-local record using the [consumer conformance schema](../consumer-reference/schema/consumer-conformance-record.schema.json). +3. Explicitly classify every migration dimension as `applicable` with runtime scenario IDs or `not_applicable` with a sentence reason. +4. Map each adopted StyleGallery source anchor to a concrete local component, selector, or module plus its local decision, deviations, debt, and scenario IDs. +5. Execute the named unit, integration, or browser command. A prose checklist, screenshot, or generated fixture alone is not runtime proof. +6. For page work, create one source-bound evidence session, execute the browser matrix, finalize the manifest, and validate the completed session against the conformance record. +7. Review the [consumer migration evidence gate](../quality/gates/consumer-migration-evidence.md). Block migration when required evidence is missing, stale, cross-session, source-drifted, or failed. + +## Migration Dimension Contract + +The record must classify exactly these thirteen dimensions: + +| Dimension | What the consumer must preserve or explicitly exclude | +| --- | --- | +| `behavior_inventory` | The complete named behavior surface being migrated. | +| `route_parity` | Routes, entry points, and navigation outcomes. | +| `field_parity` | Accepted fields, output fields, validation, and serialization. | +| `action_parity` | User and programmatic actions plus observable outcomes. | +| `state_transitions` | Allowed state changes, guards, and terminal states. | +| `contract_precedence` | Which rule wins when old, new, and local contracts conflict. | +| `direct_mutation` | Effects of direct state or model mutation. | +| `indirect_mutation` | Effects reached through events, adapters, or dependent modules. | +| `persistence_round_trip` | Save, reload, serialization, and restoration behavior. | +| `reset_boundary` | What reset clears, preserves, and returns to defaults. | +| `exact_time_boundary` | Inclusive or exclusive timing behavior at exact boundaries. | +| `defaults_tri_state_mapping` | Mapping among absent, explicit default, and explicit non-default values. | +| `atomic_batch_behavior` | All-or-nothing behavior, ordering, and rollback for grouped changes. | + +An `applicable` dimension references at least one declared runtime scenario. A `not_applicable` dimension includes a concrete sentence reason. Silence is never a classification. + +## Opinionated Guidance + +- Prefer the smallest scenario that proves an observable outcome over broad internal coverage. +- Pin both the consumer revision and each adopted StyleGallery revision; a branch name or moving tag is insufficient. +- Record local deviations honestly. Conformance means the declared boundary is internally consistent, not that every StyleGallery suggestion was copied. +- Treat expired debt as a decision trigger. Do not rewrite evidence timestamps to make a stale review appear current. +- Keep page evidence tied to the exact source inventory and browser run that produced it. + +## Platform-Specific Guidance + +Browser page evidence uses the required viewport set `320`, `375`, `768`, `1024`, and `1440` pixels plus the declared container, content, state, overlay, and page-scale cases. Platform-specific raster identity may supplement computed assertions, but it cannot replace semantic, focus, overflow, or contrast checks and cannot imply cross-platform pixel equivalence. + +## Unsupported Absolutes + +Passing this method does not prove complete accessibility, product correctness, usability, visual quality, independent adoption, cross-browser equivalence, or owner approval. Synthetic fixtures prove validator behavior only. A pinned revision proves source identity, not source authority or consumer suitability. + +## Verification Contract + +The minimum verification chain is: + +```txt +consumer source inventory + -> conformance record with 13 explicit classifications + -> executable scenarios with zero exit and result artifacts + -> adoption mappings with pinned StyleGallery anchors + -> optional source-bound page-evidence manifest + -> consumer migration evidence gate decision +``` + +Validate a record without applicable page evidence directly: + +```sh +node /scripts/validate-consumer-conformance.mjs \ + --root \ + --record \ + --json +``` + +Unit and integration scenarios declare a shell-free Node `argv` array beginning with `node`. The validator checks out the exact consumer revision in an isolated worktree, enables Node's permission model without child-process, worker, native-addon, or WASI grants, supplies only `PATH`, `CI=1`, deterministic locale flags, `STYLEGALLERY_RESULT_ARTIFACT`, and `STYLEGALLERY_RUNTIME_CONTEXT`, and requires the command to create that result artifact during the run. Commands may read and write only inside the isolated checkout, must be self-contained at the pinned revision, and cannot override the governed permission flags. The validator does not reuse the caller's `node_modules` or forward caller secrets. + +When `page_evidence.status` is `applicable`, use one artifact root for the complete lifecycle. Start the source-bound session before the browser run, write each runner result beneath that artifact root, finalize the session, and then validate the record against the completed artifacts: + +```sh +node /scripts/create-page-evidence-session.mjs \ + --root \ + --record \ + --artifact-root \ + --json + +node /scripts/finalize-page-evidence.mjs \ + --root \ + --artifact-root \ + --runner-result \ + --review-by \ + --json + +node /scripts/validate-consumer-conformance.mjs \ + --root \ + --record \ + --artifact-root \ + --json +``` + +`` resolves from ``, and each `` resolves from ``. The completed manifest must match the consumer repository, revision, relevant source set, browser scenario set, run/session identities, and source digests. The StyleGallery checkout used for validation must contain every pinned StyleGallery revision; a shallow checkout that omits a historical pin fails closed. See [Consumer Migration Evidence](../quality/evidence/consumer-migration.md) for what each channel can and cannot support. + +## Source, License, And Attribution + +This is a StyleGallery-local method. It has no external source snapshot and intentionally declares `provenance_kind: local` rather than upstream repository metadata. + +## IA Navigation + +Parent: [Design Engineering](index.md). +Next: [Consumer Migration Evidence Gate](../quality/gates/consumer-migration-evidence.md). diff --git a/design-engineering/index.md b/design-engineering/index.md index 5858fdb..95823d5 100644 --- a/design-engineering/index.md +++ b/design-engineering/index.md @@ -11,6 +11,7 @@ Out of scope: a second universal principle set, unsupported claims of beauty or ## Documents - [Interface Craft Decisions](interface-craft.md) connects product purpose, implementation detail, and verification without treating taste as proof. +- [Consumer Migration Readiness](consumer-migration-readiness.md) turns migration intent into a consumer-owned thirteen-dimension conformance record and executable evidence chain. - [Reference Profiles](reference-profiles/index.md) provides two explicitly selected, related adversarial examples and a documentation-only external adaptation route. ## Relationship To Shared Quality @@ -24,4 +25,4 @@ See [StyleGallery Domains](../DOMAINS.md) for lifecycle, provenance, page member ## IA Navigation Parent: [StyleGallery](../index.md). -Next: [Interface Craft Decisions](interface-craft.md). +Next: [Consumer Migration Readiness](consumer-migration-readiness.md). diff --git a/guides/documentation-mode-taxonomy.md b/guides/documentation-mode-taxonomy.md index eec5556..beb6561 100644 --- a/guides/documentation-mode-taxonomy.md +++ b/guides/documentation-mode-taxonomy.md @@ -37,6 +37,7 @@ Do not use `mixed` as a mode. If a page has more than one reading posture, keep | `motion/*.md` | Reference or How-to | Explanation | Apply experimental motion terminology and review guidance inside its evidence boundary. | | `design-engineering/index.md` | Reference | Explanation | Enter Design Engineering product-level decision guidance. | | `design-engineering/*.md` | Explanation | How-to | Apply experimental practitioner methods without treating taste as proof. | +| `design-engineering/consumer-migration-readiness.md` | How-to | Policy/procedure | Build a consumer-owned migration evidence chain without claiming universal authority. | | `platform-guides/index.md` | Reference | Explanation | Enter bounded comparative platform references. | | `platform-guides/*.md` | Explanation | Reference | Compare a named platform with explicit source, version, adaptation, and evidence limits. | | `guides/decision-tree.md` | How-to | Reference | Route from constraints to pattern families and recipes. | @@ -73,11 +74,13 @@ Do not use `mixed` as a mode. If a page has more than one reading posture, keep | `quality/gates/accessibility-evidence.md` | Policy/procedure | Reference | Decide whether accessibility evidence supports a claim. | | `quality/gates/design-claim.md` | Policy/procedure | Reference | Decide whether a non-layout design claim is admissible. | | `quality/gates/harmony-evaluation.md` | Policy/procedure | Reference | Decide whether a webpage composition can guide implementation. | +| `quality/gates/consumer-migration-evidence.md` | Policy/procedure | Reference | Decide whether declared consumer migration evidence is sufficient. | | `quality/gates/layout.md` | Policy/procedure | Reference | Decide whether a layout claim satisfies the pattern contract. | | `quality/gates/rationale.md` | Policy/procedure | Reference | Decide whether a rationale exposes options, warrant, and debt. | | `quality/gates/visual-evidence.md` | Policy/procedure | Reference | Decide whether rendered evidence supports a visual claim. | | `quality/evidence/index.md` | Reference | None | Look up evidence reference families. | | `quality/evidence/accessibility.md` | Reference | Explanation | Understand accessibility evidence boundaries. | +| `quality/evidence/consumer-migration.md` | Reference | Explanation | Understand runtime, browser, session, adoption, freshness, and review boundaries for a migration. | | `quality/evidence/design-rationale.md` | Reference | Explanation | Understand design-rationale evidence boundaries. | | `quality/evidence/executable-evidence.md` | Reference | Policy/procedure | Map validators, tests, rendered QA, review, and source citations to claim boundaries. | | `quality/evidence/families.md` | Reference | Explanation | Look up normalized evidence-family names and boundaries. | @@ -123,6 +126,7 @@ The wildcard rows are intentional. Generated pattern pages share one stable refe | Choose the owning StyleGallery domain. | `DOMAINS.md` | The selected domain `index.md` | | Name or review product motion. | `motion/index.md` | The matching Motion reference or workflow | | Review a product-level craft decision. | `design-engineering/index.md` | `design-engineering/interface-craft.md`, then the relevant quality gate | +| Prove an existing consumer migration. | `design-engineering/consumer-migration-readiness.md` | `quality/gates/consumer-migration-evidence.md`, then the consumer-owned conformance record | | Compare an Apple interaction convention. | `platform-guides/index.md` | `platform-guides/apple-interaction.md` and current official sources | | Choose a layout when the pattern name is unknown. | `GUIDE.md` | `guides/decision-tree.md` | | Turn raw content into a homepage or webpage. | `guides/webpage-generation-workflow.md` | `recipes/homepage.md`, `quality/gates/harmony-evaluation.md` | diff --git a/guides/layout-brief.md b/guides/layout-brief.md index 4ecadad..4fc09c0 100644 --- a/guides/layout-brief.md +++ b/guides/layout-brief.md @@ -85,6 +85,7 @@ Implementation handoff: - Which viewport, content-stress, accessibility, and visual-evidence checks must pass before shipping? - Consumer reference: `not_applicable`. - Consumer reference reason: This blank planning template declares no consumer-specific reference record. +- Consumer migration conformance: omit for ordinary work; for an elected migration method, use `declared` plus one normalized consumer-local JSON record path. ## Minimum Required Fields By Use Case diff --git a/guides/webpage-generation-workflow.md b/guides/webpage-generation-workflow.md index 353abfa..d33a3f5 100644 --- a/guides/webpage-generation-workflow.md +++ b/guides/webpage-generation-workflow.md @@ -180,4 +180,6 @@ Accepted debt: Every handoff keeps the consumer-reference field. Replace the default `not_applicable` pair only when the consuming repository has one canonical record; then use `Consumer reference: declared` and a normalized repository-relative `.json` path under that repository root. See the [Consumer Reference Receiver Contract](../consumer-reference/contract.md). +Do not add a migration-conformance field to ordinary webpage work. When an existing consumer is explicitly being migrated, add `Consumer migration conformance: declared` and a normalized consumer-local JSON path, then follow [Consumer Migration Readiness](../design-engineering/consumer-migration-readiness.md). This extension does not change the ordinary `not_applicable` default. + The implementer builds from semantic HTML and layout patterns first, then applies product-level visual styling from the image reference outside reusable pattern CSS. The final page must still pass layout stress checks, accessibility evidence, and any visual-evidence protocol declared by the consuming product. diff --git a/index.md b/index.md index 255cab1..ca8d3ff 100644 --- a/index.md +++ b/index.md @@ -16,6 +16,7 @@ Primary role: OKF bundle map. - [Design Engineering](design-engineering/index.md) - Product-layer interface-craft decisions and verification questions. - [Platform Guides](platform-guides/index.md) - Bounded comparative references for named platforms. - [Consumer reference](consumer-reference/index.md) - Shared non-domain receiver contract for declared repository-local JSON records or reasoned non-applicability. +- [Consumer migration readiness](design-engineering/consumer-migration-readiness.md) - Experimental Design Engineering method for consumer-owned conformance, runtime proof, and source-bound page evidence. - [Repository guide](README.md) - StyleGallery domain routes, shared policy, and Layout authoring principles. - [Layout planning guide](GUIDE.md) - Pre-design entry point for choosing and composing StyleGallery Layout patterns. - [Webpage generation workflow](guides/webpage-generation-workflow.md) - Intake-to-implementation workflow for homepage and webpage creation. @@ -29,4 +30,5 @@ Primary role: OKF bundle map. - [Layout pattern catalog](CATALOG.md) - Generated index of Layout-domain patterns. - [Pattern categories](patterns/index.md) - Category index for generated layout patterns. - [Quality gates](quality/index.md) - Principle-backed gates for admissible layout and design evidence claims. +- [Consumer migration evidence gate](quality/gates/consumer-migration-evidence.md) - Blocking contract for declared migration evidence. - [Update log](log.md) - Bundle update history. diff --git a/quality/evidence/consumer-migration.md b/quality/evidence/consumer-migration.md new file mode 100644 index 0000000..355fe6a --- /dev/null +++ b/quality/evidence/consumer-migration.md @@ -0,0 +1,42 @@ +--- +type: Evidence Reference +title: Consumer Migration Evidence +description: Evidence channels and claim boundaries for consumer migration readiness. +--- + +# Consumer Migration Evidence + +Consumer migration evidence is a linked chain, not a single screenshot or successful validator invocation. + +## Evidence Channels + +| Channel | Supports | Does not support by itself | +| --- | --- | --- | +| Conformance schema and validator | The record is closed, all thirteen dimensions are explicit, paths are safe, and references are internally consistent. | The behavior inventory is complete or the product is correct. | +| Unit and integration scenarios | Named observable behavior passed at the pinned source digest and zero exit. | Browser layout, visual quality, or untested behavior. | +| Browser matrix | Named viewport, container, content, state, overlay, page-scale, focus, overflow, contrast, and semantic assertions passed in the executed browser. | Complete accessibility, usability, or cross-browser equivalence. | +| Page-evidence session | Artifacts came from one source-bound session and match the finalized manifest hashes and identities. | Independent provenance, design approval, or truth outside the declared run. | +| Adoption mapping | A pinned StyleGallery anchor was interpreted as a named local decision for a concrete consumer target. | Universal adoption or permission to import product styling into Layout. | +| Freshness audit | Evidence is within its declared advisory or blocking review window. | Continued correctness after source or environment changes. | +| Human review | A named reviewer evaluated the inventory, exclusions, deviations, debt, and claim boundary. | Facts outside the recorded review method and scope. | + +## Page Evidence Matrix + +When page evidence applies, execute widths `320`, `375`, `768`, `1024`, and `1440` pixels. Cover tight, medium, roomy, and full-width containers; empty, short, long, and unbroken content; applicable interactive states and overlays; and declared page-scale changes. Assert logical focus, absence of unusable overflow, sufficient computed contrast, and semantic landmarks in the live page. + +Raster capture is review material. Deterministic computed and semantic assertions are the blocking evidence. Platform-specific screenshots remain nonblocking unless a separately approved platform baseline contract says otherwise. + +## Source And Session Binding + +Create the page session before capture and finalize only from a clean, unchanged source inventory. A completed manifest must preserve the consumer repository and revision, exact relevant-source paths and digests, intended browser scenario IDs, run ID, session ID, runner exit, artifact paths, byte counts, dimensions where relevant, and SHA-256 hashes. Reject cross-revision, cross-run, cross-session, missing, extra, changed, symlinked, escaped, or replayed artifacts. + +For unit and integration evidence, the stored result must be structurally equivalent JSON to the result created by the declared Node `argv` in an isolated checkout of the pinned consumer revision. The governed Node permission boundary denies descendant processes, workers, native addons, and WASI; an `argv` cannot weaken those denials. Whitespace and object-key order may differ; parsed values may not. A prewritten receipt, a zero-exit no-op, current-worktree dependencies, and caller CI identity variables are not runtime proof. + +## Claim Boundary + +A passing record supports: “the named migration scenarios and evidence agree with this declared contract at these pinned revisions.” It cannot support: “the product is fully accessible,” “all behavior is preserved,” “the design is correct,” “StyleGallery certifies this consumer,” or “this local decision is a shared standard.” + +## IA Navigation + +Parent: [Evidence References](index.md). +Next: [Consumer Migration Readiness](../../design-engineering/consumer-migration-readiness.md). diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index 2fd29eb..36f45be 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -25,6 +25,9 @@ Executable evidence identifies which claims are machine-enforced, which are only | OKF files have required structural metadata and valid root index or log shapes. | `scripts/validate-okf.mjs` and `scripts/test-validate-okf.mjs` | `node scripts/validate-okf.mjs --json`; `node scripts/test-validate-okf.mjs --json` | `success_path` fixture returns `ok: true`. | Missing frontmatter, missing type, concept frontmatter on `index.md`, and malformed log date fixtures must fail. | Proves repository structure, not conceptual quality or source truth. | | Pattern documents carry the required spatial contract sections and restricted CSS/HTML shape. | `scripts/validate-patterns.mjs` and `scripts/test-validate-patterns.mjs` | `node scripts/validate-patterns.mjs --min-count 46 --json`; `node scripts/test-validate-patterns.mjs --json` | Generated patterns satisfy required fields, sections, CSS ordering, selector, and count rules. | Missing metadata, unsorted CSS, forbidden decorative properties, ID selectors, missing code blocks, missing HTML hooks, and missing contract sections must fail. Preferred accessibility detail labels emit warnings without blocking equivalent prose. | Proves the written pattern contract and static examples, not rendered layout quality. | | Webpage-generation workflow references remain present across guides, recipes, and gates. | `scripts/validate-webpage-workflow.mjs` and `scripts/test-validate-webpage-workflow.mjs` | `node scripts/validate-webpage-workflow.mjs --json`; `node scripts/test-validate-webpage-workflow.mjs --json` | The success fixture includes the required workflow references and contract snippets. | Missing route structure, required template fields, link targets, safety boundaries, or route order must fail. Preferred link labels and GUIDE wording emit warnings. | Proves required structure and safety boundaries are present, not that a generated webpage is visually harmonious. | +| Consumer migration readiness remains discoverable and keeps migration-only handoff semantics. | `scripts/validate-ia.mjs`, `scripts/test-validate-ia.mjs`, `scripts/validate-webpage-workflow.mjs`, and `scripts/test-validate-webpage-workflow.mjs` | Run both validators and fixture harnesses. | Root, domain, consumer-reference, quality gate, evidence, workflow, and taxonomy routes reach the method within two hops; ordinary templates retain reasoned non-applicability while migration guidance declares a separate field. | Missing method routes, missing thirteen-dimension wording, missing migration-only boundary, or replacing the ordinary handoff declaration must fail. | Proves documentation structure and declared handoff semantics, not a consumer migration result. | +| A consumer-owned migration record links thirteen explicit classifications, executable scenarios, adoption mappings, and applicable source-bound page evidence. | `scripts/validate-consumer-conformance.mjs`, `scripts/test-validate-consumer-conformance.mjs`, page-evidence lifecycle scripts, and their fixture harnesses. | Run the conformance and page-evidence contract suites; execute the browser matrix separately when page evidence applies. | Closed schemas, pinned revisions, safe paths, zero-exit scenario records, adoption mappings, and one finalized matching page session pass. | Missing dimensions, unknown scenarios, unsafe paths, source drift, failed runner, cross-run/session/revision reuse, missing/extra/tampered artifacts, and mismatched source or scenario sets fail with named findings. | Supports agreement at pinned revisions only; it does not certify accessibility, product correctness, usability, adoption, or approval. | +| Explicit evidence deadlines are audited weekly without inventing a repository-wide maximum age. | `scripts/audit-evidence-freshness.mjs`, `scripts/test-audit-evidence-freshness.mjs`, and `scripts/validate-governance.mjs` | `.github/workflows/evidence-freshness.yml` runs Monday at 05:00 UTC and by manual dispatch in advisory mode, emits annotations, and uploads the JSON report for 14 days. | Fresh, expiring, expired, review-due, and no-explicit-expiry records retain their declared semantics. | Malformed records and auditor failures remain blocking even though due-date findings are advisory. | The audit reports only explicit `expires_at` and `review_by` values; it does not infer a TTL, establish evidence quality, or revoke a claim automatically. | | Markdown links resolve inside the repository. | `scripts/validate-links.mjs` | `node scripts/validate-links.mjs --json` | All checked Markdown links resolve to local files or are intentionally external or anchors. | A local link that escapes the repository or points at a missing file must fail. | Proves link targets exist, not that the target content supports a claim. | | Catalog and generated pattern indexes match the pattern data source. | `scripts/validate-catalog.mjs` | `node scripts/validate-catalog.mjs --json` | `CATALOG.md`, category indexes, and expected pattern files match `scripts/pattern-data.mjs`. | Missing, unexpected, or unlisted pattern files must fail. | Proves catalog consistency, not pattern usefulness. | | Governance, lifecycle, generated-file, ownership, and stale-content policy remain discoverable and CI-enforced. | `scripts/validate-governance.mjs` and `scripts/test-validate-governance.mjs` | `node scripts/validate-governance.mjs --json`; `node scripts/test-validate-governance.mjs --json` | `GOVERNANCE.md`, `.github/CODEOWNERS`, generated warnings, generated metadata, root link targets, lifecycle states, `scheduled_stale_audit` metadata, and CI wiring are present. | Missing governance file, generated warning, generated metadata, CODEOWNERS coverage, or stale policy fixtures must fail. Preferred governance prose and link labels emit warnings. | Proves governance policy is present and linked, not that CODEOWNERS users have verified repository write access. | diff --git a/quality/evidence/index.md b/quality/evidence/index.md index c49f0fa..7eaf10e 100644 --- a/quality/evidence/index.md +++ b/quality/evidence/index.md @@ -7,6 +7,7 @@ Evidence references describe what sources and artifacts can support. They do not - [Sources](sources.md) - Source admissibility and source families. - [Evidence family glossary](families.md) - Normalized `validator`, `screenshot`, `accessibility`, `user`, `source`, and `rationale` families for claim records. - [Executable evidence](executable-evidence.md) - Validator, fixture, CI, rendered, review, and citation boundaries. +- [Consumer migration evidence](consumer-migration.md) - Runtime, page-session, adoption, freshness, and human-review boundaries for migration readiness. - [Visual QA](visual-qa.md) - Rendered evidence references and limits. - [Accessibility](accessibility.md) - Accessibility evidence references and limits. - [HCI evaluation](hci-evaluation.md) - Heuristic, walkthrough, and user-evidence references. diff --git a/quality/gates/consumer-migration-evidence.md b/quality/gates/consumer-migration-evidence.md new file mode 100644 index 0000000..780ec2e --- /dev/null +++ b/quality/gates/consumer-migration-evidence.md @@ -0,0 +1,53 @@ +--- +type: Quality Gate +title: Consumer Migration Evidence Gate +description: Blocking contract for consumer-owned migration conformance and page evidence. +--- + +# Consumer Migration Evidence Gate + +Use this gate only for a declared consumer migration. Ordinary implementation handoffs keep the existing consumer-reference applicability contract and do not need a migration record. + +## Required Contract + +Record: + +```txt +Consumer migration conformance: declared +Consumer migration conformance record: path/to/consumer-conformance.json +Consumer revision: +Relevant source inventory: +Thirteen migration-dimension classifications: +Runtime scenario results: +Adoption mappings: +Page evidence: applicable | not_applicable +Deviations: +Debt and review dates: +Claim boundary: +Decision: pass | block +``` + +The record path is normalized, repository-relative JSON inside the consuming repository. The record follows the [consumer conformance schema](../../consumer-reference/schema/consumer-conformance-record.schema.json) and the [receiver contract](../../consumer-reference/contract.md). + +## Blocking Conditions + +Block when any of the following is true: + +- any of the thirteen migration dimensions is absent rather than explicitly `applicable` or `not_applicable`; +- an applicable dimension has no declared runtime scenario, or a scenario has a nonzero exit, missing result artifact, mismatched run/session identity, or stale source digest; +- an adoption mapping lacks a pinned StyleGallery revision and anchor, concrete local target, local decision, scenario link, or declared deviations and debt; +- page evidence is applicable but its source inventory, revision, browser scenario catalog, repository, run, session, artifact hashes, or finalized manifest does not match the conformance record; +- a source changes between session creation and finalization, an artifact escapes the evidence root, a symlink or unmanifested artifact is accepted, or evidence is reused across sessions; +- expired blocking evidence or malformed freshness metadata is presented as current; +- the handoff implies that passing validator or browser checks proves complete accessibility, product correctness, usability, or approval. + +## Evidence Decision + +Pass means only that the declared migration contract and its named evidence agree at the pinned consumer and StyleGallery revisions. It does not mean the migration is universally correct or that omitted behavior is safe. Reviewers still decide whether the inventory and `not_applicable` reasons are credible. + +Use [Consumer Migration Evidence](../evidence/consumer-migration.md) to interpret runtime, browser, freshness, and review channels. Use [Executable Evidence](../evidence/executable-evidence.md) for the repository-wide distinction between structural enforcement and human judgment. + +## IA Navigation + +Parent: [Gate Contracts](index.md). +Next: [Consumer Migration Evidence](../evidence/consumer-migration.md). diff --git a/quality/gates/harmony-evaluation.md b/quality/gates/harmony-evaluation.md index 509f0e5..a87413f 100644 --- a/quality/gates/harmony-evaluation.md +++ b/quality/gates/harmony-evaluation.md @@ -70,6 +70,8 @@ This gate decides whether a webpage composition is coherent enough to guide impl Use a [structured claim record](../claims.md) when the harmony decision approves, blocks, or redirects implementation. Harmony records must include the use case and implementation handoff because they decide what the implementer may safely carry from a visual reference into product-level styling. +If the work is an existing-consumer migration that declares conformance, route the separate migration evidence decision through the [Consumer Migration Evidence Gate](consumer-migration-evidence.md). Harmony cannot substitute for runtime parity or source-bound page evidence. + ## IA Navigation Parent: [Gate Contracts](index.md). diff --git a/quality/gates/index.md b/quality/gates/index.md index 1bc0373..f6d9459 100644 --- a/quality/gates/index.md +++ b/quality/gates/index.md @@ -10,3 +10,4 @@ Gate contracts define what kind of evidence can support a claim. - [Accessibility evidence gate](accessibility-evidence.md) - Accessibility evidence and review boundaries. - [Rationale gate](rationale.md) - Decision rationale, alternatives, warrants, and debt. - [Harmony evaluation gate](harmony-evaluation.md) - Contract for judging content-to-layout fit, overall harmony, GPT Image references, and implementation handoff. +- [Consumer migration evidence gate](consumer-migration-evidence.md) - Blocking contract for consumer-owned migration conformance, runtime proof, and source-bound page evidence. diff --git a/quality/index.md b/quality/index.md index c5dd82f..d03795f 100644 --- a/quality/index.md +++ b/quality/index.md @@ -16,6 +16,8 @@ The [Consumer Reference Receiver Contract](../consumer-reference/contract.md) is - [Claim records](claim-records/index.md) - Filled examples for high-impact claims. - [Accessibility evidence register](evidence/accessibility.md) - Classification for accessibility claims: automated, manual, user, or debt. - [Executable evidence coverage](evidence/executable-evidence.md) - Validator, fixture, CI, rendered-evidence, and review boundaries. +- [Consumer migration evidence gate](gates/consumer-migration-evidence.md) - Blocking contract for a declared consumer migration. +- [Consumer migration evidence](evidence/consumer-migration.md) - Claim boundaries for runtime and source-bound page evidence. ## Admission Model @@ -47,5 +49,6 @@ Use this script for lightweight tree tests: | Review product-level interface craft. | [README](../README.md) | [Interface Craft Decisions](../design-engineering/interface-craft.md) | The first selected route is Design Engineering and shared quality gates remain visible. | | Compare an Apple interaction convention. | [README](../README.md) | [Apple Interaction](../platform-guides/apple-interaction.md) | The first selected route is Platform Guides and source/version limits are reached within two hops. | | Declare whether a consumer reference applies. | [README](../README.md) | [Consumer Reference](../consumer-reference/index.md) | The handoff reaches the shared contract without classifying it as a fifth domain. | +| Prove a consumer migration preserves declared behavior. | [README](../README.md) | [Consumer Migration Readiness](../design-engineering/consumer-migration-readiness.md) | The reader reaches the Design Engineering method and its evidence gate within two hops. | Record `PASS` only when the expected primary route is the first route selected. A link resolving successfully is not enough; the selected route must match the task intent. diff --git a/scripts/test-validate-consumer-conformance.mjs b/scripts/test-validate-consumer-conformance.mjs index 2217ced..c8d5b29 100644 --- a/scripts/test-validate-consumer-conformance.mjs +++ b/scripts/test-validate-consumer-conformance.mjs @@ -43,6 +43,7 @@ const runtimeCases = Object.freeze([ ["forged-noop-result", "runtime_command_result_missing"], ["github-repository-spoof", "consumer_repository_mismatch", { GITHUB_REPOSITORY: "example/spoof" }], ]); +const documentedLifecycleCase = "documented-page-evidence-lifecycle"; function parseArguments(argv) { const options = { caseName: undefined, json: false }; @@ -199,6 +200,23 @@ function mutatedRuntimeRecord(name, source, fixture) { return value; } +function documentedLifecycleIsExecutable() { + const document = fs.readFileSync(path.join(repositoryRoot, "design-engineering", "consumer-migration-readiness.md"), "utf8"); + const start = document.indexOf("## Verification Contract"); + const end = document.indexOf("## Source, License, And Attribution"); + const verification = document.slice(start, end); + return [ + "create-page-evidence-session.mjs", + "finalize-page-evidence.mjs", + "validate-consumer-conformance.mjs", + "--root ", + "--record ", + "--artifact-root ", + "--runner-result ", + "--review-by ", + ].every((fragment) => verification.includes(fragment)); +} + function mutatedRecord(name, source) { const value = structuredClone(source); if (name === "omitted-dimension") delete value.migration_dimensions.exact_time_boundary; @@ -375,10 +393,15 @@ try { const ok = child.status !== 0 && codes.includes("runtime_command_result_missing") && child.output.checkedRuntimeCommands === 0; results.push({ actual: { checkedRuntimeCommands: child.output.checkedRuntimeCommands, codes, status: child.status }, expected: "oversized command result rejected before read", name: "oversized-command-result", ok }); } + if (selected === "all" || selected === documentedLifecycleCase) { + const actual = documentedLifecycleIsExecutable(); + results.push({ actual, expected: true, name: documentedLifecycleCase, ok: actual }); + } const knownCase = selected === "all" || selected === "valid-migration" || selected === "valid-runtime-proof" || selected === "stale-template-rebound" + || selected === documentedLifecycleCase || selected === "child-process-denied-cleanup" || selected === "timed-out-command-cleanup" || selected === "permission-override-denied" diff --git a/scripts/test-validate-consumer-reference.mjs b/scripts/test-validate-consumer-reference.mjs index 5391377..639b680 100644 --- a/scripts/test-validate-consumer-reference.mjs +++ b/scripts/test-validate-consumer-reference.mjs @@ -4,12 +4,17 @@ import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { + cleanupCompletedConsumer, + initializeCompletedConsumer, +} from "../consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs"; import { makeConsumerReferenceCaseRunner } from "./consumer-reference-case-runner.mjs"; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validator = path.join(repositoryRoot, "scripts", "validate-consumer-reference.mjs"); const itemPath = "consumer-reference/fixtures/item.json"; const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "item.schema.json"), "utf8")); +const conformanceTemplate = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "fixtures", "consumer-conformance", "valid-migration.json"), "utf8")); const baseItem = { artifact_mode: "schema_only", @@ -84,6 +89,69 @@ const behaviorCases = [ { expect: "handoff_consumer_reference_required", extraFiles: { "quality/claim.md": "Implementation handoff:\nBoundary: none.\n" }, name: "repository_handoff_omission" }, ]; +function writeReceiverFile(root, relative, content) { + const file = path.join(root, relative); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); +} + +function receiverHandoff(recordLine, declarationLine = "Consumer migration conformance: declared") { + return [ + "Implementation handoff:", + "Consumer reference: not_applicable", + "Consumer reference reason: This fixture has no consumer-specific reference record.", + declarationLine, + recordLine, + "", + ].filter(Boolean).join("\n"); +} + +function runMigrationReceiverCase(testCase) { + const fixture = initializeCompletedConsumer(conformanceTemplate); + try { + writeReceiverFile(fixture.root, itemPath, `${JSON.stringify(baseItem, null, 2)}\n`); + writeReceiverFile(fixture.root, "CATALOG.md", "# Catalog\n"); + writeReceiverFile(fixture.root, "layout/index.md", "# Layout\n"); + writeReceiverFile(fixture.root, "patterns/index.md", "# Patterns\n"); + writeReceiverFile(fixture.root, "scripts/pattern-data.mjs", "export const patterns = [];\n"); + const handoff = testCase.handoff ?? receiverHandoff(`Consumer migration conformance record: ${fixture.recordReference}`); + writeReceiverFile(fixture.root, "quality/handoff.md", handoff); + testCase.mutate?.(fixture); + const child = spawnSync(process.execPath, [validator, "--item", itemPath, "--json"], { cwd: fixture.root, encoding: "utf8" }); + const output = JSON.parse(child.stdout); + const codes = output.failures?.map((failure) => failure.code) ?? []; + const ok = testCase.expect === null + ? child.status === 0 && output.ok === true + : testCase.expect === "checkedMigrationRecords:1" + ? child.status === 0 && output.ok === true && output.checkedMigrationRecords === 1 + : child.status !== 0 && output.ok === false && codes.includes(testCase.expect); + return { actual: { checkedMigrationRecords: output.checkedMigrationRecords, codes, status: child.status }, expected: testCase.expect ?? "ok:true and exit:0", name: testCase.name, ok, rules: [] }; + } finally { + cleanupCompletedConsumer(fixture); + } +} + +const migrationReceiverCases = [ + { expect: "checkedMigrationRecords:1", name: "migration_declared_record_executes" }, + { + expect: "consumer_repository_mismatch", + mutate: (fixture) => { + const file = path.join(fixture.root, fixture.recordReference); + const record = JSON.parse(fs.readFileSync(file, "utf8")); + record.consumer.repository = "fabricated/consumer"; + fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`); + }, + name: "migration_invalid_record_blocks_handoff", + }, + { expect: "migration_conformance_record_unresolved", handoff: receiverHandoff("Consumer migration conformance record: records/missing.json"), name: "migration_missing_record_blocks_handoff" }, + { expect: "migration_conformance_declaration_required", handoff: receiverHandoff("Consumer migration conformance record: records/migration.json", ""), name: "migration_orphan_record_blocks_handoff" }, + { + expect: null, + handoff: ["Implementation handoff:", "Consumer reference: not_applicable", "Consumer reference reason: This ordinary handoff has no consumer-specific reference record.", ""].join("\n"), + name: "ordinary_not_applicable_skips_migration_receiver", + }, +]; + const requiredParityCases = schema.required.map((field) => ({ expect: field === "handoff" ? "handoff_required" : "item_field_required", mutate: (item) => { delete item[field]; }, @@ -162,7 +230,7 @@ const expectedSchemaRuleNames = [ ]; const runCase = makeConsumerReferenceCaseRunner({ baseItem, schema }); -const results = cases.map(runCase); +const results = [...cases.map(runCase), ...migrationReceiverCases.map(runMigrationReceiverCase)]; const failures = results .filter((result) => !result.ok) .map((result) => `missing_semantic:${result.name}:${result.expected}`); diff --git a/scripts/test-validate-domains.mjs b/scripts/test-validate-domains.mjs index 1269a44..1d63fa8 100644 --- a/scripts/test-validate-domains.mjs +++ b/scripts/test-validate-domains.mjs @@ -133,7 +133,7 @@ const baseFiles = { "| --- | --- | --- |", "| Layout | `layout/index.md` | Existing Layout corpus. |", "| Motion | `motion/index.md` | `motion/vocabulary.md`, `motion/review-workflow.md`, `motion/practice-reference.md` |", - "| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md`, `design-engineering/reference-profiles/index.md`, `design-engineering/reference-profiles/governed-local/index.md`, `design-engineering/reference-profiles/external-adaptation/index.md` |", + "| Design Engineering | `design-engineering/index.md` | `design-engineering/interface-craft.md`, `design-engineering/consumer-migration-readiness.md`, `design-engineering/reference-profiles/index.md`, `design-engineering/reference-profiles/governed-local/index.md`, `design-engineering/reference-profiles/external-adaptation/index.md` |", "| Platform Guides | `platform-guides/index.md` | `platform-guides/apple-interaction.md` |", "", `Source snapshot \`${revision}\`.`, @@ -157,8 +157,9 @@ const baseFiles = { "motion/vocabulary.md": leafPage({ title: "Motion Vocabulary", domain: "motion", sourcePath: "skills/animation-vocabulary/SKILL.md", parent: "index.md", next: "review-workflow.md" }), "motion/review-workflow.md": leafPage({ title: "Motion Review Workflow", domain: "motion", sourcePath: "skills/review-animations/SKILL.md", parent: "index.md", next: "practice-reference.md" }), "motion/practice-reference.md": leafPage({ title: "Motion Practice Reference", domain: "motion", sourcePath: "skills/review-animations/STANDARDS.md", parent: "index.md", next: "../design-engineering/index.md" }), - "design-engineering/index.md": indexPage("Design Engineering", [["Interface Craft", "interface-craft.md"], ["Reference Profiles", "reference-profiles/index.md"]]), + "design-engineering/index.md": indexPage("Design Engineering", [["Interface Craft", "interface-craft.md"], ["Consumer Migration Readiness", "consumer-migration-readiness.md"], ["Reference Profiles", "reference-profiles/index.md"]]), "design-engineering/interface-craft.md": leafPage({ title: "Interface Craft", domain: "design-engineering", sourcePath: "skills/emil-design-eng/SKILL.md", parent: "index.md", next: "../platform-guides/index.md" }), + [localLeafPath]: localLeaf, "design-engineering/reference-profiles/index.md": "# Reference Profiles\n\nDomain classification: design-engineering.\n\n- [Governed Local Profiles](governed-local/index.md)\n\nParent: [Design Engineering](../index.md).\nNext: [External adaptation](external-adaptation/index.md).\n", "design-engineering/reference-profiles/governed-local/index.md": "# Governed Local Profiles\n\nParent: [Reference Profiles](../index.md).\nNext: [External adaptation](../external-adaptation/index.md).\n", "design-engineering/reference-profiles/external-adaptation/index.md": "# External Adaptation\n\nSynthetic validator coverage only; no durable adopter record.\n\nParent: [Reference Profiles](../index.md).\nNext: [Platform Guides](../../../platform-guides/index.md).\n", @@ -226,14 +227,6 @@ function writeFixture(testCase) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), `stylegallery-domains-${testCase.name}-`)); const omitted = new Set(testCase.omit ?? []); const entries = { ...baseFiles }; - if (testCase.registry) { - entries[localLeafPath] = localLeaf; - entries["DOMAINS.md"] = entries["DOMAINS.md"].replace( - "`design-engineering/interface-craft.md`, `design-engineering/reference-profiles/index.md`", - "`design-engineering/interface-craft.md`, `design-engineering/consumer-migration-readiness.md`, `design-engineering/reference-profiles/index.md`", - ); - entries["design-engineering/index.md"] = indexPage("Design Engineering", [["Interface Craft", "interface-craft.md"], ["Consumer Migration Readiness", "consumer-migration-readiness.md"], ["Reference Profiles", "reference-profiles/index.md"]]); - } const mutations = testCase.mutations ?? (testCase.mutate ? [testCase.mutate] : []); for (const [relative, before, after] of mutations) entries[relative] = entries[relative].replace(before, after); if (testCase.add) { diff --git a/scripts/test-validate-ia.mjs b/scripts/test-validate-ia.mjs index 3a283cf..f9c6449 100644 --- a/scripts/test-validate-ia.mjs +++ b/scripts/test-validate-ia.mjs @@ -35,6 +35,7 @@ const baseFiles = { "| [Design Engineering](design-engineering/index.md) | Design Engineering domain |", "| [Platform Guides](platform-guides/index.md) | Platform Guides domain |", "| [Consumer Reference](consumer-reference/index.md) | Shared infrastructure contract |", + "| [Consumer Migration Readiness](design-engineering/consumer-migration-readiness.md) | Migration method |", "", "## Task Routes", "", @@ -68,6 +69,7 @@ const baseFiles = { "- [Design Engineering](design-engineering/index.md)", "- [Platform Guides](platform-guides/index.md)", "- [Consumer reference](consumer-reference/index.md)", + "- [Consumer migration readiness](design-engineering/consumer-migration-readiness.md)", "", ].join("\n"), "GUIDE.md": "# Guide\n\nPrimary role: planning workflow.\n", @@ -77,15 +79,18 @@ const baseFiles = { "patterns/stacking/stack.md": leaf("Stack", "index.md", "../../recipes/index.md"), "recipes/index.md": "# Recipes\n", "recipes/homepage.md": leaf("Homepage", "index.md", "../quality/index.md"), - "quality/index.md": "# Quality\n\n## Tree-Test Findability QA\n", - "quality/gates/index.md": "# Gates\n", + "quality/index.md": "# Quality\n\n- [Consumer migration evidence gate](gates/consumer-migration-evidence.md)\n\n## Tree-Test Findability QA\n", + "quality/gates/index.md": "# Gates\n\n- [Consumer migration evidence gate](consumer-migration-evidence.md)\n", "quality/gates/layout.md": leaf("Layout", "index.md", "../evidence/index.md"), - "quality/evidence/index.md": "# Evidence\n", + "quality/gates/consumer-migration-evidence.md": leaf("Consumer Migration Evidence Gate", "index.md", "../evidence/consumer-migration.md"), + "quality/evidence/index.md": "# Evidence\n\n- [Consumer migration evidence](consumer-migration.md)\n", + "quality/evidence/consumer-migration.md": leaf("Consumer Migration Evidence", "index.md", "../../design-engineering/consumer-migration-readiness.md"), "layout/index.md": "# Layout\n\n- [Catalog](../CATALOG.md)\n", "motion/index.md": "# Motion\n\n- [Review](review.md)\n", "motion/review.md": leaf("Motion review", "index.md", "../design-engineering/index.md"), - "design-engineering/index.md": "# Design Engineering\n\n- [Craft](craft.md)\n- [Reference Profiles](reference-profiles/index.md)\n", + "design-engineering/index.md": "# Design Engineering\n\n- [Craft](craft.md)\n- [Consumer Migration Readiness](consumer-migration-readiness.md)\n- [Reference Profiles](reference-profiles/index.md)\n", "design-engineering/craft.md": leaf("Craft", "index.md", "../platform-guides/index.md"), + "design-engineering/consumer-migration-readiness.md": leaf("Consumer Migration Readiness", "index.md", "../quality/gates/consumer-migration-evidence.md"), "design-engineering/reference-profiles/index.md": "# Reference Profiles\n\n- [Governed Local Profiles](governed-local/index.md)\n- [External Adaptation](external-adaptation/index.md)\n\nParent: [Design Engineering](../index.md).\nNext: [Governed Local Profiles](governed-local/index.md).\n", "design-engineering/reference-profiles/governed-local/index.md": [ "# Governed Local Profiles", @@ -99,7 +104,7 @@ const baseFiles = { "design-engineering/reference-profiles/external-adaptation/index.md": "# External Adaptation\n\nParent: [Reference Profiles](../index.md).\nNext: [Platform Guides](../../../platform-guides/index.md).\n", "platform-guides/index.md": "# Platform Guides\n\n- [Apple](apple.md)\n", "platform-guides/apple.md": leaf("Apple", "index.md", "../layout/index.md"), - "consumer-reference/index.md": "# Consumer Reference\n\n- [Contract](contract.md)\n", + "consumer-reference/index.md": "# Consumer Reference\n\n- [Contract](contract.md)\n- [Consumer Migration Readiness](../design-engineering/consumer-migration-readiness.md)\n", "consumer-reference/contract.md": leaf("Consumer Reference Contract", "index.md", "../quality/index.md"), }; @@ -181,6 +186,20 @@ const cases = [ }, expect: "index.md: missing [Consumer reference](consumer-reference/index.md)", }, + { + name: "missing_consumer_migration_root_route", + mutate: { + "README.md": baseFiles["README.md"].replace("| [Consumer Migration Readiness](design-engineering/consumer-migration-readiness.md) | Migration method |\n", ""), + }, + expect: "README.md: missing [Consumer Migration Readiness](design-engineering/consumer-migration-readiness.md)", + }, + { + name: "missing_consumer_migration_gate_route", + mutate: { + "quality/index.md": "# Quality\n\n## Tree-Test Findability QA\n", + }, + expect: "quality/index.md: missing [Consumer migration evidence gate](gates/consumer-migration-evidence.md)", + }, { name: "missing_domain_leaf_parent", mutate: { diff --git a/scripts/test-validate-webpage-workflow.mjs b/scripts/test-validate-webpage-workflow.mjs index 1d34f17..f76fb4f 100644 --- a/scripts/test-validate-webpage-workflow.mjs +++ b/scripts/test-validate-webpage-workflow.mjs @@ -130,6 +130,42 @@ const files = { "Final implementation proof:", "", ].join("\n"), + "design-engineering/consumer-migration-readiness.md": [ + "---", + "lifecycle: experimental", + "provenance_kind: local", + "---", + "# Consumer Migration Readiness", + "## Migration Dimension Contract", + "`behavior_inventory`", + "`route_parity`", + "`field_parity`", + "`action_parity`", + "`state_transitions`", + "`contract_precedence`", + "`direct_mutation`", + "`indirect_mutation`", + "`persistence_round_trip`", + "`reset_boundary`", + "`exact_time_boundary`", + "`defaults_tri_state_mapping`", + "`atomic_batch_behavior`", + "Consumer migration conformance: declared", + "Passing this method does not prove complete accessibility, product correctness, usability, visual quality, independent adoption, cross-browser equivalence, or owner approval.", + ].join("\n"), + "consumer-reference/contract.md": [ + "### Migration-Only Extension", + "This field is migration-only.", + "Do not add it to generic blank handoffs", + "Consumer migration conformance: declared", + ].join("\n"), + "quality/gates/consumer-migration-evidence.md": [ + "# Consumer Migration Evidence Gate", + "## Required Contract", + "## Blocking Conditions", + "thirteen migration dimensions", + "Decision: pass | block", + ].join("\n"), }; const cases = [ @@ -186,6 +222,20 @@ const cases = [ }, expect: "guides/webpage-generation-workflow.md: missing Consumer reference reason:", }, + { + name: "missing_migration_dimension", + mutate: { + "design-engineering/consumer-migration-readiness.md": files["design-engineering/consumer-migration-readiness.md"].replace("`atomic_batch_behavior`", "`batch_behavior`"), + }, + expect: "design-engineering/consumer-migration-readiness.md: missing `atomic_batch_behavior`", + }, + { + name: "migration_field_in_ordinary_handoff", + mutate: { + "guides/webpage-generation-workflow.md": `${files["guides/webpage-generation-workflow.md"]}\nConsumer migration conformance: declared\n`, + }, + expect: "guides/webpage-generation-workflow.md: ordinary handoff must not declare consumer migration conformance", + }, { name: "paraphrased_navigation_label", mutate: { diff --git a/scripts/validate-consumer-reference.mjs b/scripts/validate-consumer-reference.mjs index d64cf1b..8813db1 100644 --- a/scripts/validate-consumer-reference.mjs +++ b/scripts/validate-consumer-reference.mjs @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; +import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { isPlainObject, validateItemSchema } from "./consumer-reference-schema.mjs"; @@ -10,6 +11,7 @@ import { validateReferenceProfile, validateReferenceProfileSet } from "./referen const root = process.cwd(); const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const conformanceValidator = path.join(repositoryRoot, "scripts", "validate-consumer-conformance.mjs"); const schema = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "consumer-reference", "schema", "item.schema.json"), "utf8")); const validateFullItemSchema = new Ajv2020({ allErrors: true, strict: false }).compile(schema); const failures = []; @@ -22,6 +24,7 @@ const defaultItems = [ ]; const reverseMarkers = ["consumerreference", "designengineeringreferenceprofiles", "profilejson", "tokensdtcgjson", "relatedfixturesetid"]; const profileSummaries = []; +let checkedMigrationRecords = 0; function addFailure(code, relative, message) { failures.push({ code, message, path: relative }); @@ -73,65 +76,91 @@ function parseArguments() { return { items: requiresCanonicalPair ? [...new Set([...canonicalized, ...canonicalProfilePaths])] : canonicalized, json }; } -function validateRecordPath(record, itemPath) { +function validateRecordPath(record, itemPath, prefix = "record") { + const reject = (suffix, message) => addFailure(`${prefix}_${suffix}`, itemPath, message); if (typeof record !== "string" || record.length === 0) { - addFailure("record_required", itemPath, "declared handoff requires record"); - return; + reject("required", "declared handoff requires record"); + return undefined; } if (record.startsWith("//") || record.startsWith("\\\\")) { - addFailure("record_network_path", itemPath, "record must not use a network path"); - return; + reject("network_path", "record must not use a network path"); + return undefined; } if (path.posix.isAbsolute(record) || path.win32.isAbsolute(record)) { - addFailure("record_absolute", itemPath, "record must be repository-relative"); - return; + reject("absolute", "record must be repository-relative"); + return undefined; } if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(record)) { - addFailure("record_uri_scheme", itemPath, "record must not use a URI scheme"); - return; + reject("uri_scheme", "record must not use a URI scheme"); + return undefined; } if (record.split(/[\\/]/).includes("..")) { - addFailure("record_parent_segment", itemPath, "record must not contain a parent segment"); - return; + reject("parent_segment", "record must not contain a parent segment"); + return undefined; } if (record.includes("\\") || record.includes("?") || record.includes("#") || path.posix.normalize(record) !== record || record.startsWith("./")) { - addFailure("record_not_normalized", itemPath, "record must be a normalized POSIX repository path"); - return; + reject("not_normalized", "record must be a normalized POSIX repository path"); + return undefined; } if (!record.endsWith(".json")) { - addFailure("record_not_json", itemPath, "record must target a JSON file"); - return; + reject("not_json", "record must target a JSON file"); + return undefined; } const target = path.resolve(root, record); if (!isInside(root, target)) { - addFailure("record_escape", itemPath, "record resolves outside the repository root"); - return; + reject("escape", "record resolves outside the repository root"); + return undefined; } if (!fs.existsSync(target) || !fs.statSync(target).isFile()) { - addFailure("record_unresolved", itemPath, `record does not resolve: ${record}`); - return; + reject("unresolved", `record does not resolve: ${record}`); + return undefined; } const realRoot = fs.realpathSync(root); const realTarget = fs.realpathSync(target); if (!isInside(realRoot, realTarget)) { - addFailure("record_symlink_escape", itemPath, "record resolves through a symlink outside the repository root"); - return; + reject("symlink_escape", "record resolves through a symlink outside the repository root"); + return undefined; } if (realTarget !== path.join(realRoot, record)) { - addFailure("record_redirect", itemPath, "record must not resolve through a filesystem redirect"); - return; + reject("redirect", "record must not resolve through a filesystem redirect"); + return undefined; } try { - JSON.parse(fs.readFileSync(realTarget, "utf8")); + return { record, value: JSON.parse(fs.readFileSync(realTarget, "utf8")) }; } catch (error) { if (error instanceof SyntaxError) { - addFailure("record_invalid_json", itemPath, `record is not valid JSON: ${record}`); - return; + reject("invalid_json", `record is not valid JSON: ${record}`); + return undefined; } throw error; } } +function runMigrationConformance(packet, relative) { + const args = [conformanceValidator, "--root", root, "--record", packet.record]; + const manifest = packet.value?.page_evidence?.status === "applicable" ? packet.value.page_evidence.manifest : undefined; + if (typeof manifest === "string") args.push("--artifact-root", path.posix.dirname(manifest)); + args.push("--json"); + checkedMigrationRecords += 1; + const child = spawnSync(process.execPath, args, { cwd: repositoryRoot, encoding: "utf8" }); + let report; + try { report = JSON.parse(child.stdout); } + catch (error) { + addFailure("migration_conformance_validator_output_invalid", relative, error instanceof Error ? error.message : String(error)); + return; + } + const childFailures = Array.isArray(report.failures) ? report.failures : []; + for (const failure of childFailures) { + addFailure( + typeof failure?.code === "string" ? failure.code : "migration_conformance_validator_output_invalid", + typeof failure?.path === "string" ? failure.path : relative, + typeof failure?.message === "string" ? failure.message : "consumer conformance validator returned a malformed finding", + ); + } + if (child.status !== 0 && childFailures.length === 0) addFailure("migration_conformance_validator_failed", relative, "consumer conformance validator exited without a finding"); + if (child.status === 0 && report.ok !== true) addFailure("migration_conformance_validator_output_invalid", relative, "consumer conformance validator exited zero without ok:true"); +} + function validateHandoff(item, relative) { if (isPlainObject(item.handoff) && item.handoff.status === "declared") validateRecordPath(item.handoff.record, relative); } @@ -225,6 +254,20 @@ function validateHandoffCoverage() { } else if (!/Consumer reference record:\s*\S+\.json/i.test(window)) { addFailure("handoff_consumer_record_required", relative, `declared handoff at line ${index + 1} requires one JSON record`); } + const migrationDeclarations = [...window.matchAll(/^Consumer migration conformance:\s*(\S+)\s*$/gim)]; + const migrationRecords = [...window.matchAll(/^Consumer migration conformance record:\s*([^\n]+)\s*$/gim)]; + if (migrationDeclarations.length === 0 && migrationRecords.length > 0) { + addFailure("migration_conformance_declaration_required", relative, `migration record at line ${index + 1} requires an exact declared conformance field`); + } else if (migrationDeclarations.length > 0) { + if (migrationDeclarations.length !== 1 || migrationDeclarations[0][1].toLowerCase() !== "declared") { + addFailure("migration_conformance_status_invalid", relative, `migration handoff at line ${index + 1} accepts exactly one declared status`); + } else if (migrationRecords.length !== 1) { + addFailure("migration_conformance_record_required", relative, `declared migration handoff at line ${index + 1} requires exactly one JSON record`); + } else { + const packet = validateRecordPath(migrationRecords[0][1].trim(), relative, "migration_conformance_record"); + if (packet) runMigrationConformance(packet, relative); + } + } } } return checked; @@ -238,7 +281,7 @@ for (const finding of validateReferenceProfileSet(profileSummaries)) addFailure( const checkedHandoffs = validateHandoffCoverage(); const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()]; -const result = { checkedHandoffs, checkedItems: options.items.length, failures: uniqueFailures, ok: uniqueFailures.length === 0, profiles: profileSummaries, warnings }; +const result = { checkedHandoffs, checkedItems: options.items.length, checkedMigrationRecords, failures: uniqueFailures, ok: uniqueFailures.length === 0, profiles: profileSummaries, warnings }; if (options.json) console.log(JSON.stringify(result, null, 2)); else if (result.ok) console.log(`ok: ${result.checkedItems} consumer reference items`); else console.error(result.failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")); diff --git a/scripts/validate-domains.mjs b/scripts/validate-domains.mjs index b9eb3c3..b1face0 100644 --- a/scripts/validate-domains.mjs +++ b/scripts/validate-domains.mjs @@ -31,6 +31,7 @@ export const canonicalDomains = [ label: "Design Engineering", leaves: [ { path: "design-engineering/interface-craft.md", provenance: "external", sourcePath: "skills/emil-design-eng/SKILL.md" }, + { path: "design-engineering/consumer-migration-readiness.md", provenance: "local" }, ], referenceDocuments, }, diff --git a/scripts/validate-ia.mjs b/scripts/validate-ia.mjs index 62b9d51..64bf37a 100644 --- a/scripts/validate-ia.mjs +++ b/scripts/validate-ia.mjs @@ -37,6 +37,15 @@ const governedProfileLinks = [ "[Terminal keyboard matrix](terminal/generated/keyboard-matrix.md)", "[Terminal evidence coverage](terminal/generated/evidence-coverage.md)", ]; +const migrationRoutes = [ + ["README.md", "[Consumer Migration Readiness](design-engineering/consumer-migration-readiness.md)"], + ["index.md", "[Consumer migration readiness](design-engineering/consumer-migration-readiness.md)"], + ["design-engineering/index.md", "[Consumer Migration Readiness](consumer-migration-readiness.md)"], + ["consumer-reference/index.md", "[Consumer Migration Readiness](../design-engineering/consumer-migration-readiness.md)"], + ["quality/index.md", "[Consumer migration evidence gate](gates/consumer-migration-evidence.md)"], + ["quality/gates/index.md", "[Consumer migration evidence gate](consumer-migration-evidence.md)"], + ["quality/evidence/index.md", "[Consumer migration evidence](consumer-migration.md)"], +]; function read(relative) { const target = path.join(root, relative); @@ -81,6 +90,7 @@ function requireRootRoles() { for (const link of governedProfileLinks) { requireIncludes("design-engineering/reference-profiles/governed-local/index.md", link); } + for (const [relative, link] of migrationRoutes) requireIncludes(relative, link); } function requireTaskRoutes() { diff --git a/scripts/validate-webpage-workflow.mjs b/scripts/validate-webpage-workflow.mjs index f0e7778..1e48e04 100644 --- a/scripts/validate-webpage-workflow.mjs +++ b/scripts/validate-webpage-workflow.mjs @@ -79,6 +79,40 @@ const requiredIncludes = { "Source order, accessibility, brand correctness, and usability cannot be inferred from the image.", "Final implementation proof:", ], + "design-engineering/consumer-migration-readiness.md": [ + "lifecycle: experimental", + "provenance_kind: local", + "# Consumer Migration Readiness", + "## Migration Dimension Contract", + "`behavior_inventory`", + "`route_parity`", + "`field_parity`", + "`action_parity`", + "`state_transitions`", + "`contract_precedence`", + "`direct_mutation`", + "`indirect_mutation`", + "`persistence_round_trip`", + "`reset_boundary`", + "`exact_time_boundary`", + "`defaults_tri_state_mapping`", + "`atomic_batch_behavior`", + "Consumer migration conformance: declared", + "Passing this method does not prove complete accessibility, product correctness, usability, visual quality, independent adoption, cross-browser equivalence, or owner approval.", + ], + "consumer-reference/contract.md": [ + "### Migration-Only Extension", + "This field is migration-only.", + "Do not add it to generic blank handoffs", + "Consumer migration conformance: declared", + ], + "quality/gates/consumer-migration-evidence.md": [ + "# Consumer Migration Evidence Gate", + "## Required Contract", + "## Blocking Conditions", + "thirteen migration dimensions", + "Decision: pass | block", + ], }; const requiredLinks = { @@ -193,6 +227,12 @@ for (const route of workflowRoutes) { requireRouteOrder("guides/webpage-generation-workflow.md", route); } +for (const relative of ["guides/layout-brief.md", "guides/webpage-generation-workflow.md", "quality/gates/harmony-evaluation.md"]) { + if (/^Consumer migration conformance: declared$/m.test(read(relative))) { + failures.push(`${relative}: ordinary handoff must not declare consumer migration conformance`); + } +} + const result = { ok: failures.length === 0, checkedFiles: [...new Set([...Object.keys(requiredIncludes), ...Object.keys(requiredLinks), ...Object.keys(recommendedIncludes)])], From 60bfd96fb14ba5dd183404b4a3c27d1934cb99c1 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 23:39:33 +0900 Subject: [PATCH 44/44] ci(conformance): stage migration evidence gates Plan-Task: 8 Plan: .omo/plans/stylegallery-consumer-migration-conformance.md --- .github/workflows/evidence-freshness.yml | 62 ++++++ .github/workflows/validate.yml | 156 +++++++++++++ GOVERNANCE.md | 24 +- quality/evidence/executable-evidence.md | 1 + .../consumer-evidence-governance-contract.mjs | 210 ++++++++++++++++++ scripts/governance-test-fixture.mjs | 92 +++++++- scripts/test-validate-governance.mjs | 104 ++++++++- scripts/validate-governance.mjs | 13 +- 8 files changed, 650 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/evidence-freshness.yml create mode 100644 scripts/consumer-evidence-governance-contract.mjs diff --git a/.github/workflows/evidence-freshness.yml b/.github/workflows/evidence-freshness.yml new file mode 100644 index 0000000..9cba095 --- /dev/null +++ b/.github/workflows/evidence-freshness.yml @@ -0,0 +1,62 @@ +name: Audit explicit evidence deadlines + +on: + workflow_dispatch: + schedule: + - cron: '0 5 * * 1' + +permissions: + contents: read + +concurrency: + group: evidence-freshness-${{ github.ref }} + cancel-in-progress: true + +jobs: + evidence-freshness: + name: Advisory evidence freshness audit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - name: Install pinned validation dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Audit declared deadlines and emit annotations + shell: bash + run: | + as_of="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + set +e + node scripts/audit-evidence-freshness.mjs \ + --record consumer-reference/baselines/calibration.json \ + --as-of "$as_of" \ + --mode advisory \ + --json > evidence-freshness-report.json + audit_status=$? + set -e + AUDIT_STATUS="$audit_status" node -e ' + const fs = require("node:fs"); + const report = JSON.parse(fs.readFileSync("evidence-freshness-report.json", "utf8")); + for (const warning of report.warnings ?? []) { + console.log(`::warning file=${warning.source_path}::${warning.code}: ${warning.message}`); + } + const status = Number(process.env.AUDIT_STATUS); + if (status !== 0 || report.ok !== true) process.exit(1); + ' + + - name: Upload freshness report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: evidence-freshness-${{ github.run_id }} + path: evidence-freshness-report.json + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3530e5c..1557013 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -22,6 +22,7 @@ jobs: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: + fetch-depth: 0 persist-credentials: false - name: Setup Node @@ -47,6 +48,7 @@ jobs: node -c scripts/validate-vocabulary.mjs node -c scripts/test-validate-vocabulary.mjs node -c scripts/component-state-workflow-contract.mjs + node -c scripts/consumer-evidence-governance-contract.mjs node -c scripts/promotion-governance-contract.mjs node -c scripts/promotion-fixture-inventory.mjs node -c scripts/promotion-boundary-test-contract.mjs @@ -66,6 +68,23 @@ jobs: node -c scripts/consumer-reference-case-runner.mjs node -c scripts/validate-promotion-rfc.mjs node -c scripts/test-validate-promotion-rfc.mjs + node -c scripts/json-schema-formats.mjs + node -c scripts/consumer-conformance-contract.mjs + node -c scripts/validate-consumer-conformance.mjs + node -c scripts/test-validate-consumer-conformance.mjs + node -c consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs + node -c scripts/page-evidence-contract.mjs + node -c scripts/page-evidence-fixture.mjs + node -c scripts/create-page-evidence-session.mjs + node -c scripts/finalize-page-evidence.mjs + node -c scripts/validate-page-evidence.mjs + node -c scripts/test-validate-page-evidence.mjs + node -c scripts/audit-evidence-freshness.mjs + node -c scripts/test-audit-evidence-freshness.mjs + node -c tests/fixtures/consumer-conformance-scenarios.mjs + node -c tests/helpers/render-consumer-conformance.mjs + node -c tests/consumer-conformance.spec.mjs + node -c scripts/test-consumer-conformance-sentinel.mjs node -c consumer-reference/adapters/style-dictionary.config.mjs node -c scripts/reference-token-contract.mjs node -c scripts/reference-artifact-path-contract.mjs @@ -113,6 +132,9 @@ jobs: node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/evidence-record.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/runtime-evidence-manifest.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/promotion-rfc.schema.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/consumer-conformance-record.schema.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/page-evidence-session.schema.json"))' + node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/page-evidence-manifest.schema.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/policies/shared-experimental.json"))' node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/fixtures/promotion/manifest.json"))' git diff --check @@ -169,6 +191,13 @@ jobs: node scripts/validate-promotion-rfc.mjs --json node scripts/test-validate-promotion-rfc.mjs --json + - name: Consumer migration and page-evidence contracts + run: | + node scripts/test-validate-consumer-conformance.mjs --case valid-runtime-proof --json + node scripts/test-validate-consumer-conformance.mjs --json + node scripts/test-validate-page-evidence.mjs --json + node scripts/test-audit-evidence-freshness.mjs --json + - name: Portable token artifact integrity run: | node scripts/validate-reference-artifacts.mjs --manifest consumer-reference/generated/manifest.json --json @@ -201,6 +230,133 @@ jobs: - name: Pattern adversarial fixtures run: node scripts/test-validate-patterns.mjs --json + consumer-conformance: + name: Consumer conformance Chromium matrix + runs-on: ubuntu-24.04 + container: + image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a + options: --platform linux/amd64 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - name: Install exact conformance dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Run deterministic browser matrix + run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line + + - name: Run browser matrix semantic negative controls + run: node scripts/test-consumer-conformance-sentinel.mjs --json + + consumer-page-evidence: + name: Consumer page-evidence raster capture (nonblocking) + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + continue-on-error: true + env: + CONSUMER_CONFORMANCE_CAPTURE_DIR: .tmp/consumer-page-evidence + PAGE_EVIDENCE_ARTIFACT_ROOT: ${{ github.workspace }}/.tmp/consumer-page-evidence-consumer/evidence/session + PAGE_EVIDENCE_CASE_ID: state-w1024-focus + PAGE_EVIDENCE_CONSUMER_ROOT: ${{ github.workspace }}/.tmp/consumer-page-evidence-consumer + PAGE_EVIDENCE_PACKET_DIR: ${{ github.workspace }}/.tmp/consumer-page-evidence-packet + PAGE_EVIDENCE_RECORD: ${{ github.workspace }}/.tmp/consumer-page-evidence-consumer/records/consumer-conformance.json + PAGE_EVIDENCE_RUNNER_RESULT: runner/responsive-layout.json + PAGE_EVIDENCE_SCENARIO_ID: responsive-layout + container: + image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a + options: --platform linux/amd64 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Setup Node 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - name: Install exact capture dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Prepare synthetic consumer sources + run: >- + node scripts/run-consumer-page-evidence-ci.mjs prepare + --consumer-root "$PAGE_EVIDENCE_CONSUMER_ROOT" + --record "$PAGE_EVIDENCE_RECORD" + --artifact-root "$PAGE_EVIDENCE_ARTIFACT_ROOT" + + - name: Create source-bound page-evidence session + run: | + consumer_sha=$(git -C "$PAGE_EVIDENCE_CONSUMER_ROOT" rev-parse HEAD) + env -u GITHUB_REPOSITORY -u GITHUB_HEAD_REF -u GITHUB_REF_NAME \ + GITHUB_SHA="$consumer_sha" node scripts/create-page-evidence-session.mjs \ + --root "$PAGE_EVIDENCE_CONSUMER_ROOT" \ + --record "$PAGE_EVIDENCE_RECORD" \ + --artifact-root "$PAGE_EVIDENCE_ARTIFACT_ROOT" \ + --json + + - name: Verify synthetic page-evidence identity + run: >- + node scripts/run-consumer-page-evidence-ci.mjs verify-session + --consumer-root "$PAGE_EVIDENCE_CONSUMER_ROOT" + --artifact-root "$PAGE_EVIDENCE_ARTIFACT_ROOT" + + - name: Capture the selected consumer conformance raster + run: | + cd "$PAGE_EVIDENCE_CONSUMER_ROOT" + CONSUMER_CONFORMANCE_CAPTURE_DIR= npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line --grep "state-w1024-focus" + + - name: Finalize the page-evidence session + run: | + review_by=$(node -e 'process.stdout.write(new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString())') + env -u GITHUB_REPOSITORY -u GITHUB_HEAD_REF -u GITHUB_REF_NAME -u GITHUB_SHA \ + node scripts/finalize-page-evidence.mjs \ + --root "$PAGE_EVIDENCE_CONSUMER_ROOT" \ + --artifact-root "$PAGE_EVIDENCE_ARTIFACT_ROOT" \ + --runner-result "$PAGE_EVIDENCE_RUNNER_RESULT" \ + --review-by "$review_by" \ + --json + + - name: Validate the completed page-evidence packet + run: | + env -u GITHUB_REPOSITORY -u GITHUB_HEAD_REF -u GITHUB_REF_NAME -u GITHUB_SHA \ + node scripts/validate-page-evidence.mjs \ + --root "$PAGE_EVIDENCE_CONSUMER_ROOT" \ + --artifact-root "$PAGE_EVIDENCE_ARTIFACT_ROOT" \ + --json + + - name: Package only validated page evidence + run: >- + node scripts/run-consumer-page-evidence-ci.mjs package + --consumer-root "$PAGE_EVIDENCE_CONSUMER_ROOT" + --artifact-root "$PAGE_EVIDENCE_ARTIFACT_ROOT" + --output "$PAGE_EVIDENCE_PACKET_DIR" + + - name: Upload optional consumer page evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: consumer-page-evidence-${{ github.run_id }}-${{ github.sha }} + path: ${{ env.PAGE_EVIDENCE_PACKET_DIR }}/ + if-no-files-found: warn + retention-days: 14 + + - name: Clean up synthetic consumer + if: always() + run: >- + node scripts/run-consumer-page-evidence-ci.mjs cleanup + --consumer-root "$PAGE_EVIDENCE_CONSUMER_ROOT" + chromium-sentinel: name: Chromium reference sentinel (nonblocking) runs-on: ubuntu-24.04 diff --git a/GOVERNANCE.md b/GOVERNANCE.md index ee3d5dd..a9d801b 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -3,6 +3,7 @@ type: Governance Reference title: Governance, Lifecycle, And Docs-As-Code description: Source-of-truth, generated artifact, domain, lifecycle, ownership, and stale-content policy for StyleGallery. scheduled_stale_audit: deferred +scheduled_evidence_audit: active_advisory --- # Governance, Lifecycle, And Docs-As-Code @@ -25,6 +26,10 @@ Use this file before editing repository documentation. It names which file is au | Portable token source | `consumer-reference/fixtures/token-portability/valid-reference.json`, `consumer-reference/schema/portable-tokens.schema.json` | `scripts/build-reference-artifacts.mjs` with Style Dictionary `5.5.0` | `consumer-reference/generated/tokens.css`, `consumer-reference/generated/manifest.json` | `stable` restricted adapter contract, `generated` output | Allowed token shape, adapter/version pin, source token count, warning, declaration, or content hash changes. | `scripts/validate-reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | | Governed local reference profiles | `design-engineering/reference-profiles/governed-local/editorial/profile.json`, `design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/editorial/local-foundations.json`, `design-engineering/reference-profiles/governed-local/terminal/profile.json`, `design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/terminal/local-foundations.json` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Layout revision, identity values, UA/reset assumptions, explicit selection, or fixture relationship changes. | `scripts/validate-consumer-reference.mjs`, `scripts/test-validate-consumer-reference.mjs` | Design Engineering owner with Validation owner | | Component-state evidence matrices | Each profile's declared `components/*.component.json`, `states/*.states.json`, `fixtures/*.fixture.json`, and `evidence/*.evidence.json` records | `scripts/generate-consumer-reference-evidence.mjs` | `design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md` | `generated` output from `experimental` canonical records | Declared record paths, capture-session identity, scenario/mode/channel counts, claim boundary, generated escaping, or generator output changes. | `scripts/validate-component-state.mjs`, `scripts/test-validate-component-state.mjs`, `scripts/test-validate-component-state-artifacts.mjs`, `scripts/test-generate-consumer-reference-evidence.mjs` | Design Engineering owner with Validation owner | +| Consumer migration conformance | `consumer-reference/schema/consumer-conformance-record.schema.json`, `design-engineering/consumer-migration-readiness.md` | Manual | Consumer-owned conformance records | `experimental` method | Method, dimension, scenario, mapping, or ownership changes. | `scripts/validate-consumer-conformance.mjs`, `scripts/test-validate-consumer-conformance.mjs` | Design Engineering owner with Validation owner | +| Consumer page-evidence lifecycle | `consumer-reference/schema/page-evidence-session.schema.json`, `consumer-reference/schema/page-evidence-manifest.schema.json` | Browser/session tooling | Consumer-owned session, runner, manifest, and raster artifacts | `experimental` evidence protocol | Source, revision, session, run, scenario, review deadline, or artifact integrity changes. | `scripts/test-validate-page-evidence.mjs`, deterministic Chromium consumer-conformance job | Validation owner with consumer owner | +| Explicit evidence freshness schedule | Evidence records that declare `expires_at` or `review_by` | `.github/workflows/evidence-freshness.yml` | Advisory JSON audit artifact | `stable` narrow schedule | Explicit deadline, auditor, schedule, or inventory changes. | `scripts/audit-evidence-freshness.mjs`, `scripts/test-audit-evidence-freshness.mjs` | Validation owner | +| Deterministic consumer browser conformance | `tests/consumer-conformance.spec.mjs`, `tests/fixtures/consumer-conformance-scenarios.mjs`, `tests/helpers/render-consumer-conformance.mjs` | Playwright `1.61.0` in the digest-pinned `linux/amd64` image | Optional page-evidence raster artifact | `experimental` contract with blocking semantic gate and nonblocking raster capture | Viewport, container, content, state, overlay, page-scale, focus, overflow, contrast, semantics, or runtime-error changes. | Playwright matrix and `scripts/test-consumer-conformance-sentinel.mjs` | Validation owner with Design Engineering owner | | Shared promotion policy | `consumer-reference/policies/shared-experimental.json`, `consumer-reference/schema/promotion-rfc.schema.json` | Manual | None; promotion fixtures are synthetic examples | `stable` policy with `example_only`, deferred fixtures | Gateway scope, independence basis, evidence, ownership, support, compatibility, migration, deprecation, rollback, provenance, or decision changes. | `scripts/validate-promotion-rfc.mjs`, `scripts/test-validate-promotion-rfc.mjs` | Repository governance owner with actual consumer, support, migration, rollback, and Validation owners | | Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/{baseline-manifest,calibration-record}.schema.json`, `consumer-reference/baselines/*.json` | Playwright `1.61.0` in the digest-pinned `linux/amd64` image | `tests/snapshots/consumer-reference-card-grid.png`, raw GitHub Actions calibration artifact | `experimental`, nonblocking while owner approval is pending | Renderer source, exact image/platform/tool pin, baseline bytes, computed layout contract, or calibration metadata changes. | `scripts/validate-baseline-manifest.mjs`, `scripts/test-validate-baseline-manifest.mjs`, `scripts/test-summarize-sentinel-calibration.mjs`, Playwright | Repository governance owner with Validation owner | | Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner | @@ -35,7 +40,7 @@ Use this file before editing repository documentation. It names which file is au | Pattern data and examples | `scripts/pattern-data.mjs` | Manual data source | `patterns/**/*.md`, `patterns/**/index.md`, `patterns/index.md`, `CATALOG.md` | `generated` output from `stable` source | Source-lineage URL changes, generated drift, category changes, or pattern count changes. | `scripts/validate-patterns.mjs`, `scripts/validate-catalog.mjs`, `scripts/validate-governance.mjs` | Pattern-data owner | | Pattern generator | `scripts/generate-patterns.mjs` | Manual code source | `patterns/**/*.md`, `patterns/**/index.md`, `patterns/index.md`, `CATALOG.md` | `stable` generator, `generated` output | Generated structure changes, generated-warning changes, or generated metadata changes. | `node -c scripts/generate-patterns.mjs`, generated drift check, `scripts/validate-governance.mjs` | Pattern-data owner | | Validation scripts | `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs` | Manual code source | CI validation output | `stable` | Validator scope changes, fixture changes, or CI parity changes. | `node -c`, matching fixture tests, `.github/workflows/validate.yml` | Validation owner | -| CI workflow | `.github/workflows/validate.yml` | Manual | GitHub Actions run | `stable` | Validation step changes, generated drift policy changes, or owner changes. | GitHub Actions, `scripts/validate-governance.mjs` | Repository governance owner | +| CI workflows | `.github/workflows/validate.yml`, `.github/workflows/evidence-freshness.yml` | Manual | GitHub Actions runs and audit artifacts | `stable` | Validation step, explicit-deadline schedule, generated drift policy, or owner changes. | GitHub Actions, `scripts/validate-governance.mjs` | Repository governance owner | ## Generated Artifact Policy @@ -101,15 +106,16 @@ Consumer-reference ownership records the current truth as `owner.enforcement: "p | `guides/**`, `GUIDE.md`, `recipes/**` | Planning-doc owner | Planning flow, task routes, recipe composition boundaries. | | `quality/**` | Quality owner | Claim boundaries, executable evidence, review gates. | | `consumer-reference/**` | Repository governance owner with Validation owner | Required handoff, repository-local record safety, lifecycle separation, ownership truth, and reverse-dependency guard. | +| `consumer-reference/schema/{consumer-conformance-record,page-evidence-session,page-evidence-manifest}.schema.json`, `scripts/*consumer-conformance*.mjs`, `scripts/*page-evidence*.mjs` | Validation owner with Design Engineering owner | Closed migration dimensions, source/revision/session/run binding, artifact containment, executable scenario coverage, and consumer-owned adoption mapping. | | `consumer-reference/policies/**`, `consumer-reference/fixtures/promotion/**`, `consumer-reference/schema/promotion-rfc.schema.json`, `scripts/*promotion-rfc.mjs` | Repository governance owner with actual consumer, support, migration, rollback, and Validation owners | Invariant-only scope, independence truth, claim-scoped evidence, compatibility, support capacity, lifecycle duties, and zero promotion/adoption overclaim. | | `consumer-reference/adapters/**`, `consumer-reference/generated/**`, `scripts/*reference-artifacts.mjs`, `scripts/test-reference-adapters.mjs` | Repository governance owner with Validation owner | Restricted token ingress, exact adapter pin, warning handling, token/declaration counts, hashes, preserved aliases, and generated drift. | | `DOMAINS.md`, `layout/**` | Repository governance owner with Pattern-data owner | Domain routing and preservation of the stable Layout path contract. | | `motion/**` | Motion domain owner | Motion terminology, review procedure, practice classification, and evidence boundaries. | | `design-engineering/**` | Design Engineering domain owner | Separation of product heuristics from shared quality gates. | | `design-engineering/reference-profiles/**` | Design Engineering owner with Validation owner | Profile-local identity and values, pinned Layout provenance, explicit non-default selection, and related-fixture truth. | -| `tests/**`, `playwright.config.mjs`, `consumer-reference/baselines/**`, `scripts/*baseline*.mjs`, `scripts/*sentinel*.mjs`, `scripts/*renderer-purity.mjs` | Repository governance owner with Validation owner | Pure rendering, computed semantics before screenshots, immutable browser pins, nonblocking status, calibration cardinality, and pending owner approval. | +| `tests/**`, `playwright.config.mjs`, `consumer-reference/baselines/**`, `scripts/*baseline*.mjs`, `scripts/*sentinel*.mjs`, `scripts/*renderer-purity.mjs` | Repository governance owner with Validation owner | Pure rendering, computed semantics before screenshots, immutable browser pins, required consumer-conformance semantics, optional raster capture, calibration cardinality, and pending owner approval. | | `platform-guides/**` | Platform Guides domain owner | Platform/source/version limits, comparison boundaries, and stale review. | -| `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs`, `.github/workflows/validate.yml` | Validation owner | Validator scope, negative fixtures, CI parity. | +| `scripts/validate-*.mjs`, `scripts/test-validate-*.mjs`, `.github/workflows/validate.yml`, `.github/workflows/evidence-freshness.yml` | Validation owner | Validator scope, negative fixtures, blocking/advisory boundaries, CI parity. | ## Shared Promotion Governance @@ -123,15 +129,17 @@ If evidence fails, restrict, maintain, or roll back the candidate. A consumed st ## Staleness Control -Decision: no scheduled stale-content workflow yet. +Decision: run a weekly advisory audit only for evidence records that already declare `expires_at` or `review_by`. -Reason: the current repository is small, CI already checks local links and generated drift on pull requests and pushes, and a scheduled workflow would add notification noise without a clear owner rotation. +The independent `.github/workflows/evidence-freshness.yml` schedule runs Monday at 05:00 UTC and also supports manual dispatch. It currently inventories the committed Chromium calibration record; consumer-owned page-evidence manifests enter the same audit by passing their explicit record path to `scripts/audit-evidence-freshness.mjs`. + +No repository-wide maximum age or inferred time-to-live applies. Evidence without an explicit deadline remains outside this schedule. An expired `expires_at` or due `review_by` emits an advisory annotation and a JSON report without failing solely because the date passed. Malformed records and auditor failures remain blocking even though due-date findings are advisory. Audit trigger: - Run `node scripts/validate-links.mjs --json` and `node scripts/validate-governance.mjs --json` when external source lineage, generated policy, root navigation, or validation ownership changes. - Run `node scripts/validate-domains.mjs --json` and `node scripts/test-validate-domains.mjs` when domain membership, scope boundaries, source paths, source revisions, platform context, or promotion state changes. -- Reconsider a scheduled stale audit when source-lineage URLs exceed 50 entries, when an external-link failure escapes a pull request, or when ownership changes away from a single maintainer. +- Run `node scripts/test-audit-evidence-freshness.mjs --json` when `expires_at`, `review_by`, record inventory, advisory classification, or the scheduled workflow changes. ## Required Verification @@ -145,6 +153,10 @@ node scripts/validate-domains.mjs --json node scripts/test-validate-domains.mjs node scripts/validate-consumer-reference.mjs --json node scripts/test-validate-consumer-reference.mjs --json +node scripts/test-validate-consumer-conformance.mjs --case valid-runtime-proof --json +node scripts/test-validate-consumer-conformance.mjs --json +node scripts/test-validate-page-evidence.mjs --json +node scripts/test-audit-evidence-freshness.mjs --json ``` For generated pattern or catalog changes, also run: diff --git a/quality/evidence/executable-evidence.md b/quality/evidence/executable-evidence.md index 36f45be..c0d88a1 100644 --- a/quality/evidence/executable-evidence.md +++ b/quality/evidence/executable-evidence.md @@ -27,6 +27,7 @@ Executable evidence identifies which claims are machine-enforced, which are only | Webpage-generation workflow references remain present across guides, recipes, and gates. | `scripts/validate-webpage-workflow.mjs` and `scripts/test-validate-webpage-workflow.mjs` | `node scripts/validate-webpage-workflow.mjs --json`; `node scripts/test-validate-webpage-workflow.mjs --json` | The success fixture includes the required workflow references and contract snippets. | Missing route structure, required template fields, link targets, safety boundaries, or route order must fail. Preferred link labels and GUIDE wording emit warnings. | Proves required structure and safety boundaries are present, not that a generated webpage is visually harmonious. | | Consumer migration readiness remains discoverable and keeps migration-only handoff semantics. | `scripts/validate-ia.mjs`, `scripts/test-validate-ia.mjs`, `scripts/validate-webpage-workflow.mjs`, and `scripts/test-validate-webpage-workflow.mjs` | Run both validators and fixture harnesses. | Root, domain, consumer-reference, quality gate, evidence, workflow, and taxonomy routes reach the method within two hops; ordinary templates retain reasoned non-applicability while migration guidance declares a separate field. | Missing method routes, missing thirteen-dimension wording, missing migration-only boundary, or replacing the ordinary handoff declaration must fail. | Proves documentation structure and declared handoff semantics, not a consumer migration result. | | A consumer-owned migration record links thirteen explicit classifications, executable scenarios, adoption mappings, and applicable source-bound page evidence. | `scripts/validate-consumer-conformance.mjs`, `scripts/test-validate-consumer-conformance.mjs`, page-evidence lifecycle scripts, and their fixture harnesses. | Run the conformance and page-evidence contract suites; execute the browser matrix separately when page evidence applies. | Closed schemas, pinned revisions, safe paths, zero-exit scenario records, adoption mappings, and one finalized matching page session pass. | Missing dimensions, unknown scenarios, unsafe paths, source drift, failed runner, cross-run/session/revision reuse, missing/extra/tampered artifacts, and mismatched source or scenario sets fail with named findings. | Supports agreement at pinned revisions only; it does not certify accessibility, product correctness, usability, adoption, or approval. | +| Consumer browser semantics hold across the declared viewport, container, content, state, overlay, and Chromium page-scale matrix. | `tests/consumer-conformance.spec.mjs`, `scripts/test-consumer-conformance-sentinel.mjs`, and `scripts/run-consumer-page-evidence-ci.mjs` | The blocking `consumer-conformance` job runs 147 generated browser scenarios and all eight semantic/filesystem negative controls. The nonblocking `consumer-page-evidence` job creates a source-bound synthetic consumer session, verifies that its receipt uses the synthetic Git repository, branch, and revision instead of the parent Actions identity, captures one selected raster with `PAGE_EVIDENCE_*`, finalizes and validates it, then uploads only the completed packet. | Widths 320, 375, 768, 1024, and 1440; tight, medium, roomy, and full containers; empty, long-label, long-paragraph, CJK, and unbroken content; state, overlay, and Chromium page-scale probes exercise focus, overflow, contrast-ratio math, semantics, and runtime errors. | Overflow, clipped focus, low contrast, focus leakage, console-error mutations, or a leaked parent Actions identity fail on their named assertion; symlinked, pre-existing, and cleanup-redirected page-evidence paths fail without outside writes, deletion, or overwritten bytes. | The blocking job proves deterministic browser contracts in the pinned Chromium environment. The optional validated packet supports review but does not prove usability, accessibility, product suitability, or cross-browser behavior. | | Explicit evidence deadlines are audited weekly without inventing a repository-wide maximum age. | `scripts/audit-evidence-freshness.mjs`, `scripts/test-audit-evidence-freshness.mjs`, and `scripts/validate-governance.mjs` | `.github/workflows/evidence-freshness.yml` runs Monday at 05:00 UTC and by manual dispatch in advisory mode, emits annotations, and uploads the JSON report for 14 days. | Fresh, expiring, expired, review-due, and no-explicit-expiry records retain their declared semantics. | Malformed records and auditor failures remain blocking even though due-date findings are advisory. | The audit reports only explicit `expires_at` and `review_by` values; it does not infer a TTL, establish evidence quality, or revoke a claim automatically. | | Markdown links resolve inside the repository. | `scripts/validate-links.mjs` | `node scripts/validate-links.mjs --json` | All checked Markdown links resolve to local files or are intentionally external or anchors. | A local link that escapes the repository or points at a missing file must fail. | Proves link targets exist, not that the target content supports a claim. | | Catalog and generated pattern indexes match the pattern data source. | `scripts/validate-catalog.mjs` | `node scripts/validate-catalog.mjs --json` | `CATALOG.md`, category indexes, and expected pattern files match `scripts/pattern-data.mjs`. | Missing, unexpected, or unlisted pattern files must fail. | Proves catalog consistency, not pattern usefulness. | diff --git a/scripts/consumer-evidence-governance-contract.mjs b/scripts/consumer-evidence-governance-contract.mjs new file mode 100644 index 0000000..148f824 --- /dev/null +++ b/scripts/consumer-evidence-governance-contract.mjs @@ -0,0 +1,210 @@ +import { immutableActionPins } from "./governance-policy-contract.mjs"; +import { workflowActionFailures } from "./workflow-action-contract.mjs"; + +const validatePath = ".github/workflows/validate.yml"; +const freshnessPath = ".github/workflows/evidence-freshness.yml"; + +function workflowJob(workflow, id) { + const marker = ` ${id}:\n`; + const start = workflow.indexOf(marker); + if (start === -1) return ""; + const remainder = workflow.slice(start + marker.length); + const next = remainder.search(/^ [a-zA-Z0-9_-]+:/m); + return marker + (next === -1 ? remainder : remainder.slice(0, next)); +} + +function missing(relative, content, fragments) { + return fragments.flatMap(([fragment, label = fragment]) => content.includes(fragment) ? [] : [`${relative}: missing ${label}`]); +} + +function missingCommands(relative, content, fragments) { + const commands = content.split("\n").map((line) => line.trim()).filter(Boolean); + return fragments.flatMap(([fragment, label = fragment]) => commands.some((command) => { + let candidate = command; + const assignment = /^[A-Za-z_][A-Za-z0-9_]*=(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^\s]+)?\s+/; + if (candidate.startsWith("env ")) candidate = candidate.slice(4); + while (true) { + const withoutUnset = candidate.replace(/^-u\s+[A-Za-z_][A-Za-z0-9_]*\s+/, ""); + if (withoutUnset !== candidate) { + candidate = withoutUnset; + continue; + } + const withoutAssignment = candidate.replace(assignment, ""); + if (withoutAssignment === candidate) break; + candidate = withoutAssignment; + } + if (candidate !== fragment && !candidate.startsWith(`${fragment} `)) return false; + const suffix = candidate.slice(fragment.length); + return !/(?:&&|\|\||[;&|])/.test(suffix); + }) ? [] : [`${relative}: missing ${label}`]); +} + +function hasIf(lines, indentation) { + const pattern = new RegExp(`^ {${indentation}}(?:-\\s+)?["']?if["']?:\\s*`); + return lines.some((line) => pattern.test(line)); +} + +function hasLiteralDisabledIf(lines, indentation) { + const pattern = new RegExp(`^ {${indentation}}(?:-\\s+)?["']?if["']?:\\s*(?:false|['"]false['"]|\\$\\{\\{\\s*false\\s*\\}\\})(?:\\s+#.*)?$`, "i"); + return lines.some((line) => pattern.test(line)); +} + +function executableRunText(workflow) { + const lines = workflow.split("\n"); + const commands = []; + const stepsLine = lines.findIndex((line) => /^\s*steps:\s*$/.test(line)); + if (stepsLine === -1) return ""; + const stepsIndent = lines[stepsLine].match(/^\s*/)[0].length; + for (let index = stepsLine + 1; index < lines.length;) { + const start = lines[index].match(new RegExp(`^ {${stepsIndent + 2}}-\\s+`)); + if (!start) { + if (lines[index].trim() && lines[index].match(/^\s*/)[0].length <= stepsIndent) break; + index += 1; + continue; + } + let end = index + 1; + while (end < lines.length && !new RegExp(`^ {${stepsIndent + 2}}-\\s+`).test(lines[end]) && (!lines[end].trim() || lines[end].match(/^\s*/)[0].length > stepsIndent)) end += 1; + const step = lines.slice(index, end); + if (!hasIf(step, stepsIndent + 2) && !hasIf(step, stepsIndent + 4)) { + const runIndex = step.findIndex((line) => new RegExp(`^(?: {${stepsIndent + 2}}-\\s+| {${stepsIndent + 4}})run:`).test(line)); + if (runIndex !== -1) { + const match = step[runIndex].match(/^\s*(?:-\s+)?run:\s*(.*)$/); + const scalar = match[1].replace(/\s+#.*$/, "").trim(); + if (scalar && !["|", ">", "|-", ">-"].includes(scalar)) commands.push(scalar); + if (["|", ">", "|-", ">-"].includes(scalar)) { + const blockCommands = []; + let heredoc; + for (const raw of step.slice(runIndex + 1)) { + const command = raw.trim(); + if (!command || command.startsWith("#")) continue; + if (heredoc) { + if (command === heredoc) heredoc = undefined; + continue; + } + const opener = command.match(/<<-?\s*['"]?([A-Za-z0-9_]+)['"]?/); + blockCommands.push(command.replace(/\s+#.*$/, "").trim()); + if (opener) heredoc = opener[1]; + } + if (scalar.startsWith(">")) commands.push(blockCommands.join(" ")); + else commands.push(...blockCommands); + } + } + } + index = end; + } + const logicalCommands = []; + let logical = ""; + for (const command of commands) { + logical = logical ? `${logical} ${command}` : command; + if (logical.endsWith("\\")) { + logical = logical.slice(0, -1).trimEnd(); + continue; + } + logicalCommands.push(logical); + logical = ""; + } + if (logical) logicalCommands.push(logical); + return logicalCommands.join("\n"); +} + +export function consumerEvidenceGovernanceFailures({ evidence, freshnessWorkflow, governance, validationWorkflow }) { + const failures = []; + failures.push(...missing("GOVERNANCE.md", governance, [ + ["scheduled_stale_audit: deferred"], + ["scheduled_evidence_audit: active_advisory"], + ["Decision: run a weekly advisory audit only for evidence records that already declare `expires_at` or `review_by`."], + ["No repository-wide maximum age or inferred time-to-live applies."], + ["Malformed records and auditor failures remain blocking even though due-date findings are advisory."], + [freshnessPath], + ["Monday at 05:00 UTC"], + ])); + failures.push(...missing("quality/evidence/executable-evidence.md", evidence, [ + ["Explicit evidence deadlines are audited weekly without inventing a repository-wide maximum age."], + ["Malformed records and auditor failures remain blocking even though due-date findings are advisory."], + ])); + const validationCommands = executableRunText(workflowJob(validationWorkflow, "validate")); + failures.push(...missingCommands(`${validatePath}: validate`, validationCommands, [ + ["node -c scripts/consumer-evidence-governance-contract.mjs"], + ["node -c scripts/json-schema-formats.mjs"], + ["node -c scripts/consumer-conformance-contract.mjs"], + ["node -c scripts/validate-consumer-conformance.mjs"], + ["node -c scripts/test-validate-consumer-conformance.mjs"], + ["node -c consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs"], + ["node -c scripts/page-evidence-contract.mjs"], + ["node -c scripts/page-evidence-fixture.mjs"], + ["node -c scripts/create-page-evidence-session.mjs"], + ["node -c scripts/finalize-page-evidence.mjs"], + ["node -c scripts/validate-page-evidence.mjs"], + ["node -c scripts/test-validate-page-evidence.mjs"], + ["node -c scripts/audit-evidence-freshness.mjs"], + ["node -c scripts/test-audit-evidence-freshness.mjs"], + ["node -c tests/fixtures/consumer-conformance-scenarios.mjs"], + ["node -c tests/helpers/render-consumer-conformance.mjs"], + ["node -c tests/consumer-conformance.spec.mjs"], + ["node -c scripts/test-consumer-conformance-sentinel.mjs"], + ["node scripts/test-validate-consumer-conformance.mjs --case valid-runtime-proof --json"], + ["node scripts/test-validate-consumer-conformance.mjs --json"], + ["node scripts/test-validate-page-evidence.mjs --json"], + ["node scripts/test-audit-evidence-freshness.mjs --json"], + [`node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/consumer-conformance-record.schema.json"))'`], + [`node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/page-evidence-session.schema.json"))'`], + [`node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/page-evidence-manifest.schema.json"))'`], + ])); + + const blocking = workflowJob(validationWorkflow, "consumer-conformance"); + if (!blocking) failures.push(`${validatePath}: missing consumer-conformance job`); + else { + if (hasLiteralDisabledIf(blocking.split("\n"), 4)) failures.push(`${validatePath}: consumer-conformance job must be enabled`); + if (hasIf(blocking.split("\n"), 4)) failures.push(`${validatePath}: consumer-conformance job must be unconditional`); + if (hasIf(blocking.split("\n"), 6) || hasIf(blocking.split("\n"), 8)) failures.push(`${validatePath}: consumer-conformance: required command steps must be unconditional`); + if (/^\s+continue-on-error:/m.test(blocking)) failures.push(`${validatePath}: consumer-conformance must be blocking`); + failures.push(...missing(`${validatePath}: consumer-conformance`, blocking, [ + ["mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a"], + ])); + failures.push(...missingCommands(`${validatePath}: consumer-conformance`, executableRunText(blocking), [ + ["npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line"], + ["node scripts/test-consumer-conformance-sentinel.mjs --json"], + ])); + } + const capture = workflowJob(validationWorkflow, "consumer-page-evidence"); + if (!capture) failures.push(`${validatePath}: missing consumer-page-evidence job`); + else { + if (!/^\s+continue-on-error:\s*true\s*$/m.test(capture)) failures.push(`${validatePath}: consumer-page-evidence must be nonblocking`); + failures.push(...missing(`${validatePath}: consumer-page-evidence`, capture, [ + ["CONSUMER_CONFORMANCE_CAPTURE_DIR: .tmp/consumer-page-evidence"], + ["actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02"], + ["retention-days: 14"], + ])); + failures.push(...missingCommands(`${validatePath}: consumer-page-evidence`, executableRunText(capture), [ + ["node scripts/create-page-evidence-session.mjs"], + ["npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line"], + ["node scripts/finalize-page-evidence.mjs"], + ["node scripts/validate-page-evidence.mjs"], + ])); + } + + failures.push(...missing(freshnessPath, freshnessWorkflow, [ + ["workflow_dispatch:"], + ["cron: '0 5 * * 1'", "Monday 05:00 UTC schedule"], + ["contents: read"], + ["persist-credentials: false"], + ["evidence-freshness-report.json", "JSON report"], + ["::warning file=", "GitHub warning emission"], + ["if: always()", "always-upload evidence"], + ["actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02", "immutable upload action"], + ["retention-days: 14", "14-day retention"], + ])); + failures.push(...missing(freshnessPath, executableRunText(freshnessWorkflow), [ + ["scripts/audit-evidence-freshness.mjs", "freshness auditor"], + ["--record consumer-reference/baselines/calibration.json", "explicit calibration record inventory"], + ["--mode advisory"], + ])); + if (/continue-on-error\s*:/i.test(freshnessWorkflow)) failures.push(`${freshnessPath}: continue-on-error is forbidden because malformed records and tool failures must block`); + for (const pin of immutableActionPins) { + if ((pin.includes("actions/checkout") || pin.includes("actions/setup-node") || pin.includes("actions/upload-artifact")) && !freshnessWorkflow.includes(pin)) { + failures.push(`${freshnessPath}: missing immutable action pin ${pin}`); + } + } + failures.push(...workflowActionFailures(freshnessWorkflow, freshnessPath)); + return failures; +} diff --git a/scripts/governance-test-fixture.mjs b/scripts/governance-test-fixture.mjs index f710646..05f4f31 100644 --- a/scripts/governance-test-fixture.mjs +++ b/scripts/governance-test-fixture.mjs @@ -37,6 +37,8 @@ export const files = { "Governed-local button states retain source-bound visual, DOM, and accessibility-tree evidence across both example profiles.", "one clean capture session binds the browser artifacts to the governed source inventory", "It does not prove product suitability, independent adoption, full accessibility, cross-browser behavior, or owner approval", + "Explicit evidence deadlines are audited weekly without inventing a repository-wide maximum age.", + "Malformed records and auditor failures remain blocking even though due-date findings are advisory.", "Linux/amd64 20-run calibration and `baseline_owner_approval` remain pending.", ...sentinelProvenanceClauses, "", @@ -83,7 +85,9 @@ export const files = { " persist-credentials: false", " - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", " - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", + " - run: |", "node -c scripts/component-state-workflow-contract.mjs", + "node -c scripts/consumer-evidence-governance-contract.mjs", "node -c scripts/promotion-fixture-inventory.mjs", "node -c scripts/promotion-boundary-test-contract.mjs", "node -c scripts/validate-governance.mjs", @@ -103,10 +107,34 @@ export const files = { "node -c scripts/promotion-attestation-contract.mjs", "node -c scripts/validate-promotion-rfc.mjs", "node -c scripts/test-validate-promotion-rfc.mjs", + "node -c scripts/json-schema-formats.mjs", + "node -c scripts/consumer-conformance-contract.mjs", + "node -c scripts/validate-consumer-conformance.mjs", + "node -c scripts/test-validate-consumer-conformance.mjs", + "node -c consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs", + "node -c scripts/page-evidence-contract.mjs", + "node -c scripts/page-evidence-fixture.mjs", + "node -c scripts/create-page-evidence-session.mjs", + "node -c scripts/finalize-page-evidence.mjs", + "node -c scripts/validate-page-evidence.mjs", + "node -c scripts/test-validate-page-evidence.mjs", + "node -c scripts/audit-evidence-freshness.mjs", + "node -c scripts/test-audit-evidence-freshness.mjs", + "node -c tests/fixtures/consumer-conformance-scenarios.mjs", + "node -c tests/helpers/render-consumer-conformance.mjs", + "node -c tests/consumer-conformance.spec.mjs", + "node -c scripts/test-consumer-conformance-sentinel.mjs", "node scripts/validate-consumer-reference.mjs --json", "node scripts/test-validate-consumer-reference.mjs --json", "node scripts/validate-promotion-rfc.mjs --json", "node scripts/test-validate-promotion-rfc.mjs --json", + "node scripts/test-validate-consumer-conformance.mjs --case valid-runtime-proof --json", + "node scripts/test-validate-consumer-conformance.mjs --json", + "node scripts/test-validate-page-evidence.mjs --json", + "node scripts/test-audit-evidence-freshness.mjs --json", + `node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/consumer-conformance-record.schema.json"))'`, + `node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/page-evidence-session.schema.json"))'`, + `node -e 'JSON.parse(require("fs").readFileSync("consumer-reference/schema/page-evidence-manifest.schema.json"))'`, "consumer-reference/schema/promotion-rfc.schema.json", "consumer-reference/policies/shared-experimental.json", "node scripts/test-consumer-reference-sentinel.mjs", @@ -146,6 +174,27 @@ export const files = { " steps:", " - run: node scripts/test-consumer-reference-sentinel.mjs", " chromium-calibration:", + " consumer-conformance:", + " name: Consumer conformance Chromium matrix", + " container:", + " image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", + " steps:", + " - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + " - run: node scripts/test-consumer-conformance-sentinel.mjs --json", + " consumer-page-evidence:", + " name: Consumer page-evidence raster capture (nonblocking)", + " continue-on-error: true", + " env:", + " CONSUMER_CONFORMANCE_CAPTURE_DIR: .tmp/consumer-page-evidence", + " steps:", + " - run: node scripts/create-page-evidence-session.mjs --json", + " - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + " - run: node scripts/finalize-page-evidence.mjs --json", + " - run: node scripts/validate-page-evidence.mjs --json", + " - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", + " with:", + " name: consumer-page-evidence-${{ github.run_id }}-${{ github.sha }}", + " retention-days: 14", "node scripts/validate-baseline-manifest.mjs --json", "node scripts/test-validate-baseline-manifest.mjs --json", "node scripts/test-summarize-sentinel-calibration.mjs", @@ -158,10 +207,43 @@ export const files = { "permissions:", " contents: read", "", + ].map((line, index, lines) => { + const commandStart = lines.indexOf("node -c scripts/component-state-workflow-contract.mjs"); + const commandEnd = lines.indexOf("consumer-reference/policies/shared-experimental.json"); + return index >= commandStart && index <= commandEnd ? ` ${line}` : line; + }).join("\n"), + ".github/workflows/evidence-freshness.yml": [ + "name: Audit explicit evidence deadlines", + "on:", + " workflow_dispatch:", + " schedule:", + " - cron: '0 5 * * 1'", + "permissions:", + " contents: read", + "jobs:", + " evidence-freshness:", + " runs-on: ubuntu-latest", + " steps:", + " - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4", + " with:", + " persist-credentials: false", + " - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", + " - run: npm ci --ignore-scripts --no-audit --no-fund", + " - run: |", + " node scripts/audit-evidence-freshness.mjs --record consumer-reference/baselines/calibration.json --as-of \"$as_of\" --mode advisory --json > evidence-freshness-report.json", + " console.log(`::warning file=${warning.source_path}::${warning.code}: ${warning.message}`);", + " - if: always()", + " uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4", + " with:", + " name: evidence-freshness-${{ github.run_id }}", + " path: evidence-freshness-report.json", + " retention-days: 14", + "", ].join("\n"), "GOVERNANCE.md": [ "---", "scheduled_stale_audit: deferred", + "scheduled_evidence_audit: active_advisory", "---", "", "# Governance, Lifecycle, And Docs-As-Code", @@ -176,6 +258,10 @@ export const files = { "| Consumer reference contract | `consumer-reference/contract.md` | Manual | None | `stable` | Receiver changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", "| Governed local reference profiles | `design-engineering/reference-profiles/governed-local/editorial/profile.json`, `design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/editorial/local-foundations.json`, `design-engineering/reference-profiles/governed-local/terminal/profile.json`, `design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json`, `design-engineering/reference-profiles/governed-local/terminal/local-foundations.json` | Manual | None | `experimental`, `example_only`, non-default related fixtures | Profile source changes. | `scripts/validate-consumer-reference.mjs` | Validation owner |", "| Component-state evidence matrices | Each profile's declared `components/*.component.json`, `states/*.states.json`, `fixtures/*.fixture.json`, and `evidence/*.evidence.json` records | `scripts/generate-consumer-reference-evidence.mjs` | `design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md`, `design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md` | `generated` | Source or generator changes. | `scripts/validate-component-state.mjs` | Validation owner |", + "| Consumer migration conformance | `consumer-reference/schema/consumer-conformance-record.schema.json`, `design-engineering/consumer-migration-readiness.md` | Manual | Consumer-owned conformance records | `experimental` method | Method, dimension, scenario, mapping, or ownership changes. | `scripts/validate-consumer-conformance.mjs`, `scripts/test-validate-consumer-conformance.mjs` | Design Engineering owner with Validation owner |", + "| Consumer page-evidence lifecycle | `consumer-reference/schema/page-evidence-session.schema.json`, `consumer-reference/schema/page-evidence-manifest.schema.json` | Browser/session tooling | Consumer-owned session, runner, manifest, and raster artifacts | `experimental` evidence protocol | Source, revision, session, run, scenario, review deadline, or artifact integrity changes. | `scripts/test-validate-page-evidence.mjs`, deterministic Chromium consumer-conformance job | Validation owner with consumer owner |", + "| Explicit evidence freshness schedule | Evidence records that declare `expires_at` or `review_by` | `.github/workflows/evidence-freshness.yml` | Advisory JSON audit artifact | `stable` narrow schedule | Explicit deadline, auditor, schedule, or inventory changes. | `scripts/audit-evidence-freshness.mjs`, `scripts/test-audit-evidence-freshness.mjs` | Validation owner |", + "| Deterministic consumer browser conformance | `tests/consumer-conformance.spec.mjs`, `tests/fixtures/consumer-conformance-scenarios.mjs`, `tests/helpers/render-consumer-conformance.mjs` | Playwright `1.61.0` in the digest-pinned `linux/amd64` image | Optional page-evidence raster artifact | `experimental` contract with blocking semantic gate and nonblocking raster capture | Viewport, container, content, state, overlay, page-scale, focus, overflow, contrast, semantics, or runtime-error changes. | Playwright matrix and `scripts/test-consumer-conformance-sentinel.mjs` | Validation owner with Design Engineering owner |", "| Shared promotion policy | Promotion policy | Manual | None | stable | Policy changes. | promotion validator | Validation owner |", "| Proposed Chromium sentinel | `tests/helpers/render-consumer-reference.mjs`, `consumer-reference/schema/calibration-record.schema.json`, `consumer-reference/baselines/*.json` | Playwright | Snapshot and raw calibration evidence | `experimental` | Renderer or baseline changes. | `scripts/validate-baseline-manifest.mjs` | Validation owner |", "| Domain manifest and scope decision | `DOMAINS.md`, `quality/claim-records/stylegallery-multidomain-scope.md` | Manual | None | `stable` | Domain membership, repository-scope, or provenance-policy changes. | `scripts/validate-domains.mjs`, `scripts/validate-governance.mjs` | Repository governance owner |", @@ -219,7 +305,11 @@ export const files = { "A stable contract must never be silently relabeled shared-experimental.", "", "## Staleness Control", - "Decision: no scheduled stale-content workflow yet.", + "Decision: run a weekly advisory audit only for evidence records that already declare `expires_at` or `review_by`.", + "No repository-wide maximum age or inferred time-to-live applies.", + "Malformed records and auditor failures remain blocking even though due-date findings are advisory.", + ".github/workflows/evidence-freshness.yml", + "Monday at 05:00 UTC", "Audit trigger:", "node scripts/validate-links.mjs --json", "node scripts/test-consumer-reference-sentinel.mjs", diff --git a/scripts/test-validate-governance.mjs b/scripts/test-validate-governance.mjs index bfed51d..df54005 100644 --- a/scripts/test-validate-governance.mjs +++ b/scripts/test-validate-governance.mjs @@ -63,16 +63,114 @@ const cases = [ expect: ".github/workflows/validate.yml: missing permissions:", }, { name: "missing_stale_policy", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("scheduled_stale_audit: deferred\n", "") }, expect: "GOVERNANCE.md: missing scheduled_stale_audit: deferred" }, + { name: "missing_evidence_audit_policy", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("scheduled_evidence_audit: active_advisory\n", "") }, expect: "GOVERNANCE.md: missing scheduled_evidence_audit: active_advisory" }, { name: "paraphrased_stale_policy", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace( - "Decision: no scheduled stale-content workflow yet.", - "Decision: a scheduled stale-content workflow is not needed yet.", + "Decision: run a weekly advisory audit only for evidence records that already declare `expires_at` or `review_by`.", + "Decision: inspect old evidence from time to time.", ), }, - expectWarning: "GOVERNANCE.md: recommended wording missing Decision: no scheduled stale-content workflow yet.", + expect: "GOVERNANCE.md: missing Decision: run a weekly advisory audit only for evidence records that already declare `expires_at` or `review_by`.", }, + { name: "missing_no_universal_age_boundary", mutate: { "GOVERNANCE.md": files["GOVERNANCE.md"].replace("No repository-wide maximum age or inferred time-to-live applies.", "Evidence gets old after 30 days.") }, expect: "GOVERNANCE.md: missing No repository-wide maximum age or inferred time-to-live applies." }, + { name: "missing_freshness_workflow", omit: [".github/workflows/evidence-freshness.yml"], expect: ".github/workflows/evidence-freshness.yml: missing file" }, + { name: "freshness_schedule_drift", mutate: { ".github/workflows/evidence-freshness.yml": files[".github/workflows/evidence-freshness.yml"].replace("cron: '0 5 * * 1'", "cron: '0 5 * * 2'") }, expect: ".github/workflows/evidence-freshness.yml: missing Monday 05:00 UTC schedule" }, + { name: "freshness_mode_became_blocking", mutate: { ".github/workflows/evidence-freshness.yml": files[".github/workflows/evidence-freshness.yml"].replace("--mode advisory", "--mode blocking") }, expect: ".github/workflows/evidence-freshness.yml: missing --mode advisory" }, + { name: "freshness_blanket_continue", mutate: { ".github/workflows/evidence-freshness.yml": `${files[".github/workflows/evidence-freshness.yml"]}\ncontinue-on-error: true\n` }, expect: ".github/workflows/evidence-freshness.yml: continue-on-error is forbidden because malformed records and tool failures must block" }, + { name: "consumer_browser_became_nonblocking", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" consumer-conformance:\n", " consumer-conformance:\n continue-on-error: true\n") }, expect: ".github/workflows/validate.yml: consumer-conformance must be blocking" }, + { + name: "consumer_browser_command_commented_out", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: |\n # npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line\n true") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_relocated", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: true").replace(" chromium-sentinel:\n", " chromium-sentinel:\n steps:\n - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line\n") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_hidden_in_env", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - name: Hidden browser command\n env:\n run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line\n run: true") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_hidden_in_inline_comment", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: true # npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_in_disabled_step", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - name: Disabled browser command\n if: false\n run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_hidden_in_heredoc", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: |\n cat <<'PROBE'\n npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line\n PROBE\n true") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_hidden_in_numeric_heredoc", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: |\n cat <<'123'\n npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line\n 123\n true") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_echo_only", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: echo npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_shell_builtin_echo", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: command echo npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_short_circuited", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: false && npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_failure_ignored", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line || true") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_split_failure_ignored", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: |\n npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line \\\n || true") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_folded_failure_ignored", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - run: >-\n npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line\n || true") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_command_compound_false_step", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - if: ${{ false && true }}\n run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line") }, + expect: ".github/workflows/validate.yml: consumer-conformance: required command steps must be unconditional", + }, + { + name: "consumer_browser_job_compound_false", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" consumer-conformance:\n", " consumer-conformance:\n if: ${{ false && true }}\n") }, + expect: ".github/workflows/validate.yml: consumer-conformance job must be unconditional", + }, + { + name: "consumer_browser_command_quoted_disabled_step", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", " - name: Disabled browser command\n \"if\": false # disabled\n run: npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line") }, + expect: ".github/workflows/validate.yml: consumer-conformance: missing npx playwright test tests/consumer-conformance.spec.mjs --project=chromium --reporter=line", + }, + { + name: "consumer_browser_job_disabled", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" consumer-conformance:\n", " consumer-conformance:\n if: false\n") }, + expect: ".github/workflows/validate.yml: consumer-conformance job must be enabled", + }, + { + name: "page_lifecycle_command_commented_out", + mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" - run: node scripts/finalize-page-evidence.mjs --json", " - run: |\n # node scripts/finalize-page-evidence.mjs --json\n true") }, + expect: ".github/workflows/validate.yml: consumer-page-evidence: missing node scripts/finalize-page-evidence.mjs", + }, + { name: "page_capture_became_blocking", mutate: { ".github/workflows/validate.yml": files[".github/workflows/validate.yml"].replace(" continue-on-error: true\n env:\n CONSUMER_CONFORMANCE_CAPTURE_DIR", " env:\n CONSUMER_CONFORMANCE_CAPTURE_DIR") }, expect: ".github/workflows/validate.yml: consumer-page-evidence must be nonblocking" }, { name: "paraphrased_governance_link_label", mutate: { "README.md": files["README.md"].replace("[Governance, Lifecycle, And Docs-As-Code]", "[Governance reference]") }, expectWarning: "README.md: recommended link label missing [Governance, Lifecycle, And Docs-As-Code](GOVERNANCE.md)" }, { name: "missing_motion_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/motion/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /motion/ @changeroa" }, { name: "missing_consumer_reference_codeowner", mutate: { ".github/CODEOWNERS": files[".github/CODEOWNERS"].replace("/consumer-reference/ @changeroa\n", "") }, expect: ".github/CODEOWNERS: missing /consumer-reference/ @changeroa" }, diff --git a/scripts/validate-governance.mjs b/scripts/validate-governance.mjs index 51b3279..67c0ca9 100644 --- a/scripts/validate-governance.mjs +++ b/scripts/validate-governance.mjs @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { componentStateWorkflowFailures } from "./component-state-workflow-contract.mjs"; +import { consumerEvidenceGovernanceFailures } from "./consumer-evidence-governance-contract.mjs"; import { referenceOwnershipFailures } from "./governance-matrix-contract.mjs"; import { immutableActionPins, requiredCodeowners, sentinelProvenanceClauses } from "./governance-policy-contract.mjs"; import { promotionGovernanceFailures } from "./promotion-governance-contract.mjs"; @@ -53,6 +54,10 @@ function requireGovernanceMatrix() { "Consumer reference contract", "Governed local reference profiles", "Component-state evidence matrices", + "Consumer migration conformance", + "Consumer page-evidence lifecycle", + "Explicit evidence freshness schedule", + "Deterministic consumer browser conformance", "Shared promotion policy", "Proposed Chromium sentinel", "Domain manifest and scope decision", @@ -100,8 +105,6 @@ function requireOwnership() { } function requireStalenessDecision() { - requireIncludes("GOVERNANCE.md", "scheduled_stale_audit: deferred"); - recommendIncludes("GOVERNANCE.md", "Decision: no scheduled stale-content workflow yet."); requireIncludes("GOVERNANCE.md", "Audit trigger:"); requireIncludes("GOVERNANCE.md", "node scripts/validate-links.mjs --json"); } @@ -209,6 +212,12 @@ requireLifecycleStates(); requireOwnership(); requireStalenessDecision(); requireCiwiring(); +failures.push(...consumerEvidenceGovernanceFailures({ + evidence: read("quality/evidence/executable-evidence.md"), + freshnessWorkflow: read(".github/workflows/evidence-freshness.yml"), + governance: read("GOVERNANCE.md"), + validationWorkflow: read(".github/workflows/validate.yml"), +})); requireComponentStateCiIsolation(); requireImmutableActions(); requireRootLinks();