stylegallery 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +156 -0
- package/CATALOG.md +69 -0
- package/DOMAINS.md +65 -0
- package/GOVERNANCE.md +222 -0
- package/GUIDE.md +59 -0
- package/README.md +313 -0
- package/consumer-reference/adapters/style-dictionary.config.mjs +36 -0
- package/consumer-reference/agent-native/README.md +157 -0
- package/consumer-reference/agent-native/registry.json +640 -0
- package/consumer-reference/agent-native/schema/agent-native.schema.json +848 -0
- package/consumer-reference/agent-native/schema/epistemic.schema.json +14 -0
- package/consumer-reference/agent-native/schema/execution.schema.json +12 -0
- package/consumer-reference/agent-native/schema/identity.schema.json +10 -0
- package/consumer-reference/agent-native/schema/learning.schema.json +12 -0
- package/consumer-reference/agent-native/schema/manifest.schema.json +6 -0
- package/consumer-reference/agent-native/schema/operation.schema.json +12 -0
- package/consumer-reference/agent-native/schema/protocol-binding.schema.json +9 -0
- package/consumer-reference/agent-native/schema/retrieval.schema.json +10 -0
- package/consumer-reference/baselines/calibration.json +260 -0
- package/consumer-reference/baselines/manifest.json +21 -0
- package/consumer-reference/contract.md +83 -0
- package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +282 -0
- package/consumer-reference/fixtures/consumer-conformance/manifest.json +102 -0
- package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +32 -0
- package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +18 -0
- package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +142 -0
- package/consumer-reference/fixtures/invalid-stable-ended-support.json +19 -0
- package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +26 -0
- package/consumer-reference/fixtures/promotion/invalid-count-only.json +29 -0
- package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +25 -0
- package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +25 -0
- package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +45 -0
- package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +26 -0
- package/consumer-reference/fixtures/promotion/manifest.json +15 -0
- package/consumer-reference/fixtures/promotion/valid-deferred-example.json +67 -0
- package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +67 -0
- package/consumer-reference/fixtures/token-portability/invalid-cycle.json +7 -0
- package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +6 -0
- package/consumer-reference/fixtures/token-portability/invalid-extends.json +4 -0
- package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +7 -0
- package/consumer-reference/fixtures/token-portability/invalid-resolver.json +9 -0
- package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +10 -0
- package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +6 -0
- package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +6 -0
- package/consumer-reference/fixtures/token-portability/valid-border.json +14 -0
- package/consumer-reference/fixtures/token-portability/valid-reference.json +32 -0
- package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +7 -0
- package/consumer-reference/fixtures/valid-deprecated.json +21 -0
- package/consumer-reference/fixtures/valid-experimental.json +19 -0
- package/consumer-reference/generated/manifest.json +40 -0
- package/consumer-reference/generated/tokens.css +12 -0
- package/consumer-reference/index.md +32 -0
- package/consumer-reference/policies/shared-experimental.json +37 -0
- package/consumer-reference/schema/ax-evidence.schema.json +30 -0
- package/consumer-reference/schema/baseline-manifest.schema.json +55 -0
- package/consumer-reference/schema/calibration-record.schema.json +190 -0
- package/consumer-reference/schema/capture-session.schema.json +161 -0
- package/consumer-reference/schema/component-state.schema.json +150 -0
- package/consumer-reference/schema/consumer-conformance-record.schema.json +365 -0
- package/consumer-reference/schema/dom-evidence.schema.json +38 -0
- package/consumer-reference/schema/evidence-record.schema.json +81 -0
- package/consumer-reference/schema/fixture-manifest.schema.json +32 -0
- package/consumer-reference/schema/item.schema.json +188 -0
- package/consumer-reference/schema/page-evidence-manifest.schema.json +216 -0
- package/consumer-reference/schema/page-evidence-session.schema.json +119 -0
- package/consumer-reference/schema/portable-tokens.schema.json +104 -0
- package/consumer-reference/schema/promotion-rfc.schema.json +219 -0
- package/consumer-reference/schema/runtime-evidence-manifest.schema.json +33 -0
- package/consumer-reference/schema/visual-evidence.schema.json +31 -0
- package/design-engineering/consumer-migration-readiness.md +124 -0
- package/design-engineering/index.md +28 -0
- package/design-engineering/interface-craft.md +107 -0
- package/design-engineering/reference-profiles/external-adaptation/index.md +12 -0
- package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +125 -0
- package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +3504 -0
- package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +65 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +23 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +19 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +21 -0
- package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +12 -0
- package/design-engineering/reference-profiles/governed-local/editorial/profile.json +43 -0
- package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +195 -0
- package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +17 -0
- package/design-engineering/reference-profiles/governed-local/index.md +26 -0
- package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +125 -0
- package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +3504 -0
- package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +65 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +23 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +19 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +21 -0
- package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +12 -0
- package/design-engineering/reference-profiles/governed-local/terminal/profile.json +43 -0
- package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +195 -0
- package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +17 -0
- package/design-engineering/reference-profiles/index.md +19 -0
- package/game-ui/classification.md +82 -0
- package/game-ui/index.md +35 -0
- package/game-ui/reference-record.md +62 -0
- package/game-ui/screen-hierarchy.md +107 -0
- package/game-ui/unity/architecture.md +166 -0
- package/game-ui/unity/cli-loop.md +84 -0
- package/game-ui/unity/data/unity-technologies-public-repositories.json +26654 -0
- package/game-ui/unity/org-wiki.md +146 -0
- package/game-ui/unity/repository-map.md +102 -0
- package/game-ui/unity/ui-systems.md +85 -0
- package/guides/decision-tree.md +149 -0
- package/guides/documentation-mode-taxonomy.md +157 -0
- package/guides/layout-brief.md +220 -0
- package/guides/vocabulary.md +148 -0
- package/guides/webpage-generation-workflow.md +185 -0
- package/index.md +36 -0
- package/layout/index.md +26 -0
- package/motion/index.md +24 -0
- package/motion/practice-reference.md +145 -0
- package/motion/review-workflow.md +86 -0
- package/motion/vocabulary.md +82 -0
- package/package.json +78 -0
- package/patterns/centering/center.md +87 -0
- package/patterns/centering/index.md +6 -0
- package/patterns/centering/super-center.md +89 -0
- package/patterns/containment/box.md +87 -0
- package/patterns/containment/clamped-card.md +86 -0
- package/patterns/containment/content-limiter.md +87 -0
- package/patterns/containment/fluid-styles.md +86 -0
- package/patterns/containment/index.md +8 -0
- package/patterns/grid-repetition/card-grid.md +88 -0
- package/patterns/grid-repetition/columns.md +87 -0
- package/patterns/grid-repetition/dense-grid.md +88 -0
- package/patterns/grid-repetition/grid-wrapper.md +89 -0
- package/patterns/grid-repetition/index.md +12 -0
- package/patterns/grid-repetition/masonry-approx.md +87 -0
- package/patterns/grid-repetition/page-grid.md +90 -0
- package/patterns/grid-repetition/ram-grid.md +88 -0
- package/patterns/grid-repetition/twelve-span-grid.md +92 -0
- package/patterns/in-line-grouping/badge-list.md +94 -0
- package/patterns/in-line-grouping/breadcrumb.md +89 -0
- package/patterns/in-line-grouping/cluster.md +89 -0
- package/patterns/in-line-grouping/deconstructed-pancake.md +92 -0
- package/patterns/in-line-grouping/index.md +13 -0
- package/patterns/in-line-grouping/pagination.md +89 -0
- package/patterns/in-line-grouping/reel.md +90 -0
- package/patterns/in-line-grouping/split-nav.md +97 -0
- package/patterns/in-line-grouping/tab-strip.md +88 -0
- package/patterns/in-line-grouping/wrap-row.md +89 -0
- package/patterns/index.md +13 -0
- package/patterns/media-fit/frame.md +95 -0
- package/patterns/media-fit/icon-frame.md +87 -0
- package/patterns/media-fit/index.md +6 -0
- package/patterns/overlay-exception/imposter.md +95 -0
- package/patterns/overlay-exception/index.md +6 -0
- package/patterns/overlay-exception/overlay-stack.md +89 -0
- package/patterns/split-sidebar/index.md +12 -0
- package/patterns/split-sidebar/list-detail.md +95 -0
- package/patterns/split-sidebar/main-with-rail.md +95 -0
- package/patterns/split-sidebar/media-object.md +102 -0
- package/patterns/split-sidebar/sidebar.md +101 -0
- package/patterns/split-sidebar/split-screen.md +92 -0
- package/patterns/split-sidebar/sticky-aside.md +97 -0
- package/patterns/split-sidebar/supporting-pane.md +95 -0
- package/patterns/split-sidebar/switcher.md +93 -0
- package/patterns/stacking/feed.md +88 -0
- package/patterns/stacking/index.md +8 -0
- package/patterns/stacking/line-up.md +95 -0
- package/patterns/stacking/stack.md +87 -0
- package/patterns/stacking/step-nav.md +88 -0
- package/patterns/viewport-shell/cover.md +90 -0
- package/patterns/viewport-shell/fixed-sidenav-shell.md +97 -0
- package/patterns/viewport-shell/holy-grail.md +112 -0
- package/patterns/viewport-shell/index.md +11 -0
- package/patterns/viewport-shell/panel-layout.md +95 -0
- package/patterns/viewport-shell/scroll-body-shell.md +93 -0
- package/patterns/viewport-shell/sticky-footer.md +92 -0
- package/patterns/viewport-shell/sticky-header.md +86 -0
- package/platform-guides/apple-interaction.md +89 -0
- package/platform-guides/index.md +26 -0
- package/quality/claim-records/index.md +3 -0
- package/quality/claim-records/samples.md +92 -0
- package/quality/claim-records/stylegallery-multidomain-scope.md +42 -0
- package/quality/claims.md +77 -0
- package/quality/evidence/accessibility.md +58 -0
- package/quality/evidence/consumer-migration.md +42 -0
- package/quality/evidence/design-rationale.md +34 -0
- package/quality/evidence/executable-evidence.md +79 -0
- package/quality/evidence/families.md +56 -0
- package/quality/evidence/hci-evaluation.md +36 -0
- package/quality/evidence/index.md +16 -0
- package/quality/evidence/pattern-contract-audit.md +96 -0
- package/quality/evidence/search-metadata-polyhierarchy.md +86 -0
- package/quality/evidence/sources.md +43 -0
- package/quality/evidence/tokens.md +33 -0
- package/quality/evidence/visual-qa.md +41 -0
- package/quality/gates/accessibility-evidence.md +60 -0
- package/quality/gates/consumer-migration-evidence.md +53 -0
- package/quality/gates/design-claim.md +56 -0
- package/quality/gates/harmony-evaluation.md +78 -0
- package/quality/gates/index.md +13 -0
- package/quality/gates/layout.md +52 -0
- package/quality/gates/rationale.md +53 -0
- package/quality/gates/visual-evidence.md +47 -0
- package/quality/index.md +55 -0
- package/quality/principles.md +58 -0
- package/recipes/article-page.md +52 -0
- package/recipes/command-surface.md +55 -0
- package/recipes/dashboard.md +56 -0
- package/recipes/form-flow.md +55 -0
- package/recipes/homepage.md +89 -0
- package/recipes/index.md +13 -0
- package/recipes/list-detail.md +55 -0
- package/recipes/primitive-to-recipe-matrix.md +83 -0
- package/recipes/saas-settings.md +52 -0
- package/scripts/agent-native/a2a-projection.mjs +248 -0
- package/scripts/agent-native/agui-projection.mjs +178 -0
- package/scripts/agent-native/canonical-json.mjs +109 -0
- package/scripts/agent-native/cli-adapter.mjs +68 -0
- package/scripts/agent-native/events.mjs +113 -0
- package/scripts/agent-native/execution.mjs +227 -0
- package/scripts/agent-native/fixture.mjs +159 -0
- package/scripts/agent-native/identity.mjs +184 -0
- package/scripts/agent-native/immutable-store.mjs +102 -0
- package/scripts/agent-native/knowledge.mjs +168 -0
- package/scripts/agent-native/learning-records.mjs +244 -0
- package/scripts/agent-native/learning.mjs +247 -0
- package/scripts/agent-native/mcp-adapter.mjs +173 -0
- package/scripts/agent-native/queries.mjs +173 -0
- package/scripts/agent-native/registry.mjs +154 -0
- package/scripts/agent-native/retrieval.mjs +171 -0
- package/scripts/agent-native/self-description.mjs +160 -0
- package/scripts/artifact-metadata.mjs +20 -0
- package/scripts/audit-evidence-freshness.mjs +308 -0
- package/scripts/baseline-contract.mjs +189 -0
- package/scripts/baseline-schema-parity.mjs +40 -0
- package/scripts/build-reference-artifacts.mjs +127 -0
- package/scripts/calibration-raw-contract.mjs +172 -0
- package/scripts/capture-session-contract.mjs +187 -0
- package/scripts/component-state-artifact-fixture.mjs +44 -0
- package/scripts/component-state-contract.mjs +99 -0
- package/scripts/component-state-semantics.mjs +128 -0
- package/scripts/component-state-workflow-contract.mjs +67 -0
- package/scripts/component-state-workflow-negative-cases.mjs +64 -0
- package/scripts/consumer-conformance-contract.mjs +198 -0
- package/scripts/consumer-evidence-governance-contract.mjs +210 -0
- package/scripts/consumer-reference-case-runner.mjs +93 -0
- package/scripts/consumer-reference-schema.mjs +155 -0
- package/scripts/create-component-state-session.mjs +100 -0
- package/scripts/create-page-evidence-session.mjs +139 -0
- package/scripts/domain-document-boundaries.mjs +40 -0
- package/scripts/domain-validation-checks.mjs +141 -0
- package/scripts/evidence-artifact-contract.mjs +191 -0
- package/scripts/finalize-component-state-evidence.mjs +165 -0
- package/scripts/finalize-page-evidence.mjs +197 -0
- package/scripts/generate-consumer-reference-evidence.mjs +154 -0
- package/scripts/generate-patterns.mjs +218 -0
- package/scripts/governance-matrix-contract.mjs +73 -0
- package/scripts/governance-matrix-negative-cases.mjs +114 -0
- package/scripts/governance-policy-contract.mjs +49 -0
- package/scripts/governance-test-fixture.mjs +321 -0
- package/scripts/governance-workflow-negative-cases.mjs +241 -0
- package/scripts/governed-profile-registry.mjs +125 -0
- package/scripts/json-schema-formats.mjs +31 -0
- package/scripts/markdown-structure.mjs +45 -0
- package/scripts/page-artifact-metadata.mjs +124 -0
- package/scripts/page-evidence-contract.mjs +297 -0
- package/scripts/page-evidence-fixture.mjs +271 -0
- package/scripts/pattern-data.mjs +325 -0
- package/scripts/profile-record-contract.mjs +140 -0
- package/scripts/promotion-attestation-contract.mjs +91 -0
- package/scripts/promotion-boundary-test-contract.mjs +92 -0
- package/scripts/promotion-fixture-inventory.mjs +109 -0
- package/scripts/promotion-governance-contract.mjs +28 -0
- package/scripts/reference-artifact-path-contract.mjs +28 -0
- package/scripts/reference-profile-contract.mjs +114 -0
- package/scripts/reference-token-contract.mjs +156 -0
- package/scripts/run-consumer-page-evidence-ci.mjs +170 -0
- package/scripts/sg-mcp.mjs +16 -0
- package/scripts/sg.mjs +27 -0
- package/scripts/strict-json.mjs +72 -0
- package/scripts/summarize-sentinel-calibration.mjs +98 -0
- package/scripts/test-agent-adapter-conformance.mjs +250 -0
- package/scripts/test-agent-conformance-receipt.mjs +106 -0
- package/scripts/test-agent-events.mjs +81 -0
- package/scripts/test-agent-execution-effects.mjs +114 -0
- package/scripts/test-agent-execution-fixtures.mjs +140 -0
- package/scripts/test-agent-execution-kernel.mjs +39 -0
- package/scripts/test-agent-execution-learning.mjs +130 -0
- package/scripts/test-agent-execution-lifecycle.mjs +143 -0
- package/scripts/test-agent-execution-safety.mjs +158 -0
- package/scripts/test-agent-execution-views.mjs +117 -0
- package/scripts/test-agent-identity-integrity-cases.mjs +160 -0
- package/scripts/test-agent-knowledge-kernel.mjs +183 -0
- package/scripts/test-agent-learning.mjs +246 -0
- package/scripts/test-agent-mcp.mjs +246 -0
- package/scripts/test-agent-native-schemas.mjs +215 -0
- package/scripts/test-agent-projections.mjs +249 -0
- package/scripts/test-agent-registry-mcp-integrity.mjs +215 -0
- package/scripts/test-agent-registry-mutations.mjs +110 -0
- package/scripts/test-audit-evidence-freshness.mjs +177 -0
- package/scripts/test-component-state-sentinel.mjs +35 -0
- package/scripts/test-component-state-source-contract.mjs +83 -0
- package/scripts/test-consumer-conformance-sentinel.mjs +165 -0
- package/scripts/test-consumer-reference-sentinel.mjs +31 -0
- package/scripts/test-generate-consumer-reference-evidence.mjs +124 -0
- package/scripts/test-json-cli-output.mjs +151 -0
- package/scripts/test-json-schema-formats.mjs +40 -0
- package/scripts/test-reference-adapters.mjs +199 -0
- package/scripts/test-renderer-purity.mjs +34 -0
- package/scripts/test-sg-cli.mjs +129 -0
- package/scripts/test-summarize-sentinel-calibration.mjs +226 -0
- package/scripts/test-validate-baseline-manifest.mjs +234 -0
- package/scripts/test-validate-component-state-artifacts.mjs +243 -0
- package/scripts/test-validate-component-state.mjs +106 -0
- package/scripts/test-validate-consumer-conformance.mjs +427 -0
- package/scripts/test-validate-consumer-reference-cli.mjs +69 -0
- package/scripts/test-validate-consumer-reference.mjs +263 -0
- package/scripts/test-validate-domains.mjs +357 -0
- package/scripts/test-validate-governance.mjs +292 -0
- package/scripts/test-validate-ia.mjs +262 -0
- package/scripts/test-validate-okf.mjs +73 -0
- package/scripts/test-validate-page-evidence.mjs +302 -0
- package/scripts/test-validate-patterns.mjs +157 -0
- package/scripts/test-validate-promotion-rfc.mjs +250 -0
- package/scripts/test-validate-reference-profiles.mjs +170 -0
- package/scripts/test-validate-unity-org-wiki.mjs +57 -0
- package/scripts/test-validate-unity-source-contracts.mjs +67 -0
- package/scripts/test-validate-vocabulary.mjs +174 -0
- package/scripts/test-validate-webpage-workflow.mjs +293 -0
- package/scripts/validate-baseline-manifest.mjs +44 -0
- package/scripts/validate-catalog.mjs +65 -0
- package/scripts/validate-component-state.mjs +141 -0
- package/scripts/validate-consumer-conformance.mjs +477 -0
- package/scripts/validate-consumer-reference.mjs +288 -0
- package/scripts/validate-domains.mjs +237 -0
- package/scripts/validate-governance.mjs +248 -0
- package/scripts/validate-ia.mjs +136 -0
- package/scripts/validate-links.mjs +66 -0
- package/scripts/validate-okf.mjs +104 -0
- package/scripts/validate-page-evidence.mjs +191 -0
- package/scripts/validate-patterns.mjs +246 -0
- package/scripts/validate-promotion-rfc.mjs +224 -0
- package/scripts/validate-reference-artifacts.mjs +125 -0
- package/scripts/validate-renderer-purity.mjs +56 -0
- package/scripts/validate-unity-org-wiki.mjs +146 -0
- package/scripts/validate-unity-source-contracts.mjs +107 -0
- package/scripts/validate-vocabulary.mjs +115 -0
- package/scripts/validate-webpage-workflow.mjs +254 -0
- package/scripts/visual-expectation-contract.mjs +20 -0
- package/scripts/workflow-action-contract.mjs +163 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { artifactMetadata } from "./artifact-metadata.mjs";
|
|
7
|
+
import {
|
|
8
|
+
canonicalIntended,
|
|
9
|
+
readCaptureSession,
|
|
10
|
+
sameJson,
|
|
11
|
+
sourceManifestMatches,
|
|
12
|
+
} from "./capture-session-contract.mjs";
|
|
13
|
+
import { compileSchemas, validateEvidenceArtifacts } from "./component-state-contract.mjs";
|
|
14
|
+
import { resolveProfileRecords } from "./profile-record-contract.mjs";
|
|
15
|
+
import { parseStrictJson } from "./strict-json.mjs";
|
|
16
|
+
|
|
17
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
const options = {
|
|
19
|
+
artifactRoot: undefined,
|
|
20
|
+
json: false,
|
|
21
|
+
output: undefined,
|
|
22
|
+
profileRoot: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"),
|
|
23
|
+
sessionReceipt: undefined,
|
|
24
|
+
writeCanonical: false,
|
|
25
|
+
};
|
|
26
|
+
const failures = [];
|
|
27
|
+
|
|
28
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
29
|
+
const argument = process.argv[index];
|
|
30
|
+
if (argument === "--json") options.json = true;
|
|
31
|
+
else if (argument === "--write-canonical") options.writeCanonical = true;
|
|
32
|
+
else if (["--artifact-root", "--output", "--root", "--session-receipt"].includes(argument)) {
|
|
33
|
+
const value = process.argv[index + 1];
|
|
34
|
+
if (!value) failures.push({ code: "argument_value_required", message: `${argument} requires a value`, path: "<cli>" });
|
|
35
|
+
else {
|
|
36
|
+
if (argument === "--artifact-root") options.artifactRoot = path.resolve(value);
|
|
37
|
+
if (argument === "--output") options.output = path.resolve(value);
|
|
38
|
+
if (argument === "--root") options.profileRoot = path.resolve(value);
|
|
39
|
+
if (argument === "--session-receipt") options.sessionReceipt = path.resolve(value);
|
|
40
|
+
index += 1;
|
|
41
|
+
}
|
|
42
|
+
} else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "<cli>" });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!options.artifactRoot) failures.push({ code: "argument_value_required", message: "--artifact-root is required", path: "<cli>" });
|
|
46
|
+
const output = options.output ?? (options.artifactRoot ? path.join(options.artifactRoot, "runtime-manifest.json") : undefined);
|
|
47
|
+
if (output && fs.existsSync(output)) failures.push({ code: "capture_session_replay", message: "completed session manifest already exists and cannot be overwritten", path: output });
|
|
48
|
+
const receiptFile = options.sessionReceipt ?? (options.artifactRoot ? path.join(options.artifactRoot, "capture-session.json") : "");
|
|
49
|
+
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 });
|
|
50
|
+
const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/schema"));
|
|
51
|
+
const capture = receiptFile ? readCaptureSession(receiptFile, schemas.capture, failures) : undefined;
|
|
52
|
+
const intended = canonicalIntended(options.profileRoot, failures);
|
|
53
|
+
if (capture && !sameJson(capture.receipt.intended, intended)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from canonical profile scenarios", path: receiptFile });
|
|
54
|
+
if (capture && !capture.receipt.source) failures.push({ code: "capture_source_missing", message: "capture receipt must include its canonical source manifest", path: receiptFile });
|
|
55
|
+
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 });
|
|
56
|
+
|
|
57
|
+
const completedAt = new Date().toISOString();
|
|
58
|
+
const completedSession = capture ? {
|
|
59
|
+
attempt: capture.receipt.attempt,
|
|
60
|
+
branch: capture.receipt.branch,
|
|
61
|
+
completed_at: completedAt,
|
|
62
|
+
environment: capture.receipt.environment,
|
|
63
|
+
intended: capture.receipt.intended,
|
|
64
|
+
nonce: capture.receipt.nonce,
|
|
65
|
+
receipt: capture.artifact,
|
|
66
|
+
receipt_sha256: capture.digest,
|
|
67
|
+
repository: capture.receipt.repository,
|
|
68
|
+
revision: capture.receipt.revision,
|
|
69
|
+
session_id: capture.receipt.session_id,
|
|
70
|
+
source: capture.receipt.source,
|
|
71
|
+
started_at: capture.receipt.started_at,
|
|
72
|
+
} : undefined;
|
|
73
|
+
const run = capture ? {
|
|
74
|
+
attempt: capture.receipt.attempt,
|
|
75
|
+
id: capture.receipt.session_id,
|
|
76
|
+
repository: capture.receipt.repository,
|
|
77
|
+
revision: capture.receipt.revision,
|
|
78
|
+
source: process.env.GITHUB_ACTIONS === "true" ? "github_actions" : "local",
|
|
79
|
+
} : undefined;
|
|
80
|
+
const channelFiles = Object.freeze({ ax: "ax.json", dom: "dom.json", visual: "png" });
|
|
81
|
+
const records = [];
|
|
82
|
+
const expectedFiles = new Set();
|
|
83
|
+
const canonicalWrites = [];
|
|
84
|
+
|
|
85
|
+
for (const profileIntent of intended) {
|
|
86
|
+
const profileRoot = path.join(options.profileRoot, profileIntent.profile_name);
|
|
87
|
+
const profileFailures = [];
|
|
88
|
+
const resolved = resolveProfileRecords(profileRoot, profileFailures);
|
|
89
|
+
failures.push(...profileFailures);
|
|
90
|
+
const fixture = resolved?.records.fixture[0]?.value;
|
|
91
|
+
const states = resolved?.records.states[0]?.value;
|
|
92
|
+
if (!resolved || !fixture || !states || !capture || !run || !completedSession) continue;
|
|
93
|
+
const stateById = new Map(states.scenarios.map((scenario) => [scenario.id, scenario]));
|
|
94
|
+
const passes = [];
|
|
95
|
+
for (const scenario of fixture.scenarios) {
|
|
96
|
+
const canonical = stateById.get(scenario.id);
|
|
97
|
+
const documents = {};
|
|
98
|
+
for (const channel of ["dom", "ax", "visual"]) {
|
|
99
|
+
const file = path.join(options.artifactRoot, "runtime", `${profileIntent.profile_name}-${scenario.id}.${channel}.json`);
|
|
100
|
+
try { documents[channel] = parseStrictJson(fs.readFileSync(file, "utf8")); }
|
|
101
|
+
catch (error) { failures.push({ code: "evidence_json_invalid", message: error instanceof Error ? error.message : String(error), path: file }); }
|
|
102
|
+
if (channel === "visual") expectedFiles.add(path.basename(file));
|
|
103
|
+
}
|
|
104
|
+
const capturedTimes = new Set(Object.values(documents).map((document) => document?.captured_at).filter(Boolean));
|
|
105
|
+
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 });
|
|
106
|
+
const recordedAt = [...capturedTimes][0] ?? "";
|
|
107
|
+
for (const channel of scenario.required_channels) {
|
|
108
|
+
const suffix = channelFiles[channel];
|
|
109
|
+
const artifactPath = `runtime/${profileIntent.profile_name}-${scenario.id}.${suffix}`;
|
|
110
|
+
expectedFiles.add(path.basename(artifactPath));
|
|
111
|
+
const absolute = path.join(options.artifactRoot, artifactPath);
|
|
112
|
+
if (!fs.existsSync(absolute) || !fs.lstatSync(absolute).isFile() || fs.lstatSync(absolute).isSymbolicLink()) {
|
|
113
|
+
failures.push({ code: "runtime_artifact_missing", message: `${artifactPath} is missing or not a regular file`, path: absolute });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const mediaType = channel === "visual" ? "image/png" : "application/json";
|
|
117
|
+
passes.push({
|
|
118
|
+
artifact: { path: artifactPath, ...artifactMetadata(fs.readFileSync(absolute), mediaType) },
|
|
119
|
+
channel,
|
|
120
|
+
environment: capture.receipt.environment,
|
|
121
|
+
id: `${scenario.id}-${channel}`,
|
|
122
|
+
recorded_at: recordedAt,
|
|
123
|
+
result: { observed: `Recorded ${channel} evidence for ${scenario.id}; this is not certification.`, status: "passed" },
|
|
124
|
+
run,
|
|
125
|
+
scenario_id: scenario.id,
|
|
126
|
+
session: capture.link,
|
|
127
|
+
scope: { component: "button", semantic_mode: scenario.semantic_mode, state_set: canonical.states },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const evidence = {
|
|
132
|
+
claim_boundary: "Scenario artifacts are evidence, not visual regression baselines or accessibility certification.",
|
|
133
|
+
component_id: "button",
|
|
134
|
+
passes,
|
|
135
|
+
profile_id: resolved.profile.id,
|
|
136
|
+
schema_version: "1.0",
|
|
137
|
+
};
|
|
138
|
+
failures.push(...validateEvidenceArtifacts(profileRoot, options.artifactRoot, evidence, schemas, completedSession));
|
|
139
|
+
records.push(evidence);
|
|
140
|
+
if (resolved.records.evidence[0]) canonicalWrites.push([resolved.records.evidence[0].path, evidence]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const runtimeRoot = options.artifactRoot ? path.join(options.artifactRoot, "runtime") : "";
|
|
144
|
+
if (runtimeRoot && fs.existsSync(runtimeRoot)) {
|
|
145
|
+
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) });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const manifest = {
|
|
149
|
+
claim_boundary: "Scenario artifacts are evidence, not visual regression baselines or accessibility certification.",
|
|
150
|
+
environment: capture?.receipt.environment,
|
|
151
|
+
recorded_at: completedAt,
|
|
152
|
+
record_kind: "component_state_runtime_manifest",
|
|
153
|
+
records,
|
|
154
|
+
run,
|
|
155
|
+
schema_version: "1.0",
|
|
156
|
+
session: completedSession,
|
|
157
|
+
};
|
|
158
|
+
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 ?? "<manifest>" });
|
|
159
|
+
if (failures.length === 0 && options.writeCanonical) for (const [file, evidence] of canonicalWrites) fs.writeFileSync(file, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
160
|
+
if (failures.length === 0 && output) fs.writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
161
|
+
const result = { artifactCount: expectedFiles.size, failures, manifest: output, ok: failures.length === 0, profileCount: records.length, sessionId: capture?.receipt.session_id };
|
|
162
|
+
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
163
|
+
else if (result.ok) process.stdout.write(`finalized ${result.artifactCount} component state artifacts\n`);
|
|
164
|
+
else process.stderr.write(`${failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")}\n`);
|
|
165
|
+
if (!result.ok) process.exitCode = 1;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
addSchemaFindings,
|
|
8
|
+
canonicalSourceManifest,
|
|
9
|
+
compilePageEvidenceSchemas,
|
|
10
|
+
dirtyRelevantSources,
|
|
11
|
+
finding,
|
|
12
|
+
gitIdentity,
|
|
13
|
+
listArtifactFiles,
|
|
14
|
+
metadata,
|
|
15
|
+
normalizeReference,
|
|
16
|
+
PAGE_EVIDENCE_CLAIM,
|
|
17
|
+
PAGE_EVIDENCE_MANIFEST,
|
|
18
|
+
PAGE_EVIDENCE_RECEIPT,
|
|
19
|
+
readJsonFile,
|
|
20
|
+
readContainedBytes,
|
|
21
|
+
readReceipt,
|
|
22
|
+
resolveContained,
|
|
23
|
+
sameJson,
|
|
24
|
+
sameStringSet,
|
|
25
|
+
withinSession,
|
|
26
|
+
} from "./page-evidence-contract.mjs";
|
|
27
|
+
|
|
28
|
+
const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
|
+
const options = { artifactRoot: undefined, json: false, priorManifest: undefined, reviewBy: undefined, root: undefined, runnerResults: [] };
|
|
30
|
+
const failures = [];
|
|
31
|
+
|
|
32
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
33
|
+
const argument = process.argv[index];
|
|
34
|
+
if (argument === "--json") options.json = true;
|
|
35
|
+
else if (["--artifact-root", "--prior-manifest", "--review-by", "--root", "--runner-result"].includes(argument)) {
|
|
36
|
+
const value = process.argv[index + 1];
|
|
37
|
+
if (!value || value.startsWith("--")) failures.push(finding("argument_value_required", `${argument} requires a value`, "<cli>"));
|
|
38
|
+
else {
|
|
39
|
+
if (argument === "--artifact-root") options.artifactRoot = path.resolve(process.cwd(), value);
|
|
40
|
+
if (argument === "--prior-manifest") options.priorManifest = value;
|
|
41
|
+
if (argument === "--review-by") options.reviewBy = value;
|
|
42
|
+
if (argument === "--root") options.root = path.resolve(process.cwd(), value);
|
|
43
|
+
if (argument === "--runner-result") options.runnerResults.push(value);
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
} else failures.push(finding("argument_unknown", `unsupported argument ${argument}`, "<cli>"));
|
|
47
|
+
}
|
|
48
|
+
for (const [name, value] of [["--root", options.root], ["--artifact-root", options.artifactRoot], ["--review-by", options.reviewBy]]) {
|
|
49
|
+
if (!value) failures.push(finding("argument_value_required", `${name} is required`, "<cli>"));
|
|
50
|
+
}
|
|
51
|
+
if (options.runnerResults.length === 0) failures.push(finding("argument_value_required", "at least one --runner-result is required", "<cli>"));
|
|
52
|
+
|
|
53
|
+
let schemas;
|
|
54
|
+
try { schemas = compilePageEvidenceSchemas(path.join(styleGalleryRoot, "consumer-reference/schema")); }
|
|
55
|
+
catch (error) { failures.push(finding("page_evidence_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema")); }
|
|
56
|
+
|
|
57
|
+
let artifactReference;
|
|
58
|
+
let artifactFiles = [];
|
|
59
|
+
if (options.root && options.artifactRoot) {
|
|
60
|
+
artifactReference = normalizeReference(options.root, options.artifactRoot);
|
|
61
|
+
if (!artifactReference) failures.push(finding("page_evidence_artifact_escape", "artifact root must be inside the consumer repository", options.artifactRoot));
|
|
62
|
+
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));
|
|
63
|
+
else {
|
|
64
|
+
const resolved = resolveContained({ expectedType: "directory", prefix: "page_evidence_artifact", reference: artifactReference, root: options.root }, failures);
|
|
65
|
+
if (resolved) artifactFiles = listArtifactFiles(options.artifactRoot, failures);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const packetWithinLimits = !failures.some((entry) => entry.code === "page_evidence_artifact_limit");
|
|
69
|
+
const manifestFile = options.artifactRoot ? path.join(options.artifactRoot, PAGE_EVIDENCE_MANIFEST) : undefined;
|
|
70
|
+
if (manifestFile && fs.existsSync(manifestFile)) failures.push(finding("page_evidence_session_replay", "completed manifest already exists and cannot be overwritten", PAGE_EVIDENCE_MANIFEST));
|
|
71
|
+
const receiptRecord = options.artifactRoot && schemas && packetWithinLimits ? readReceipt(options.artifactRoot, schemas, failures) : undefined;
|
|
72
|
+
const receipt = receiptRecord?.valid ? receiptRecord : undefined;
|
|
73
|
+
|
|
74
|
+
const git = options.root ? gitIdentity(options.root, failures) : undefined;
|
|
75
|
+
if (receipt && git) {
|
|
76
|
+
if (git.revision !== receipt.value.revision) failures.push(finding("page_evidence_revision_mismatch", "checked-out revision differs from the start receipt", PAGE_EVIDENCE_RECEIPT));
|
|
77
|
+
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));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (receipt && options.root) {
|
|
81
|
+
const sourcePaths = receipt.value.source.files.map((entry) => entry.path);
|
|
82
|
+
const currentSource = canonicalSourceManifest(options.root, sourcePaths, failures);
|
|
83
|
+
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));
|
|
84
|
+
if (dirtyRelevantSources(options.root, sourcePaths).length > 0) failures.push(finding("page_evidence_source_drift", "relevant sources became dirty after session start", PAGE_EVIDENCE_RECEIPT));
|
|
85
|
+
const conformance = readJsonFile({ prefix: "page_evidence_conformance", reference: receipt.value.conformance_record.path, root: options.root }, failures);
|
|
86
|
+
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));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let prior;
|
|
90
|
+
if (options.priorManifest && options.root && schemas) {
|
|
91
|
+
const reference = normalizeReference(options.root, options.priorManifest);
|
|
92
|
+
if (!reference) failures.push(finding("page_evidence_reuse_manifest_escape", "prior manifest must be inside the consumer repository", options.priorManifest));
|
|
93
|
+
else {
|
|
94
|
+
prior = readJsonFile({ prefix: "page_evidence_reuse_manifest", reference, root: options.root }, failures);
|
|
95
|
+
if (prior) {
|
|
96
|
+
prior.sha256 = metadata(prior.bytes, "application/json", reference).sha256;
|
|
97
|
+
prior.valid = addSchemaFindings(schemas.manifest, prior.value, reference, "page_evidence_reuse_manifest_invalid", failures);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const runnerReferences = options.artifactRoot ? options.runnerResults.map((candidate) => normalizeReference(options.artifactRoot, candidate)) : [];
|
|
103
|
+
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", "<cli>"));
|
|
104
|
+
const completedAt = new Date().toISOString();
|
|
105
|
+
const scenarios = [];
|
|
106
|
+
const controlFiles = new Set([PAGE_EVIDENCE_MANIFEST, PAGE_EVIDENCE_RECEIPT, ...runnerReferences.filter(Boolean)]);
|
|
107
|
+
const expectedFiles = new Set([...controlFiles].filter((reference) => reference !== PAGE_EVIDENCE_MANIFEST));
|
|
108
|
+
const artifactOwners = new Map();
|
|
109
|
+
let canonicalRun;
|
|
110
|
+
|
|
111
|
+
for (const reference of packetWithinLimits ? runnerReferences.filter(Boolean) : []) {
|
|
112
|
+
const runner = readJsonFile({ prefix: "page_evidence_runner", reference, root: options.artifactRoot }, failures);
|
|
113
|
+
if (!runner || !schemas) continue;
|
|
114
|
+
if (!addSchemaFindings(schemas.runner, runner.value, reference, "page_evidence_runner_schema_invalid", failures)) continue;
|
|
115
|
+
const value = runner.value;
|
|
116
|
+
if (value.status !== "passed") failures.push(finding("page_evidence_runner_failed", `${value.scenario_id ?? reference} runner did not pass`, reference));
|
|
117
|
+
if (receipt) {
|
|
118
|
+
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));
|
|
119
|
+
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));
|
|
120
|
+
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));
|
|
121
|
+
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));
|
|
122
|
+
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));
|
|
123
|
+
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));
|
|
124
|
+
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));
|
|
125
|
+
}
|
|
126
|
+
const run = value.run ? { ...value.run, status: "passed" } : undefined;
|
|
127
|
+
if (canonicalRun && !sameJson(canonicalRun, run)) failures.push(finding("page_evidence_run_mismatch", "runner results mix run identities", reference));
|
|
128
|
+
else if (!canonicalRun) canonicalRun = run;
|
|
129
|
+
let evidence;
|
|
130
|
+
if (value.evidence?.kind === "captured") {
|
|
131
|
+
const artifacts = [];
|
|
132
|
+
for (const declaration of value.evidence.artifacts ?? []) {
|
|
133
|
+
if (controlFiles.has(declaration.path)) {
|
|
134
|
+
failures.push(finding("page_evidence_artifact_control_collision", "captured artifacts must be disjoint from manifest, receipt, and runner-result controls", declaration.path));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const owner = artifactOwners.get(declaration.path);
|
|
138
|
+
if (owner) failures.push(finding("page_evidence_artifact_reused", `${declaration.path} is shared by ${owner} and ${value.scenario_id}`, reference));
|
|
139
|
+
else artifactOwners.set(declaration.path, value.scenario_id);
|
|
140
|
+
const resolved = readContainedBytes({ prefix: "page_evidence_artifact", reference: declaration.path, root: options.artifactRoot }, failures);
|
|
141
|
+
if (!resolved) continue;
|
|
142
|
+
expectedFiles.add(declaration.path);
|
|
143
|
+
const artifact = metadata(resolved.bytes, declaration.media_type, declaration.path);
|
|
144
|
+
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));
|
|
145
|
+
artifacts.push(artifact);
|
|
146
|
+
}
|
|
147
|
+
evidence = { artifacts, kind: "captured" };
|
|
148
|
+
} else if (value.evidence?.kind === "reused") {
|
|
149
|
+
evidence = value.evidence;
|
|
150
|
+
if (!prior?.valid) failures.push(finding("page_evidence_reuse_manifest_required", "reused evidence requires a valid --prior-manifest", reference));
|
|
151
|
+
else {
|
|
152
|
+
const priorScenario = prior.value.scenarios?.find((scenario) => scenario.id === value.evidence.prior_scenario_id);
|
|
153
|
+
if (value.evidence.prior_manifest_sha256 !== prior.sha256) failures.push(finding("page_evidence_reuse_manifest_mismatch", "prior manifest digest does not match", reference));
|
|
154
|
+
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));
|
|
155
|
+
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));
|
|
156
|
+
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));
|
|
157
|
+
if (priorScenario && !sameJson(priorScenario.semantic_environment, value.semantic_environment)) failures.push(finding("page_evidence_reuse_environment_mismatch", "reuse requires the same semantic environment", reference));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
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 });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (receipt) {
|
|
164
|
+
const passedIds = scenarios.map((scenario) => scenario.id);
|
|
165
|
+
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));
|
|
166
|
+
}
|
|
167
|
+
if (packetWithinLimits && options.artifactRoot && fs.existsSync(options.artifactRoot)) {
|
|
168
|
+
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));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const completedSession = receipt ? {
|
|
172
|
+
...receipt.value,
|
|
173
|
+
completed_at: completedAt,
|
|
174
|
+
receipt: receipt.artifact,
|
|
175
|
+
receipt_sha256: receipt.digest,
|
|
176
|
+
} : undefined;
|
|
177
|
+
delete completedSession?.record_kind;
|
|
178
|
+
delete completedSession?.schema_version;
|
|
179
|
+
const manifest = receipt && canonicalRun ? {
|
|
180
|
+
claim_boundary: PAGE_EVIDENCE_CLAIM,
|
|
181
|
+
completed_at: completedAt,
|
|
182
|
+
record_kind: "page_evidence_manifest",
|
|
183
|
+
repository: receipt.value.repository,
|
|
184
|
+
review_by: options.reviewBy,
|
|
185
|
+
revision: receipt.value.revision,
|
|
186
|
+
run: canonicalRun,
|
|
187
|
+
scenarios,
|
|
188
|
+
schema_version: "1.0",
|
|
189
|
+
session: completedSession,
|
|
190
|
+
} : undefined;
|
|
191
|
+
if (manifest && schemas) addSchemaFindings(schemas.manifest, manifest, PAGE_EVIDENCE_MANIFEST, "page_evidence_manifest_schema_invalid", failures);
|
|
192
|
+
if (failures.length === 0 && manifestFile) fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
|
|
193
|
+
const result = { artifactCount: artifactOwners.size, failures, manifest: manifestFile, ok: failures.length === 0, scenarioCount: scenarios.length, sessionId: receipt?.value.session_id };
|
|
194
|
+
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
195
|
+
else if (result.ok) process.stdout.write(`finalized ${result.scenarioCount} page evidence scenarios\n`);
|
|
196
|
+
else process.stderr.write(`${failures.map((issue) => `${issue.code}: ${issue.path}: ${issue.message}`).join("\n")}\n`);
|
|
197
|
+
if (!result.ok) process.exitCode = 1;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveProfileRecords } from "./profile-record-contract.mjs";
|
|
7
|
+
|
|
8
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const options = { check: false, json: false, root: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local") };
|
|
10
|
+
const failures = [];
|
|
11
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
12
|
+
const argument = process.argv[index];
|
|
13
|
+
if (argument === "--json") options.json = true;
|
|
14
|
+
else if (argument === "--check") options.check = true;
|
|
15
|
+
else if (argument === "--root") {
|
|
16
|
+
const value = process.argv[index + 1];
|
|
17
|
+
if (!value) failures.push({ code: "argument_value_required", message: "--root requires a directory", path: "<cli>" });
|
|
18
|
+
else { options.root = path.resolve(value); index += 1; }
|
|
19
|
+
} else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "<cli>" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compare(left, right) {
|
|
23
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isSorted(values) {
|
|
27
|
+
return values.every((value, index) => index === 0 || compare(values[index - 1], value) <= 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function duplicateValues(values) {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
return [...new Set(values.filter((value) => seen.has(value) || !seen.add(value)))];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function escapeMarkdown(value) {
|
|
36
|
+
return String(value)
|
|
37
|
+
.replaceAll("&", "&")
|
|
38
|
+
.replaceAll("<", "<")
|
|
39
|
+
.replaceAll(">", ">")
|
|
40
|
+
.replaceAll("|", "|")
|
|
41
|
+
.replaceAll("`", "`")
|
|
42
|
+
.replace(/\r\n?|\n/g, "<br>");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function codeCell(value) {
|
|
46
|
+
return `<code>${escapeMarkdown(value)}</code>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function markdownTable(headers, rows) {
|
|
50
|
+
return [`| ${headers.map(escapeMarkdown).join(" | ")} |`, `| ${headers.map(() => "---").join(" | ")} |`, ...rows.map((row) => `| ${row.join(" | ")} |`)].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function frontmatter(title, description) {
|
|
54
|
+
return `---\ntype: Generated Evidence\ntitle: ${JSON.stringify(String(title))}\ndescription: ${JSON.stringify(String(description))}\n---\n\n<!-- Generated from canonical JSON. Do not edit. -->`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function navigation(parentLabel, parentPath, nextLabel, nextPath) {
|
|
58
|
+
return `## IA Navigation\n\nParent: [${parentLabel}](${parentPath}).\nNext: [${nextLabel}](${nextPath}).`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderStateMatrix(profile, states) {
|
|
62
|
+
const rows = [...states.scenarios].sort((a, b) => compare(a.id, b.id)).map((scenario) => [
|
|
63
|
+
codeCell(scenario.id),
|
|
64
|
+
escapeMarkdown(scenario.semantic_mode),
|
|
65
|
+
[...scenario.states].sort(compare).map(codeCell).join(", "),
|
|
66
|
+
Object.entries(scenario.aria).sort(([left], [right]) => compare(left, right)).map(([name, value]) => codeCell(`aria-${name}=${value}`)).join("<br>") || "none",
|
|
67
|
+
scenario.expected.visual.map(codeCell).join(", "),
|
|
68
|
+
escapeMarkdown(scenario.expected.activation),
|
|
69
|
+
]);
|
|
70
|
+
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`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderKeyboardMatrix(profile, component, fixture) {
|
|
74
|
+
const fixtureByMode = new Map(fixture.scenarios.map((scenario) => [scenario.semantic_mode, scenario.id]));
|
|
75
|
+
const rows = [...component.semantic_modes].sort((a, b) => compare(a.id, b.id)).map((mode) => [
|
|
76
|
+
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",
|
|
77
|
+
]);
|
|
78
|
+
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`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderEvidenceCoverage(profile, fixture, evidence) {
|
|
82
|
+
const rows = [...fixture.scenarios].sort((a, b) => compare(a.id, b.id)).map((scenario) => {
|
|
83
|
+
const passes = evidence.passes.filter((pass) => pass.scenario_id === scenario.id).sort((a, b) => compare(a.channel, b.channel));
|
|
84
|
+
return [
|
|
85
|
+
codeCell(scenario.id),
|
|
86
|
+
scenario.required_channels.map(codeCell).join(", "),
|
|
87
|
+
passes.map((pass) => codeCell(pass.channel)).join(", "),
|
|
88
|
+
String(passes.length),
|
|
89
|
+
passes.map((pass) => codeCell(pass.artifact.path)).join("<br>"),
|
|
90
|
+
];
|
|
91
|
+
});
|
|
92
|
+
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`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function generatedRowIds(content) {
|
|
96
|
+
return content.split("\n").map((line) => /^\| <code>([^<]+)<\/code> \|/.exec(line)?.[1]).filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function classifyDrift(file, expectedIds, expected, actual) {
|
|
100
|
+
const ids = generatedRowIds(actual);
|
|
101
|
+
for (const id of duplicateValues(ids)) failures.push({ code: "generated_duplicate_row", message: `${id} is duplicated`, path: file });
|
|
102
|
+
if (!isSorted(ids)) failures.push({ code: "generated_unsorted", message: "generated rows must be sorted", path: file });
|
|
103
|
+
const missing = expectedIds.filter((id) => !ids.includes(id));
|
|
104
|
+
for (const id of missing) failures.push({ code: "generated_missing_row", message: `${id} is missing`, path: file });
|
|
105
|
+
if (ids.length !== expectedIds.length) failures.push({ code: "generated_source_count_mismatch", message: `expected ${expectedIds.length} rows, found ${ids.length}`, path: file });
|
|
106
|
+
if (actual !== expected) failures.push({ code: "generated_stale", message: "generated table differs from canonical JSON", path: file });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeOrCheck(file, expectedIds, content) {
|
|
110
|
+
if (options.check) {
|
|
111
|
+
if (!fs.existsSync(file)) failures.push({ code: "generated_missing", message: "generated table is missing", path: file });
|
|
112
|
+
else classifyDrift(file, expectedIds, content, fs.readFileSync(file, "utf8"));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
116
|
+
fs.writeFileSync(file, content);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const profiles = fs.existsSync(options.root)
|
|
120
|
+
? 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)
|
|
121
|
+
: [];
|
|
122
|
+
let generated = 0;
|
|
123
|
+
for (const profile of profiles) {
|
|
124
|
+
const profileRoot = path.join(options.root, profile);
|
|
125
|
+
const resolved = resolveProfileRecords(profileRoot, failures);
|
|
126
|
+
if (!resolved) continue;
|
|
127
|
+
const component = resolved.records.component[0]?.value;
|
|
128
|
+
const fixture = resolved.records.fixture[0]?.value;
|
|
129
|
+
const states = resolved.records.states[0]?.value;
|
|
130
|
+
const evidence = resolved.records.evidence[0]?.value;
|
|
131
|
+
if (!component || !fixture || !states || !evidence) continue;
|
|
132
|
+
const scenarioIds = states.scenarios.map((scenario) => scenario.id);
|
|
133
|
+
for (const id of duplicateValues(scenarioIds)) failures.push({ code: "scenario_duplicate", message: `${id} is duplicated`, path: profileRoot });
|
|
134
|
+
if (!isSorted(scenarioIds)) failures.push({ code: "source_unsorted", message: "canonical scenario IDs must be sorted", path: profileRoot });
|
|
135
|
+
const fixtureIds = fixture.scenarios.map((scenario) => scenario.id);
|
|
136
|
+
for (const id of fixtureIds.filter((id) => !scenarioIds.includes(id))) failures.push({ code: "fixture_scenario_missing", message: `${id} has no canonical scenario`, path: profileRoot });
|
|
137
|
+
const outputs = new Map([
|
|
138
|
+
["state-matrix.md", [[...scenarioIds].sort(compare), renderStateMatrix(profile, states)]],
|
|
139
|
+
["keyboard-matrix.md", [component.semantic_modes.map((mode) => mode.id).sort(compare), renderKeyboardMatrix(profile, component, fixture)]],
|
|
140
|
+
["evidence-coverage.md", [[...fixtureIds].sort(compare), renderEvidenceCoverage(profile, fixture, evidence)]],
|
|
141
|
+
]);
|
|
142
|
+
for (const record of resolved.records.generated) {
|
|
143
|
+
const output = outputs.get(path.basename(record.path));
|
|
144
|
+
if (!output) { failures.push({ code: "generated_reference_unknown", message: `${record.reference} has no renderer`, path: resolved.profileFile }); continue; }
|
|
145
|
+
writeOrCheck(record.path, output[0], output[1]);
|
|
146
|
+
generated += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()];
|
|
150
|
+
const result = { checkedProfiles: profiles.length, failures: uniqueFailures, generated, mode: options.check ? "check" : "write", ok: uniqueFailures.length === 0, warnings: [] };
|
|
151
|
+
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
152
|
+
else if (result.ok) process.stdout.write(`${options.check ? "checked" : "generated"} ${generated} consumer reference evidence tables\n`);
|
|
153
|
+
else process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")}\n`);
|
|
154
|
+
if (!result.ok) process.exitCode = 1;
|