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,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
addSchemaFindings,
|
|
9
|
+
canonicalSourceManifest,
|
|
10
|
+
compilePageEvidenceSchemas,
|
|
11
|
+
dirtyRelevantSources,
|
|
12
|
+
finding,
|
|
13
|
+
gitIdentity,
|
|
14
|
+
metadata,
|
|
15
|
+
normalizeReference,
|
|
16
|
+
PAGE_EVIDENCE_RECEIPT,
|
|
17
|
+
readJsonFile,
|
|
18
|
+
resolveContained,
|
|
19
|
+
} from "./page-evidence-contract.mjs";
|
|
20
|
+
|
|
21
|
+
const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
const options = { artifactRoot: undefined, json: false, record: undefined, root: undefined };
|
|
23
|
+
const failures = [];
|
|
24
|
+
|
|
25
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
26
|
+
const argument = process.argv[index];
|
|
27
|
+
if (argument === "--json") options.json = true;
|
|
28
|
+
else if (["--artifact-root", "--record", "--root"].includes(argument)) {
|
|
29
|
+
const value = process.argv[index + 1];
|
|
30
|
+
if (!value || value.startsWith("--")) failures.push(finding("argument_value_required", `${argument} requires a value`, "<cli>"));
|
|
31
|
+
else {
|
|
32
|
+
if (argument === "--artifact-root") options.artifactRoot = path.resolve(process.cwd(), value);
|
|
33
|
+
if (argument === "--record") options.record = value;
|
|
34
|
+
if (argument === "--root") options.root = path.resolve(process.cwd(), value);
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
} else failures.push(finding("argument_unknown", `unsupported argument ${argument}`, "<cli>"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [name, value] of [["--root", options.root], ["--record", options.record], ["--artifact-root", options.artifactRoot]]) {
|
|
41
|
+
if (!value) failures.push(finding("argument_value_required", `${name} is required`, "<cli>"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let schemas;
|
|
45
|
+
try { schemas = compilePageEvidenceSchemas(path.join(styleGalleryRoot, "consumer-reference/schema")); }
|
|
46
|
+
catch (error) { failures.push(finding("page_evidence_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema")); }
|
|
47
|
+
|
|
48
|
+
let artifactReference;
|
|
49
|
+
if (options.root && options.artifactRoot) {
|
|
50
|
+
artifactReference = normalizeReference(options.root, options.artifactRoot);
|
|
51
|
+
if (!artifactReference) failures.push(finding("page_evidence_artifact_escape", "artifact root must be inside the consumer repository", options.artifactRoot));
|
|
52
|
+
else {
|
|
53
|
+
const resolved = resolveContained({ allowMissing: true, expectedType: "directory", prefix: "page_evidence_artifact", reference: artifactReference, root: options.root }, failures);
|
|
54
|
+
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));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let record;
|
|
59
|
+
let recordReference;
|
|
60
|
+
if (options.root && options.record) {
|
|
61
|
+
recordReference = normalizeReference(options.root, options.record);
|
|
62
|
+
if (!recordReference) failures.push(finding("page_evidence_conformance_escape", "conformance record must be inside the consumer repository", options.record));
|
|
63
|
+
else record = readJsonFile({ prefix: "page_evidence_conformance", reference: recordReference, root: options.root }, failures);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const consumer = record?.value?.consumer;
|
|
67
|
+
const sourcePaths = consumer?.relevant_sources;
|
|
68
|
+
const browserScenarios = Array.isArray(record?.value?.scenarios)
|
|
69
|
+
? record.value.scenarios.filter((scenario) => scenario?.evidence_method === "browser")
|
|
70
|
+
: [];
|
|
71
|
+
if (!consumer || typeof consumer.repository !== "string" || !/^[a-f0-9]{40}$/.test(consumer.revision ?? "") || !Array.isArray(sourcePaths)) {
|
|
72
|
+
failures.push(finding("page_evidence_conformance_invalid", "conformance record must declare consumer repository, revision, and relevant_sources", recordReference ?? "<conformance-record>"));
|
|
73
|
+
}
|
|
74
|
+
if (browserScenarios.length === 0) failures.push(finding("page_evidence_intent_missing", "conformance record must declare at least one browser scenario", recordReference ?? "<conformance-record>"));
|
|
75
|
+
|
|
76
|
+
const scenarioIds = browserScenarios.map((scenario) => scenario.id);
|
|
77
|
+
const sessionIds = new Set(browserScenarios.map((scenario) => scenario.session_id));
|
|
78
|
+
const runIds = new Set(browserScenarios.map((scenario) => scenario.run_id));
|
|
79
|
+
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 ?? "<conformance-record>"));
|
|
80
|
+
if (sessionIds.size !== 1 || runIds.size !== 1 || [...sessionIds, ...runIds].some((identity) => typeof identity !== "string" || identity.length === 0)) {
|
|
81
|
+
failures.push(finding("page_evidence_intent_identity_mismatch", "all page scenarios must use one nonempty session and run identity", recordReference ?? "<conformance-record>"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const git = options.root ? gitIdentity(options.root, failures) : undefined;
|
|
85
|
+
if (git && consumer) {
|
|
86
|
+
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));
|
|
87
|
+
if (git.repository && git.repository !== consumer.repository) failures.push(finding("page_evidence_repository_mismatch", "conformance repository differs from the Git remote identity", recordReference));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let source;
|
|
91
|
+
if (options.root && Array.isArray(sourcePaths)) {
|
|
92
|
+
source = canonicalSourceManifest(options.root, sourcePaths, failures);
|
|
93
|
+
if (source) {
|
|
94
|
+
const dirty = dirtyRelevantSources(options.root, sourcePaths);
|
|
95
|
+
if (dirty.length > 0) failures.push(finding("page_evidence_source_dirty", `relevant sources must be clean tracked files: ${dirty.join(", ")}`, options.root));
|
|
96
|
+
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));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const receipt = record && source && git && sessionIds.size === 1 && runIds.size === 1 ? {
|
|
101
|
+
attempt: Number(process.env.GITHUB_RUN_ATTEMPT ?? 1),
|
|
102
|
+
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || git.branch,
|
|
103
|
+
conformance_record: metadata(record.bytes, "application/json", recordReference),
|
|
104
|
+
environment: {
|
|
105
|
+
kind: "browser_evidence",
|
|
106
|
+
node: process.version,
|
|
107
|
+
platform: `${process.platform}/${process.arch}`,
|
|
108
|
+
producer: "stylegallery_page_evidence",
|
|
109
|
+
},
|
|
110
|
+
intended_scenario_ids: scenarioIds,
|
|
111
|
+
nonce: crypto.randomBytes(32).toString("hex"),
|
|
112
|
+
record_kind: "page_evidence_session",
|
|
113
|
+
repository: consumer.repository,
|
|
114
|
+
revision: consumer.revision,
|
|
115
|
+
run_id: [...runIds][0],
|
|
116
|
+
schema_version: "1.0",
|
|
117
|
+
session_id: [...sessionIds][0],
|
|
118
|
+
source,
|
|
119
|
+
started_at: new Date().toISOString(),
|
|
120
|
+
} : undefined;
|
|
121
|
+
if (receipt && schemas) addSchemaFindings(schemas.session, receipt, PAGE_EVIDENCE_RECEIPT, "page_evidence_session_schema_invalid", failures);
|
|
122
|
+
|
|
123
|
+
const receiptFile = options.artifactRoot ? path.join(options.artifactRoot, PAGE_EVIDENCE_RECEIPT) : undefined;
|
|
124
|
+
if (failures.length === 0 && receiptFile) {
|
|
125
|
+
fs.mkdirSync(options.artifactRoot, { recursive: true });
|
|
126
|
+
fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" });
|
|
127
|
+
}
|
|
128
|
+
const result = {
|
|
129
|
+
failures,
|
|
130
|
+
intendedScenarios: receipt?.intended_scenario_ids.length ?? 0,
|
|
131
|
+
ok: failures.length === 0,
|
|
132
|
+
receipt: receiptFile,
|
|
133
|
+
sessionId: receipt?.session_id,
|
|
134
|
+
sourceSha256: receipt?.source.sha256,
|
|
135
|
+
};
|
|
136
|
+
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
137
|
+
else if (result.ok) process.stdout.write(`created page evidence session ${result.sessionId}\n`);
|
|
138
|
+
else process.stderr.write(`${failures.map((issue) => `${issue.code}: ${issue.path}: ${issue.message}`).join("\n")}\n`);
|
|
139
|
+
if (!result.ok) process.exitCode = 1;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isOmoDependency, markdownLinkDestinations } from "./markdown-structure.mjs";
|
|
4
|
+
|
|
5
|
+
const generatedReference = /^design-engineering\/reference-profiles\/governed-local\/(?:editorial|terminal)\/generated\/(?:state-matrix|keyboard-matrix|evidence-coverage)\.md$/;
|
|
6
|
+
const ignoredDirectories = new Set([".git", ".omo", "node_modules"]);
|
|
7
|
+
|
|
8
|
+
function walkMarkdown(dir) {
|
|
9
|
+
if (!fs.existsSync(dir)) return [];
|
|
10
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
11
|
+
if (ignoredDirectories.has(entry.name)) return [];
|
|
12
|
+
const target = path.join(dir, entry.name);
|
|
13
|
+
if (entry.isDirectory()) return walkMarkdown(target);
|
|
14
|
+
return entry.isFile() && entry.name.endsWith(".md") ? [target] : [];
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function undeclaredDomainDocumentFailures(root, domains) {
|
|
19
|
+
return domains.flatMap((domain) => {
|
|
20
|
+
const declared = new Set([`${domain.slug}/index.md`, ...domain.leaves.map(([leaf]) => leaf), ...(domain.referenceDocuments ?? [])]);
|
|
21
|
+
return walkMarkdown(path.join(root, domain.slug)).flatMap((absolute) => {
|
|
22
|
+
const relative = path.relative(root, absolute);
|
|
23
|
+
if (generatedReference.test(relative) || declared.has(relative)) return [];
|
|
24
|
+
return [`${relative}: undeclared governed domain document`];
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function omoDependencyFailures(root) {
|
|
30
|
+
return walkMarkdown(root).flatMap((absolute) => {
|
|
31
|
+
const relative = path.relative(root, absolute);
|
|
32
|
+
const content = fs.readFileSync(absolute, "utf8");
|
|
33
|
+
if (markdownLinkDestinations(content).some(isOmoDependency)) return [`${relative}: tracked document must not depend on .omo`];
|
|
34
|
+
return [];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function collectDomainBoundaryFailures(root, domains) {
|
|
39
|
+
return [...undeclaredDomainDocumentFailures(root, domains), ...omoDependencyFailures(root)];
|
|
40
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { isOmoDependency, markdownLinkDestinations, stripFencedCodeBlocks, structuralMarkdown } from "./markdown-structure.mjs";
|
|
3
|
+
|
|
4
|
+
export function createDomainValidationChecks({
|
|
5
|
+
domains,
|
|
6
|
+
failures,
|
|
7
|
+
read,
|
|
8
|
+
referenceDocuments,
|
|
9
|
+
repository,
|
|
10
|
+
revision,
|
|
11
|
+
revisionPattern,
|
|
12
|
+
requiredLeafSections,
|
|
13
|
+
sourceOverrides,
|
|
14
|
+
}) {
|
|
15
|
+
function parseFrontmatter(relative, content) {
|
|
16
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
17
|
+
if (!match) {
|
|
18
|
+
failures.push(`${relative}: missing frontmatter`);
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
const metadata = {};
|
|
22
|
+
for (const rawLine of match[1].split("\n")) {
|
|
23
|
+
if (!rawLine.trim()) continue;
|
|
24
|
+
const separator = rawLine.indexOf(":");
|
|
25
|
+
if (separator === -1) {
|
|
26
|
+
failures.push(`${relative}: malformed frontmatter line`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const key = rawLine.slice(0, separator).trim();
|
|
30
|
+
const value = rawLine.slice(separator + 1).trim().replace(/^["']|["']$/g, "");
|
|
31
|
+
if (Object.hasOwn(metadata, key)) failures.push(`${relative}: duplicate frontmatter key ${key}`);
|
|
32
|
+
metadata[key] = value;
|
|
33
|
+
}
|
|
34
|
+
return metadata;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function checkIndex(domain) {
|
|
38
|
+
const relative = `${domain.slug}/index.md`;
|
|
39
|
+
const content = structuralMarkdown(read(relative));
|
|
40
|
+
if (!content) return;
|
|
41
|
+
if (!content.includes("## Scope Boundary")) failures.push(`${relative}: missing Scope Boundary section`);
|
|
42
|
+
if (!/^In scope:\s*\S/m.test(content)) failures.push(`${relative}: missing In scope declaration`);
|
|
43
|
+
if (!/^Out of scope:\s*\S/m.test(content)) failures.push(`${relative}: missing Out of scope declaration`);
|
|
44
|
+
if (!/^Parent: \[[^\]]+\]\([^)]+\)/m.test(content)) failures.push(`${relative}: missing Parent navigation link`);
|
|
45
|
+
if (!/^Next: \[[^\]]+\]\([^)]+\)/m.test(content)) failures.push(`${relative}: missing Next navigation link`);
|
|
46
|
+
for (const leaf of domain.leaves) {
|
|
47
|
+
const target = path.posix.relative(domain.slug, leaf.path);
|
|
48
|
+
if (!content.match(new RegExp(`\\[[^\\]]+\\]\\(${target.replaceAll(".", "\\.")}\\)`))) {
|
|
49
|
+
failures.push(`${relative}: missing leaf route ${target}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (domain.referenceDocuments?.length > 0 && !content.includes("[Reference Profiles](reference-profiles/index.md)")) {
|
|
53
|
+
failures.push(`${relative}: missing reference profile route`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkLeaf(domain, leaf, titles) {
|
|
58
|
+
const { path: relative, provenance, sourcePath: expectedSourcePath } = leaf;
|
|
59
|
+
const content = read(relative);
|
|
60
|
+
if (!content) return;
|
|
61
|
+
const metadata = parseFrontmatter(relative, content);
|
|
62
|
+
const requiredFields = ["type", "title", "description", "domain", "lifecycle"];
|
|
63
|
+
if (provenance === "external") requiredFields.push("source_repository", "source_path", "source_revision");
|
|
64
|
+
if (provenance === "local") requiredFields.push("provenance_kind");
|
|
65
|
+
for (const field of requiredFields) {
|
|
66
|
+
if (!metadata[field]) failures.push(`${relative}: missing ${field}`);
|
|
67
|
+
}
|
|
68
|
+
const knownDomain = domains.some((candidate) => candidate.slug === metadata.domain);
|
|
69
|
+
if (metadata.domain && !knownDomain) failures.push(`${relative}: unknown domain ${metadata.domain}`);
|
|
70
|
+
else if (metadata.domain && metadata.domain !== domain.slug) failures.push(`${relative}: domain ${metadata.domain} does not match ${domain.slug}`);
|
|
71
|
+
if (metadata.lifecycle && metadata.lifecycle !== "experimental") {
|
|
72
|
+
const kind = provenance === "local" ? "local leaf" : provenance === "external" ? "external adaptation" : "domain leaf";
|
|
73
|
+
failures.push(`${relative}: ${kind} lifecycle must be experimental`);
|
|
74
|
+
}
|
|
75
|
+
if (provenance === "external") {
|
|
76
|
+
const expectedSource = sourceOverrides[relative] ?? { repository, revision };
|
|
77
|
+
if (metadata.source_repository && metadata.source_repository !== expectedSource.repository) failures.push(`${relative}: unexpected source_repository ${metadata.source_repository}`);
|
|
78
|
+
if (metadata.source_path && metadata.source_path !== expectedSourcePath) failures.push(`${relative}: unexpected source_path ${metadata.source_path}`);
|
|
79
|
+
if (metadata.source_revision && !revisionPattern.test(metadata.source_revision)) {
|
|
80
|
+
failures.push(`${relative}: source_revision must be a full 40-character lowercase Git SHA`);
|
|
81
|
+
} else if (metadata.source_revision && metadata.source_revision !== expectedSource.revision) {
|
|
82
|
+
failures.push(`${relative}: unexpected source_revision ${metadata.source_revision}`);
|
|
83
|
+
}
|
|
84
|
+
} else if (provenance === "local") {
|
|
85
|
+
if (metadata.provenance_kind && metadata.provenance_kind !== "local") failures.push(`${relative}: local leaf provenance_kind must be local`);
|
|
86
|
+
for (const field of ["source_repository", "source_path", "source_revision"]) {
|
|
87
|
+
if (Object.hasOwn(metadata, field)) failures.push(`${relative}: local leaf must not declare ${field}`);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
for (const field of ["source_repository", "source_path", "source_revision"]) {
|
|
91
|
+
if (Object.hasOwn(metadata, field)) failures.push(`${relative}: locally authored leaf must omit ${field}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (metadata.title) {
|
|
95
|
+
if (titles.has(metadata.title)) failures.push(`${relative}: duplicate title ${metadata.title}`);
|
|
96
|
+
titles.add(metadata.title);
|
|
97
|
+
}
|
|
98
|
+
const body = structuralMarkdown(content);
|
|
99
|
+
for (const section of requiredLeafSections) {
|
|
100
|
+
if (!body.includes(`## ${section}`)) failures.push(`${relative}: missing ${section} section`);
|
|
101
|
+
}
|
|
102
|
+
if (!/^Parent: \[[^\]]+\]\([^)]+\)/m.test(body)) failures.push(`${relative}: missing Parent navigation link`);
|
|
103
|
+
if (!/^Next: \[[^\]]+\]\([^)]+\)/m.test(body)) failures.push(`${relative}: missing Next navigation link`);
|
|
104
|
+
if (markdownLinkDestinations(content).some(isOmoDependency)) failures.push(`${relative}: tracked document must not depend on .omo`);
|
|
105
|
+
if (metadata.lifecycle === "experimental" && /canonical universal policy/i.test(body)) {
|
|
106
|
+
failures.push(`${relative}: experimental document claims canonical authority`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function checkReferenceDocuments() {
|
|
111
|
+
const index = stripFencedCodeBlocks(read(referenceDocuments[0]));
|
|
112
|
+
if (!/Domain classification:\s*design-engineering\./i.test(index)) {
|
|
113
|
+
failures.push(`${referenceDocuments[0]}: reference profiles must remain in the Design Engineering domain`);
|
|
114
|
+
}
|
|
115
|
+
if (!index.includes("[Governed Local Profiles](governed-local/index.md)")) {
|
|
116
|
+
failures.push(`${referenceDocuments[0]}: missing governed-local route`);
|
|
117
|
+
}
|
|
118
|
+
const external = stripFencedCodeBlocks(read(referenceDocuments[2]));
|
|
119
|
+
if (!external.includes("Synthetic validator coverage only") || !external.includes("no durable adopter record")) {
|
|
120
|
+
failures.push(`${referenceDocuments[1]}: external adaptation must remain documentation-only`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkPromotionBoundary() {
|
|
125
|
+
const relative = "DOMAINS.md";
|
|
126
|
+
const content = stripFencedCodeBlocks(read(relative));
|
|
127
|
+
const required = [
|
|
128
|
+
"### Consumer Reference Promotion",
|
|
129
|
+
"applies only to consumer-local → shared-experimental invariant eligibility",
|
|
130
|
+
"Editorial and terminal are related examples in one fixture set",
|
|
131
|
+
"Shared stable has no numeric adoption threshold",
|
|
132
|
+
"Normative correctness may waive adoption count only",
|
|
133
|
+
"never silently relabeled experimental",
|
|
134
|
+
"Promotion records are JSON-only",
|
|
135
|
+
"zero adopter attestations",
|
|
136
|
+
];
|
|
137
|
+
for (const clause of required) if (!content.includes(clause)) failures.push(`${relative}: missing promotion boundary ${clause}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { checkIndex, checkLeaf, checkReferenceDocuments, checkPromotionBoundary };
|
|
141
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { artifactMetadata } from "./artifact-metadata.mjs";
|
|
4
|
+
import { sameJson, withinSession } from "./capture-session-contract.mjs";
|
|
5
|
+
import { parseStrictJson } from "./strict-json.mjs";
|
|
6
|
+
import { visualExpectationFor } from "./visual-expectation-contract.mjs";
|
|
7
|
+
|
|
8
|
+
function finding(code, file, message) {
|
|
9
|
+
return { code, message, path: file };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isInside(base, target) {
|
|
13
|
+
const relative = path.relative(base, target);
|
|
14
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSafeArtifactPath(reference) {
|
|
18
|
+
return typeof reference === "string"
|
|
19
|
+
&& reference.length > 0
|
|
20
|
+
&& !path.posix.isAbsolute(reference)
|
|
21
|
+
&& !path.win32.isAbsolute(reference)
|
|
22
|
+
&& !reference.includes("\\")
|
|
23
|
+
&& !reference.includes("?")
|
|
24
|
+
&& !reference.includes("#")
|
|
25
|
+
&& !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference)
|
|
26
|
+
&& path.posix.normalize(reference) === reference
|
|
27
|
+
&& !reference.split("/").some((segment) => segment === "." || segment === "..");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateIntegrity(pass, bytes, evidenceFile, failures) {
|
|
31
|
+
const actual = artifactMetadata(bytes, pass.artifact.media_type);
|
|
32
|
+
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`));
|
|
33
|
+
if (pass.channel === "visual") {
|
|
34
|
+
if (!actual.width || !actual.height) failures.push(finding("evidence_png_invalid", evidenceFile, `${pass.id} is not a valid PNG with IHDR dimensions`));
|
|
35
|
+
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`));
|
|
36
|
+
}
|
|
37
|
+
return actual;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function observedDomValue(document, key) {
|
|
41
|
+
if (key === "active") return String(document.active);
|
|
42
|
+
if (key === "role") return document.role;
|
|
43
|
+
return document.attributes?.[key];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateDom(document, pass, scenario, fixture, profileId, evidenceFile, failures) {
|
|
47
|
+
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) {
|
|
48
|
+
failures.push(finding("evidence_dom_identity_mismatch", evidenceFile, `${pass.id} DOM identity does not match its pass`));
|
|
49
|
+
}
|
|
50
|
+
if (document.activation_key !== fixture.activation_key) failures.push(finding("evidence_runtime_identity_mismatch", evidenceFile, `${pass.id} activation key differs from the canonical fixture`));
|
|
51
|
+
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`));
|
|
52
|
+
const visual = JSON.stringify([...(document.visual_states ?? [])].sort());
|
|
53
|
+
if (visual !== JSON.stringify([...scenario.expected.visual].sort())) failures.push(finding("evidence_dom_content_mismatch", evidenceFile, `${pass.id} visual states differ from canonical state`));
|
|
54
|
+
const expectedActivations = scenario.expected.activation === "allowed" ? 1 : 0;
|
|
55
|
+
if (document.activation_count !== expectedActivations) failures.push(finding("evidence_dom_content_mismatch", evidenceFile, `${pass.id} activation result differs from canonical state`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateAx(document, pass, scenario, fixture, profileId, evidenceFile, failures) {
|
|
59
|
+
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) {
|
|
60
|
+
failures.push(finding("evidence_ax_identity_mismatch", evidenceFile, `${pass.id} AX identity does not match its pass`));
|
|
61
|
+
}
|
|
62
|
+
if (document.role !== scenario.expected.ax.role) failures.push(finding("evidence_ax_content_mismatch", evidenceFile, `${pass.id} AX role differs from canonical state`));
|
|
63
|
+
for (const [key, expected] of Object.entries(scenario.expected.ax)) {
|
|
64
|
+
const actual = key === "role" ? document.role : document.properties?.[key];
|
|
65
|
+
if (actual !== expected) failures.push(finding("evidence_ax_content_mismatch", evidenceFile, `${pass.id} AX ${key} differs from canonical state`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateVisual(document, pass, scenario, profileId, actual, visualEnvironments, evidenceFile, failures) {
|
|
70
|
+
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) {
|
|
71
|
+
failures.push(finding("evidence_visual_identity_mismatch", evidenceFile, `${pass.id} visual identity does not match its pass`));
|
|
72
|
+
}
|
|
73
|
+
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`));
|
|
74
|
+
const expectedImage = { path: path.posix.basename(pass.artifact.path), ...actual };
|
|
75
|
+
if (!sameJson(document.image, expectedImage)) failures.push(finding("evidence_visual_image_mismatch", evidenceFile, `${pass.id} visual metadata does not match its PNG`));
|
|
76
|
+
try {
|
|
77
|
+
const expectation = visualExpectationFor(scenario, pass.environment, visualEnvironments);
|
|
78
|
+
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`));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
failures.push(finding("evidence_visual_expectation_invalid", evidenceFile, error instanceof Error ? error.message : String(error)));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArtifactJson(bytes, pass, scenario, fixture, profileId, evidenceFile, failures, schemas, session) {
|
|
85
|
+
let document;
|
|
86
|
+
try {
|
|
87
|
+
document = parseStrictJson(bytes.toString("utf8"));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
failures.push(finding("evidence_json_invalid", evidenceFile, `${pass.id} is not structured JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const validate = pass.channel === "dom" ? schemas.dom : pass.channel === "ax" ? schemas.ax : undefined;
|
|
93
|
+
if (validate && !validate(document)) {
|
|
94
|
+
const code = pass.channel === "dom" ? "evidence_dom_schema_invalid" : "evidence_ax_schema_invalid";
|
|
95
|
+
for (const error of validate.errors ?? []) failures.push(finding(code, evidenceFile, `${pass.id} ${error.instancePath || "/"} ${error.message}`));
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (!sameJson(document.capture_session, pass.session)) failures.push(finding("capture_session_mismatch", evidenceFile, `${pass.id} artifact session differs from its pass`));
|
|
99
|
+
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`));
|
|
100
|
+
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`));
|
|
101
|
+
if (pass.channel === "dom") validateDom(document, pass, scenario, fixture, profileId, evidenceFile, failures);
|
|
102
|
+
else if (pass.channel === "ax") validateAx(document, pass, scenario, fixture, profileId, evidenceFile, failures);
|
|
103
|
+
else failures.push(finding("evidence_channel_content_unknown", evidenceFile, `${pass.id} has no structured content contract`));
|
|
104
|
+
return document;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readArtifact(pass, artifactRoot, evidenceFile, failures) {
|
|
108
|
+
const reference = pass.artifact?.path;
|
|
109
|
+
if (!isSafeArtifactPath(reference)) {
|
|
110
|
+
failures.push(finding("evidence_artifact_escape", evidenceFile, `${pass.id} artifact path is unsafe`));
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const root = path.resolve(artifactRoot);
|
|
114
|
+
const target = path.resolve(root, reference);
|
|
115
|
+
if (!isInside(root, target) || !fs.existsSync(target)) {
|
|
116
|
+
failures.push(finding("evidence_artifact_missing", evidenceFile, `${pass.id} artifact is missing`));
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const metadata = fs.lstatSync(target);
|
|
120
|
+
if (metadata.isSymbolicLink()) {
|
|
121
|
+
failures.push(finding("evidence_artifact_symlink", evidenceFile, `${pass.id} artifact must not be a symlink`));
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (!metadata.isFile()) {
|
|
125
|
+
failures.push(finding("evidence_artifact_type_invalid", evidenceFile, `${pass.id} artifact must be a regular file`));
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const realRoot = fs.realpathSync(root);
|
|
129
|
+
const realTarget = fs.realpathSync(target);
|
|
130
|
+
if (!isInside(realRoot, realTarget) || realTarget !== path.join(realRoot, reference)) {
|
|
131
|
+
failures.push(finding("evidence_artifact_redirect", evidenceFile, `${pass.id} artifact resolves through a redirect`));
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return fs.readFileSync(realTarget);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, visualEnvironments, evidenceFile, failures, schemas, session) {
|
|
138
|
+
const reference = pass.artifact.path.replace(/\.png$/, ".visual.json");
|
|
139
|
+
const bytes = readArtifact({ artifact: { path: reference }, id: `${pass.id}-metadata` }, artifactRoot, evidenceFile, failures);
|
|
140
|
+
if (!bytes) return undefined;
|
|
141
|
+
let document;
|
|
142
|
+
try {
|
|
143
|
+
document = parseStrictJson(bytes.toString("utf8"));
|
|
144
|
+
} catch (error) {
|
|
145
|
+
failures.push(finding("evidence_visual_json_invalid", evidenceFile, `${pass.id} visual metadata is not structured JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
if (!schemas.visual(document)) {
|
|
149
|
+
for (const error of schemas.visual.errors ?? []) failures.push(finding("evidence_visual_schema_invalid", evidenceFile, `${pass.id} ${error.instancePath || "/"} ${error.message}`));
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (!sameJson(document.capture_session, pass.session)) failures.push(finding("capture_session_mismatch", evidenceFile, `${pass.id} visual metadata session differs from its pass`));
|
|
153
|
+
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`));
|
|
154
|
+
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`));
|
|
155
|
+
validateVisual(document, pass, scenario, profileId, actual, visualEnvironments, evidenceFile, failures);
|
|
156
|
+
return document;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function validateEvidenceArtifacts({ artifactRoot, evidence, evidenceFile, fixture, profileId, schemas, session, states }) {
|
|
160
|
+
const failures = [];
|
|
161
|
+
const scenarios = new Map(states.scenarios.map((scenario) => [scenario.id, scenario]));
|
|
162
|
+
const fixtures = new Map(fixture.scenarios.map((scenario) => [scenario.id, scenario]));
|
|
163
|
+
const capturedTimes = new Map();
|
|
164
|
+
for (const pass of evidence.passes ?? []) {
|
|
165
|
+
const scenario = scenarios.get(pass.scenario_id);
|
|
166
|
+
const runtimeFixture = fixtures.get(pass.scenario_id);
|
|
167
|
+
if (!scenario || !runtimeFixture || !pass.artifact?.media_type) continue;
|
|
168
|
+
const bytes = readArtifact(pass, artifactRoot, evidenceFile, failures);
|
|
169
|
+
if (!bytes) continue;
|
|
170
|
+
const actual = validateIntegrity(pass, bytes, evidenceFile, failures);
|
|
171
|
+
if (pass.artifact.media_type === "application/json") {
|
|
172
|
+
const document = parseArtifactJson(bytes, pass, scenario, runtimeFixture, profileId, evidenceFile, failures, schemas, session);
|
|
173
|
+
if (document?.captured_at) {
|
|
174
|
+
if (!capturedTimes.has(pass.scenario_id)) capturedTimes.set(pass.scenario_id, new Set());
|
|
175
|
+
capturedTimes.get(pass.scenario_id).add(document.captured_at);
|
|
176
|
+
}
|
|
177
|
+
} else if (pass.channel === "visual") {
|
|
178
|
+
const document = parseVisualMetadata(pass, artifactRoot, scenario, profileId, actual, states.visual_environments, evidenceFile, failures, schemas, session);
|
|
179
|
+
if (document?.captured_at) {
|
|
180
|
+
if (!capturedTimes.has(pass.scenario_id)) capturedTimes.set(pass.scenario_id, new Set());
|
|
181
|
+
capturedTimes.get(pass.scenario_id).add(document.captured_at);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const [scenarioId, times] of capturedTimes) {
|
|
186
|
+
if (times.size !== 1) failures.push(finding("capture_session_time_mismatch", evidenceFile, `${scenarioId} visual, DOM, and AX captured_at values differ`));
|
|
187
|
+
const capturedAt = [...times][0];
|
|
188
|
+
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`));
|
|
189
|
+
}
|
|
190
|
+
return failures;
|
|
191
|
+
}
|