stylegallery 0.1.0 → 0.1.2
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 +4 -1
- package/DOMAINS.md +2 -6
- package/GOVERNANCE.md +10 -9
- package/README.md +29 -127
- package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
- package/consumer-reference/agent-native/v2/material-registry.json +1 -0
- package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
- package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
- package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
- package/consumer-reference/contract.md +1 -1
- package/consumer-reference/index.md +5 -3
- package/layout/index.md +124 -0
- package/log.md +57 -0
- package/package.json +183 -22
- package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
- package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
- package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
- package/scripts/agent-native/v2/material-admission.mjs +295 -0
- package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
- package/scripts/agent-native/v2/material-context.mjs +251 -0
- package/scripts/agent-native/v2/material-identity.mjs +71 -0
- package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
- package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
- package/scripts/agent-native/v2/material-queries.mjs +398 -0
- package/scripts/agent-native/v2/material-registry.mjs +319 -0
- package/scripts/sg-material-mcp.mjs +98 -0
- package/scripts/sg-material.mjs +27 -0
- package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
- package/consumer-reference/baselines/calibration.json +0 -260
- package/consumer-reference/baselines/manifest.json +0 -21
- package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
- package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
- package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
- package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
- package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
- package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
- package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
- package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
- package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
- package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
- package/consumer-reference/fixtures/promotion/manifest.json +0 -15
- package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
- package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
- package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
- package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
- package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
- package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
- package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
- package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
- package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
- package/consumer-reference/fixtures/valid-deprecated.json +0 -21
- package/consumer-reference/fixtures/valid-experimental.json +0 -19
- package/consumer-reference/generated/manifest.json +0 -40
- package/consumer-reference/generated/tokens.css +0 -12
- package/consumer-reference/policies/shared-experimental.json +0 -37
- package/consumer-reference/schema/ax-evidence.schema.json +0 -30
- package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
- package/consumer-reference/schema/calibration-record.schema.json +0 -190
- package/consumer-reference/schema/capture-session.schema.json +0 -161
- package/consumer-reference/schema/component-state.schema.json +0 -150
- package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
- package/consumer-reference/schema/dom-evidence.schema.json +0 -38
- package/consumer-reference/schema/evidence-record.schema.json +0 -81
- package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
- package/consumer-reference/schema/item.schema.json +0 -188
- package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
- package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
- package/consumer-reference/schema/portable-tokens.schema.json +0 -104
- package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
- package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
- package/consumer-reference/schema/visual-evidence.schema.json +0 -31
- package/design-engineering/consumer-migration-readiness.md +0 -124
- package/design-engineering/interface-craft.md +0 -107
- package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
- package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
- package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
- package/game-ui/classification.md +0 -82
- package/game-ui/reference-record.md +0 -62
- package/game-ui/screen-hierarchy.md +0 -107
- package/game-ui/unity/architecture.md +0 -166
- package/game-ui/unity/cli-loop.md +0 -84
- package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
- package/game-ui/unity/org-wiki.md +0 -146
- package/game-ui/unity/repository-map.md +0 -102
- package/game-ui/unity/ui-systems.md +0 -85
- package/motion/practice-reference.md +0 -145
- package/motion/review-workflow.md +0 -86
- package/motion/vocabulary.md +0 -82
- package/platform-guides/apple-interaction.md +0 -89
- package/scripts/agent-native/events.mjs +0 -113
- package/scripts/agent-native/immutable-store.mjs +0 -102
- package/scripts/artifact-metadata.mjs +0 -20
- package/scripts/audit-evidence-freshness.mjs +0 -308
- package/scripts/baseline-contract.mjs +0 -189
- package/scripts/baseline-schema-parity.mjs +0 -40
- package/scripts/build-reference-artifacts.mjs +0 -127
- package/scripts/calibration-raw-contract.mjs +0 -172
- package/scripts/capture-session-contract.mjs +0 -187
- package/scripts/component-state-artifact-fixture.mjs +0 -44
- package/scripts/component-state-contract.mjs +0 -99
- package/scripts/component-state-semantics.mjs +0 -128
- package/scripts/component-state-workflow-contract.mjs +0 -67
- package/scripts/component-state-workflow-negative-cases.mjs +0 -64
- package/scripts/consumer-conformance-contract.mjs +0 -198
- package/scripts/consumer-evidence-governance-contract.mjs +0 -210
- package/scripts/consumer-reference-case-runner.mjs +0 -93
- package/scripts/consumer-reference-schema.mjs +0 -155
- package/scripts/create-component-state-session.mjs +0 -100
- package/scripts/create-page-evidence-session.mjs +0 -139
- package/scripts/domain-document-boundaries.mjs +0 -40
- package/scripts/domain-validation-checks.mjs +0 -141
- package/scripts/evidence-artifact-contract.mjs +0 -191
- package/scripts/finalize-component-state-evidence.mjs +0 -165
- package/scripts/finalize-page-evidence.mjs +0 -197
- package/scripts/generate-consumer-reference-evidence.mjs +0 -154
- package/scripts/generate-patterns.mjs +0 -218
- package/scripts/governance-matrix-contract.mjs +0 -73
- package/scripts/governance-matrix-negative-cases.mjs +0 -114
- package/scripts/governance-policy-contract.mjs +0 -49
- package/scripts/governance-test-fixture.mjs +0 -321
- package/scripts/governance-workflow-negative-cases.mjs +0 -241
- package/scripts/governed-profile-registry.mjs +0 -125
- package/scripts/json-schema-formats.mjs +0 -31
- package/scripts/markdown-structure.mjs +0 -45
- package/scripts/page-artifact-metadata.mjs +0 -124
- package/scripts/page-evidence-contract.mjs +0 -297
- package/scripts/page-evidence-fixture.mjs +0 -271
- package/scripts/pattern-data.mjs +0 -325
- package/scripts/profile-record-contract.mjs +0 -140
- package/scripts/promotion-attestation-contract.mjs +0 -91
- package/scripts/promotion-boundary-test-contract.mjs +0 -92
- package/scripts/promotion-fixture-inventory.mjs +0 -109
- package/scripts/promotion-governance-contract.mjs +0 -28
- package/scripts/reference-artifact-path-contract.mjs +0 -28
- package/scripts/reference-profile-contract.mjs +0 -114
- package/scripts/reference-token-contract.mjs +0 -156
- package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
- package/scripts/summarize-sentinel-calibration.mjs +0 -98
- package/scripts/test-agent-adapter-conformance.mjs +0 -250
- package/scripts/test-agent-conformance-receipt.mjs +0 -106
- package/scripts/test-agent-events.mjs +0 -81
- package/scripts/test-agent-execution-effects.mjs +0 -114
- package/scripts/test-agent-execution-fixtures.mjs +0 -140
- package/scripts/test-agent-execution-kernel.mjs +0 -39
- package/scripts/test-agent-execution-learning.mjs +0 -130
- package/scripts/test-agent-execution-lifecycle.mjs +0 -143
- package/scripts/test-agent-execution-safety.mjs +0 -158
- package/scripts/test-agent-execution-views.mjs +0 -117
- package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
- package/scripts/test-agent-knowledge-kernel.mjs +0 -183
- package/scripts/test-agent-learning.mjs +0 -246
- package/scripts/test-agent-mcp.mjs +0 -246
- package/scripts/test-agent-native-schemas.mjs +0 -215
- package/scripts/test-agent-projections.mjs +0 -249
- package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
- package/scripts/test-agent-registry-mutations.mjs +0 -110
- package/scripts/test-audit-evidence-freshness.mjs +0 -177
- package/scripts/test-component-state-sentinel.mjs +0 -35
- package/scripts/test-component-state-source-contract.mjs +0 -83
- package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
- package/scripts/test-consumer-reference-sentinel.mjs +0 -31
- package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
- package/scripts/test-json-cli-output.mjs +0 -151
- package/scripts/test-json-schema-formats.mjs +0 -40
- package/scripts/test-reference-adapters.mjs +0 -199
- package/scripts/test-renderer-purity.mjs +0 -34
- package/scripts/test-sg-cli.mjs +0 -129
- package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
- package/scripts/test-validate-baseline-manifest.mjs +0 -234
- package/scripts/test-validate-component-state-artifacts.mjs +0 -243
- package/scripts/test-validate-component-state.mjs +0 -106
- package/scripts/test-validate-consumer-conformance.mjs +0 -427
- package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
- package/scripts/test-validate-consumer-reference.mjs +0 -263
- package/scripts/test-validate-domains.mjs +0 -357
- package/scripts/test-validate-governance.mjs +0 -292
- package/scripts/test-validate-ia.mjs +0 -262
- package/scripts/test-validate-okf.mjs +0 -73
- package/scripts/test-validate-page-evidence.mjs +0 -302
- package/scripts/test-validate-patterns.mjs +0 -157
- package/scripts/test-validate-promotion-rfc.mjs +0 -250
- package/scripts/test-validate-reference-profiles.mjs +0 -170
- package/scripts/test-validate-unity-org-wiki.mjs +0 -57
- package/scripts/test-validate-unity-source-contracts.mjs +0 -67
- package/scripts/test-validate-vocabulary.mjs +0 -174
- package/scripts/test-validate-webpage-workflow.mjs +0 -293
- package/scripts/validate-baseline-manifest.mjs +0 -44
- package/scripts/validate-catalog.mjs +0 -65
- package/scripts/validate-component-state.mjs +0 -141
- package/scripts/validate-consumer-conformance.mjs +0 -477
- package/scripts/validate-consumer-reference.mjs +0 -288
- package/scripts/validate-domains.mjs +0 -237
- package/scripts/validate-governance.mjs +0 -248
- package/scripts/validate-ia.mjs +0 -136
- package/scripts/validate-links.mjs +0 -66
- package/scripts/validate-okf.mjs +0 -104
- package/scripts/validate-page-evidence.mjs +0 -191
- package/scripts/validate-patterns.mjs +0 -246
- package/scripts/validate-promotion-rfc.mjs +0 -224
- package/scripts/validate-reference-artifacts.mjs +0 -125
- package/scripts/validate-renderer-purity.mjs +0 -56
- package/scripts/validate-unity-org-wiki.mjs +0 -146
- package/scripts/validate-unity-source-contracts.mjs +0 -107
- package/scripts/validate-vocabulary.mjs +0 -115
- package/scripts/validate-webpage-workflow.mjs +0 -254
- package/scripts/visual-expectation-contract.mjs +0 -20
- package/scripts/workflow-action-contract.mjs +0 -163
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
function finding(code, file, message) {
|
|
2
|
-
return { code, message, path: file };
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function duplicateValues(values, normalize = (value) => value) {
|
|
6
|
-
const seen = new Set();
|
|
7
|
-
const duplicates = new Set();
|
|
8
|
-
for (const value of values) {
|
|
9
|
-
const key = normalize(value);
|
|
10
|
-
if (seen.has(key)) duplicates.add(key);
|
|
11
|
-
seen.add(key);
|
|
12
|
-
}
|
|
13
|
-
return [...duplicates];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function sameSet(left, right) {
|
|
17
|
-
return JSON.stringify([...(left ?? [])].sort()) === JSON.stringify([...(right ?? [])].sort());
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function validateApplicability(component, file, failures) {
|
|
21
|
-
for (const id of duplicateValues((component.semantic_modes ?? []).map((mode) => mode.id))) failures.push(finding("semantic_mode_duplicate", file, `semantic mode ${id} is duplicated`));
|
|
22
|
-
for (const mode of component.semantic_modes ?? []) {
|
|
23
|
-
for (const name of duplicateValues((mode.aria_states ?? []).map((state) => state.name))) failures.push(finding("aria_state_duplicate", file, `${mode.id} repeats ${name}`));
|
|
24
|
-
for (const state of mode.aria_states ?? []) {
|
|
25
|
-
if (["not_applicable", "prohibited"].includes(state.status) && !state.reason) failures.push(finding("applicability_reason_required", file, `${mode.id}.${state.name} requires a reason`));
|
|
26
|
-
if (state.status === "conditional" && (!state.condition || !state.resolution)) failures.push(finding("conditional_unresolved", file, `${mode.id}.${state.name} requires condition and resolution`));
|
|
27
|
-
if (state.status === "conditional" && ["not_applicable", "prohibited"].includes(state.resolution) && !state.reason) failures.push(finding("applicability_reason_required", file, `${mode.id}.${state.name} requires a resolution reason`));
|
|
28
|
-
}
|
|
29
|
-
const required = mode.id === "toggle" ? "pressed" : mode.id === "disclosure" ? "expanded" : mode.id === "switch" ? "checked" : undefined;
|
|
30
|
-
const state = (mode.aria_states ?? []).find((candidate) => candidate.name === required);
|
|
31
|
-
if (required && (!state || !["required", "conditional"].includes(state.status) || (state.status === "conditional" && state.resolution !== "required"))) {
|
|
32
|
-
failures.push(finding("role_required_aria_state", file, `${mode.id} requires applicable aria-${required}`));
|
|
33
|
-
}
|
|
34
|
-
if (mode.id === "switch" && (mode.aria_states ?? []).find((state) => state.name === "checked")?.values?.includes("mixed")) {
|
|
35
|
-
failures.push(finding("switch_mixed_prohibited", file, "switch aria-checked must not include mixed"));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function validateSurface(scenario, state, visual, aria, dom, ax, code, failures, file) {
|
|
41
|
-
const present = new Set(scenario.states ?? []).has(state);
|
|
42
|
-
const surfaces = [
|
|
43
|
-
(scenario.expected?.visual ?? []).includes(visual),
|
|
44
|
-
aria ? scenario.aria?.[aria] === "true" : present,
|
|
45
|
-
dom ? scenario.expected?.dom?.[dom] === "true" : present,
|
|
46
|
-
ax ? scenario.expected?.ax?.[ax] === true : present,
|
|
47
|
-
];
|
|
48
|
-
if (surfaces.some((value) => value !== present)) failures.push(finding(code, file, `${scenario.id} ${state} surfaces disagree`));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function validateComponentSemantics(component, file, failures) {
|
|
52
|
-
validateApplicability(component, file, failures);
|
|
53
|
-
if (component.versions?.delivery_channel === "stable" && component.versions.lifecycle !== "stable") failures.push(finding("preview_rolled_stable", file, "preview components cannot be relabeled stable"));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function validateStateSemantics(states, component, file, failures) {
|
|
57
|
-
const scenarios = states.scenarios ?? [];
|
|
58
|
-
const visualEnvironments = states.visual_environments ?? [];
|
|
59
|
-
const visualEnvironmentIds = new Set(visualEnvironments.map((environment) => environment.id));
|
|
60
|
-
for (const id of duplicateValues(visualEnvironments.map((environment) => environment.id))) failures.push(finding("visual_environment_duplicate", file, `visual environment ${id} is duplicated`));
|
|
61
|
-
for (const selector of duplicateValues(visualEnvironments, (environment) => JSON.stringify(Object.entries(environment).filter(([key]) => key !== "id").sort()))) failures.push(finding("visual_environment_selector_duplicate", file, `visual environment selector ${selector} is duplicated`));
|
|
62
|
-
for (const id of duplicateValues(scenarios.map((scenario) => scenario.id))) failures.push(finding("scenario_duplicate", file, `scenario ${id} is duplicated`));
|
|
63
|
-
for (const key of duplicateValues(scenarios, (scenario) => [...(scenario.states ?? [])].sort().join("\u0000"))) failures.push(finding("state_set_duplicate", file, `state set ${key} is duplicated`));
|
|
64
|
-
const modes = new Map((component.semantic_modes ?? []).map((mode) => [mode.id, mode]));
|
|
65
|
-
for (const scenario of scenarios) {
|
|
66
|
-
const visualImages = scenario.expected?.visual_image ?? [];
|
|
67
|
-
for (const id of duplicateValues(visualImages.map((image) => image.environment_id))) failures.push(finding("visual_expectation_duplicate", file, `${scenario.id} repeats visual expectation ${id}`));
|
|
68
|
-
for (const image of visualImages) if (!visualEnvironmentIds.has(image.environment_id)) failures.push(finding("visual_environment_unknown", file, `${scenario.id} references unknown visual environment ${image.environment_id}`));
|
|
69
|
-
for (const id of visualEnvironmentIds) if (!visualImages.some((image) => image.environment_id === id)) failures.push(finding("visual_expectation_missing", file, `${scenario.id} lacks visual expectation ${id}`));
|
|
70
|
-
const stateSet = new Set(scenario.states ?? []);
|
|
71
|
-
const mode = modes.get(scenario.semantic_mode);
|
|
72
|
-
if (!mode) failures.push(finding("scenario_mode_unknown", file, `${scenario.id} references an unknown semantic mode`));
|
|
73
|
-
if (scenario.expected?.dom?.role !== mode?.role || scenario.expected?.ax?.role !== mode?.role) failures.push(finding("role_surface_mismatch", file, `${scenario.id} role surfaces disagree with its semantic mode`));
|
|
74
|
-
validateSurface(scenario, "focus", "focus", undefined, "active", "focused", "focus_surface_mismatch", failures, file);
|
|
75
|
-
validateSurface(scenario, "pressed", "pressed", "pressed", "aria-pressed", "pressed", "pressed_surface_mismatch", failures, file);
|
|
76
|
-
validateSurface(scenario, "expanded", "expanded", "expanded", "aria-expanded", "expanded", "expanded_surface_mismatch", failures, file);
|
|
77
|
-
validateSurface(scenario, "busy", "busy", "busy", "aria-busy", "busy", "busy_surface_mismatch", failures, file);
|
|
78
|
-
validateSurface(scenario, "disabled", "disabled", "disabled", "disabled", "disabled", "disabled_surface_mismatch", failures, file);
|
|
79
|
-
if (stateSet.has("loading") && (!stateSet.has("busy") || !(scenario.expected?.visual ?? []).includes("loading"))) failures.push(finding("loading_busy_state_required", file, `${scenario.id} loading requires distinct visible busy state`));
|
|
80
|
-
if (stateSet.has("loading") && (scenario.expected?.visual ?? []).includes("disabled") && !stateSet.has("disabled")) failures.push(finding("loading_disabled_conflated", file, `${scenario.id} renders loading as disabled`));
|
|
81
|
-
if (stateSet.has("disabled") && scenario.expected?.activation !== "suppressed") failures.push(finding("disabled_activation", file, `${scenario.id} disabled activation must be suppressed`));
|
|
82
|
-
if (!stateSet.has("disabled") && !stateSet.has("loading") && scenario.expected?.activation === "suppressed") failures.push(finding("normative_activation_conflict", file, `${scenario.id} suppresses activation without a suppressing state`));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function validateFixtureSemantics(fixture, states, component, file, failures) {
|
|
87
|
-
const canonical = new Map((states.scenarios ?? []).map((scenario) => [scenario.id, scenario]));
|
|
88
|
-
const modes = new Map((component.semantic_modes ?? []).map((mode) => [mode.id, mode]));
|
|
89
|
-
for (const id of duplicateValues((fixture.scenarios ?? []).map((scenario) => scenario.id))) failures.push(finding("fixture_scenario_duplicate", file, `fixture scenario ${id} is duplicated`));
|
|
90
|
-
for (const scenario of fixture.scenarios ?? []) {
|
|
91
|
-
const source = canonical.get(scenario.id);
|
|
92
|
-
if (!source) failures.push(finding("fixture_scenario_missing", file, `${scenario.id} has no canonical state scenario`));
|
|
93
|
-
else if (source.semantic_mode !== scenario.semantic_mode) failures.push(finding("normative_mode_conflict", file, `${scenario.id} semantic modes disagree`));
|
|
94
|
-
if (!modes.get(scenario.semantic_mode)?.keys?.includes(scenario.activation_key)) failures.push(finding("fixture_activation_key_invalid", file, `${scenario.id} activation key is not declared by its mode`));
|
|
95
|
-
}
|
|
96
|
-
for (const id of canonical.keys()) if (!(fixture.scenarios ?? []).some((scenario) => scenario.id === id)) failures.push(finding("fixture_scenario_unexercised", file, `${id} lacks a runtime fixture`));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function validateEvidenceSemantics(evidence, fixture, states, file, failures) {
|
|
100
|
-
if (Object.hasOwn(evidence, "aggregate_pass") || Object.hasOwn(evidence, "certification")) failures.push(finding("aggregate_pass_forbidden", file, "aggregate pass and certification claims are forbidden"));
|
|
101
|
-
const canonical = new Map((states.scenarios ?? []).map((scenario) => [scenario.id, scenario]));
|
|
102
|
-
const fixtures = new Map((fixture.scenarios ?? []).map((scenario) => [scenario.id, scenario]));
|
|
103
|
-
const passes = evidence.passes ?? [];
|
|
104
|
-
for (const id of duplicateValues(passes.map((pass) => pass.id))) failures.push(finding("evidence_pass_duplicate", file, `evidence pass ${id} is duplicated`));
|
|
105
|
-
for (const key of duplicateValues(passes, (pass) => `${pass.scenario_id}\u0000${pass.channel}`)) failures.push(finding("evidence_channel_duplicate", file, `scenario channel ${key} is duplicated`));
|
|
106
|
-
for (const artifact of duplicateValues(passes.map((pass) => pass.artifact?.path).filter(Boolean))) failures.push(finding("evidence_artifact_reused", file, `artifact ${artifact} is reused`));
|
|
107
|
-
for (const digest of duplicateValues(passes.map((pass) => pass.artifact?.sha256).filter(Boolean))) failures.push(finding("evidence_artifact_content_reused", file, `artifact content ${digest} is reused`));
|
|
108
|
-
const runIdentities = new Set(passes.map((pass) => JSON.stringify(pass.run)));
|
|
109
|
-
const environments = new Set(passes.map((pass) => JSON.stringify(pass.environment)));
|
|
110
|
-
if (runIdentities.size > 1 || environments.size > 1) failures.push(finding("evidence_runtime_identity_mismatch", file, "passes must share one actual run and environment"));
|
|
111
|
-
const sessions = new Set(passes.map((pass) => JSON.stringify(pass.session)));
|
|
112
|
-
if (sessions.size > 1) failures.push(finding("capture_session_mismatch", file, "passes must share one capture session"));
|
|
113
|
-
for (const pass of passes) {
|
|
114
|
-
const source = canonical.get(pass.scenario_id);
|
|
115
|
-
const scenario = fixtures.get(pass.scenario_id);
|
|
116
|
-
if (!source || !scenario) failures.push(finding("evidence_scenario_unknown", file, `${pass.id} references unknown scenario ${pass.scenario_id}`));
|
|
117
|
-
if (pass.channel === "at" || pass.environment?.kind === "assistive_technology") failures.push(finding("at_evidence_unverified", file, `${pass.id} self-asserts an AT run without external attestation`));
|
|
118
|
-
if (pass.session && (pass.run?.id !== pass.session.session_id || pass.run?.revision !== pass.session.revision || pass.run?.attempt !== pass.session.attempt || !sameSet([JSON.stringify(pass.environment)], [JSON.stringify(pass.session.environment)]))) failures.push(finding("capture_session_mismatch", file, `${pass.id} run or environment differs from its capture session`));
|
|
119
|
-
if (scenario && !(scenario.required_channels ?? []).includes(pass.channel)) failures.push(finding("evidence_channel_mismatch", file, `${pass.id} channel is not required for its scenario`));
|
|
120
|
-
if (source && (!sameSet(pass.scope?.state_set, source.states) || pass.scope?.semantic_mode !== source.semantic_mode)) failures.push(finding("evidence_scope_mismatch", file, `${pass.id} scope differs from canonical state JSON`));
|
|
121
|
-
const expectedMedia = pass.channel === "visual" ? "image/png" : ["dom", "ax"].includes(pass.channel) ? "application/json" : undefined;
|
|
122
|
-
if (expectedMedia && pass.artifact?.media_type !== expectedMedia) failures.push(finding("evidence_media_type_mismatch", file, `${pass.id} media type does not match its channel`));
|
|
123
|
-
}
|
|
124
|
-
for (const scenario of fixture.scenarios ?? []) {
|
|
125
|
-
const channels = new Set(passes.filter((pass) => pass.scenario_id === scenario.id).map((pass) => pass.channel));
|
|
126
|
-
for (const channel of scenario.required_channels ?? []) if (!channels.has(channel)) failures.push(finding("required_channel_missing", file, `${scenario.id} lacks ${channel} evidence`));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
const artifactHarness = "node scripts/test-validate-component-state-artifacts.mjs";
|
|
2
|
-
const pinnedContainer = "mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a";
|
|
3
|
-
const sharedRoot = "$STATE_EVIDENCE_ROOT";
|
|
4
|
-
|
|
5
|
-
function jobSection(workflow, jobName) {
|
|
6
|
-
const marker = ` ${jobName}:\n`;
|
|
7
|
-
const start = workflow.indexOf(marker);
|
|
8
|
-
if (start === -1) return "";
|
|
9
|
-
const remainder = workflow.slice(start + marker.length);
|
|
10
|
-
const nextJob = remainder.search(/^ [a-z0-9][a-z0-9-]*:\s*$/m);
|
|
11
|
-
return nextJob === -1 ? remainder : remainder.slice(0, nextJob);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function normalized(line) {
|
|
15
|
-
return line.trim().replace(/\s*\\$/, "");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function commandCount(section, command) {
|
|
19
|
-
return section.split("\n").filter((line) => normalized(line).replace(/^(?:-\s+)?run:\s+/, "") === command).length;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function invocation(section, command) {
|
|
23
|
-
const lines = section.split("\n");
|
|
24
|
-
const starts = lines.flatMap((line, index) => normalized(line) === command ? [index] : []);
|
|
25
|
-
if (starts.length !== 1) return [];
|
|
26
|
-
const start = starts[0];
|
|
27
|
-
const indentation = lines[start].search(/\S/);
|
|
28
|
-
const block = [command];
|
|
29
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
30
|
-
if (lines[index].trim() && lines[index].search(/\S/) <= indentation) break;
|
|
31
|
-
if (lines[index].trim()) block.push(normalized(lines[index]));
|
|
32
|
-
}
|
|
33
|
-
return block;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function hasAdjacentLines(section, first, second) {
|
|
37
|
-
const lines = section.split("\n");
|
|
38
|
-
return lines.some((line, index) => line === first && lines[index + 1] === second);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function componentStateWorkflowFailures(workflow) {
|
|
42
|
-
const failures = [];
|
|
43
|
-
const validateJob = jobSection(workflow, "validate");
|
|
44
|
-
const componentJob = jobSection(workflow, "component-state-evidence");
|
|
45
|
-
const totalHarnesses = commandCount(workflow, artifactHarness);
|
|
46
|
-
const componentHarnesses = commandCount(componentJob, artifactHarness);
|
|
47
|
-
if (commandCount(validateJob, artifactHarness) > 0) failures.push("browser-dependent artifact/session harness must not run in validate job");
|
|
48
|
-
if (componentHarnesses === 0) failures.push("component-state artifact/session harness must run in Playwright container job");
|
|
49
|
-
if (totalHarnesses !== 1 || componentHarnesses !== 1) failures.push("artifact/session harness must run exactly once and only in component-state-evidence job");
|
|
50
|
-
if (!hasAdjacentLines(componentJob, " container:", ` image: ${pinnedContainer}`)) failures.push("component-state container.image must equal pinned Playwright digest");
|
|
51
|
-
if (/runner\.temp|RUNNER_TEMP/.test(componentJob)) failures.push("component-state Playwright container job must not use runner temp paths");
|
|
52
|
-
if (!hasAdjacentLines(componentJob, " env:", " STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state")) failures.push("missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state");
|
|
53
|
-
if (!componentJob.includes(` env:\n STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state\n SENTINEL_CONTAINER_IMAGE: ${pinnedContainer}`)) failures.push("component-state job must export the pinned Playwright image identity");
|
|
54
|
-
|
|
55
|
-
const runtime = invocation(componentJob, `STATE_ARTIFACT_DIR="${sharedRoot}/runtime"`);
|
|
56
|
-
if (!runtime.includes(`STATE_SESSION_RECEIPT="${sharedRoot}/capture-session.json"`)) failures.push("component-state runtime must bind receipt under shared root");
|
|
57
|
-
const finalizer = invocation(componentJob, "node scripts/finalize-component-state-evidence.mjs");
|
|
58
|
-
if (!finalizer.includes(`--artifact-root "${sharedRoot}"`)) failures.push("component-state finalizer must use shared artifact root");
|
|
59
|
-
if (!finalizer.includes(`--output "${sharedRoot}/runtime-manifest.json"`)) failures.push("component-state finalizer must write manifest under shared root");
|
|
60
|
-
const validator = invocation(componentJob, "node scripts/validate-component-state.mjs");
|
|
61
|
-
if (!validator.includes(`--artifact-root "${sharedRoot}"`)) failures.push("component-state validator must use shared artifact root");
|
|
62
|
-
if (!validator.includes(`--runtime-manifest "${sharedRoot}/runtime-manifest.json"`)) failures.push("component-state validator must read manifest under shared root");
|
|
63
|
-
const creator = invocation(componentJob, "node scripts/create-component-state-session.mjs");
|
|
64
|
-
if (!creator.includes(`--output "${sharedRoot}/capture-session.json"`)) failures.push("missing shared component-state workspace path --output \"$STATE_EVIDENCE_ROOT/capture-session.json\"");
|
|
65
|
-
if (!componentJob.split("\n").includes(" path: ${{ env.STATE_EVIDENCE_ROOT }}/")) failures.push("missing shared component-state workspace path path: ${{ env.STATE_EVIDENCE_ROOT }}/");
|
|
66
|
-
return failures;
|
|
67
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
export function componentStateWorkflowCases(workflow) {
|
|
2
|
-
return [
|
|
3
|
-
{
|
|
4
|
-
name: "browser_artifact_harness_in_static_job",
|
|
5
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" validate:\n", " validate:\nnode scripts/test-validate-component-state-artifacts.mjs\n") },
|
|
6
|
-
expect: ".github/workflows/validate.yml: browser-dependent artifact/session harness must not run in validate job",
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
name: "browser_artifact_harness_missing_container_job",
|
|
10
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" run: node scripts/test-validate-component-state-artifacts.mjs\n", "") },
|
|
11
|
-
expect: ".github/workflows/validate.yml: component-state artifact/session harness must run in Playwright container job",
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
name: "duplicate_artifact_harness_in_chromium_sentinel",
|
|
15
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" - run: node scripts/test-consumer-reference-sentinel.mjs\n", " - run: node scripts/test-consumer-reference-sentinel.mjs\n - run: node scripts/test-validate-component-state-artifacts.mjs\n") },
|
|
16
|
-
expect: ".github/workflows/validate.yml: artifact/session harness must run exactly once and only in component-state-evidence job",
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: "unpinned_component_container_with_pinned_env",
|
|
20
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" image: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a", " image: mcr.microsoft.com/playwright:v1.61.0-noble") },
|
|
21
|
-
expect: ".github/workflows/validate.yml: component-state container.image must equal pinned Playwright digest",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "session_receipt_outside_shared_root",
|
|
25
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" STATE_SESSION_RECEIPT=\"$STATE_EVIDENCE_ROOT/capture-session.json\" \\\n", " STATE_SESSION_RECEIPT=\"/tmp/capture-session.json\" \\\n") },
|
|
26
|
-
expect: ".github/workflows/validate.yml: component-state runtime must bind receipt under shared root",
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: "finalizer_output_outside_shared_root",
|
|
30
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" --output \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --output \"/tmp/runtime-manifest.json\" \\\n") },
|
|
31
|
-
expect: ".github/workflows/validate.yml: component-state finalizer must write manifest under shared root",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: "validator_manifest_outside_shared_root",
|
|
35
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" --runtime-manifest \"$STATE_EVIDENCE_ROOT/runtime-manifest.json\" \\\n", " --runtime-manifest \"/tmp/runtime-manifest.json\" \\\n") },
|
|
36
|
-
expect: ".github/workflows/validate.yml: component-state validator must read manifest under shared root",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: "finalizer_artifact_root_outside_shared_root",
|
|
40
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/finalize-component-state-evidence.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") },
|
|
41
|
-
expect: ".github/workflows/validate.yml: component-state finalizer must use shared artifact root",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: "validator_artifact_root_outside_shared_root",
|
|
45
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" node scripts/validate-component-state.mjs \\\n --artifact-root \"$STATE_EVIDENCE_ROOT\" \\\n", " node scripts/validate-component-state.mjs \\\n --artifact-root \"/tmp/consumer-reference-state\" \\\n") },
|
|
46
|
-
expect: ".github/workflows/validate.yml: component-state validator must use shared artifact root",
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
name: "runner_temp_in_component_job",
|
|
50
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace("STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state", "STATE_EVIDENCE_ROOT: ${{ runner.temp }}/consumer-reference-state") },
|
|
51
|
-
expect: ".github/workflows/validate.yml: component-state Playwright container job must not use runner temp paths",
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: "component_state_workspace_root_drift",
|
|
55
|
-
mutate: { ".github/workflows/validate.yml": workflow.replaceAll(".tmp/consumer-reference-state", "state-output") },
|
|
56
|
-
expect: ".github/workflows/validate.yml: missing shared component-state workspace path STATE_EVIDENCE_ROOT: .tmp/consumer-reference-state",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "missing_component_state_image_identity",
|
|
60
|
-
mutate: { ".github/workflows/validate.yml": workflow.replace(" SENTINEL_CONTAINER_IMAGE: mcr.microsoft.com/playwright:v1.61.0-noble@sha256:57b65fdc9ceabe0ef613124c7bbe2babcf9362c4d85e382fe3b03604e84b428a\n", "") },
|
|
61
|
-
expect: ".github/workflows/validate.yml: component-state job must export the pinned Playwright image identity",
|
|
62
|
-
},
|
|
63
|
-
];
|
|
64
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { isPlainObject } from "./consumer-reference-schema.mjs";
|
|
2
|
-
|
|
3
|
-
export const MIGRATION_DIMENSIONS = Object.freeze([
|
|
4
|
-
"behavior_inventory",
|
|
5
|
-
"route_parity",
|
|
6
|
-
"field_parity",
|
|
7
|
-
"action_parity",
|
|
8
|
-
"state_transitions",
|
|
9
|
-
"contract_precedence",
|
|
10
|
-
"direct_mutation",
|
|
11
|
-
"indirect_mutation",
|
|
12
|
-
"persistence_round_trip",
|
|
13
|
-
"reset_boundary",
|
|
14
|
-
"exact_time_boundary",
|
|
15
|
-
"defaults_tri_state_mapping",
|
|
16
|
-
"atomic_batch_behavior",
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
export const RUNTIME_EVIDENCE_METHODS = Object.freeze(["unit", "integration", "browser"]);
|
|
20
|
-
|
|
21
|
-
const sentencePattern = /^\S+(?:\s+\S+){2,}\s*[.!?]$/;
|
|
22
|
-
const revisionPattern = /^[a-f0-9]{40}$/;
|
|
23
|
-
const componentPattern = /^[A-Z][A-Za-z0-9]*(?:[._/-][A-Za-z0-9_-]+)*$/;
|
|
24
|
-
const selectorPattern = /^(?:[.#][A-Za-z_][A-Za-z0-9_-]*|\[[A-Za-z_:][-A-Za-z0-9_:.]*(?:=[^\]\r\n]+)?\])$/;
|
|
25
|
-
|
|
26
|
-
function finding(code, message, recordPath) {
|
|
27
|
-
return { code, message, path: recordPath };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function uniqueFindings(findings) {
|
|
31
|
-
return [...new Map(findings.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function sameStringSet(left, right) {
|
|
35
|
-
return Array.isArray(left)
|
|
36
|
-
&& Array.isArray(right)
|
|
37
|
-
&& left.length === right.length
|
|
38
|
-
&& new Set(left).size === left.length
|
|
39
|
-
&& left.every((entry) => right.includes(entry));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function isNormalizedRepositoryPath(value, { jsonOnly = false } = {}) {
|
|
43
|
-
if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("//") || value.includes("?") || value.includes("#")) return false;
|
|
44
|
-
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return false;
|
|
45
|
-
if (value.split("/").some((segment) => segment === "." || segment === ".." || segment.length === 0)) return false;
|
|
46
|
-
return !jsonOnly || value.endsWith(".json");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function isSentence(value) {
|
|
50
|
-
return typeof value === "string" && value.length >= 10 && sentencePattern.test(value);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isConsumerTarget(target) {
|
|
54
|
-
if (!isPlainObject(target) || typeof target.identity !== "string") return false;
|
|
55
|
-
if (target.kind === "component") return componentPattern.test(target.identity);
|
|
56
|
-
if (target.kind === "selector") return selectorPattern.test(target.identity);
|
|
57
|
-
if (target.kind === "module") return isNormalizedRepositoryPath(target.identity);
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function schemaCode(error) {
|
|
62
|
-
const pointer = error.instancePath ?? "";
|
|
63
|
-
const missing = error.params?.missingProperty;
|
|
64
|
-
if (pointer === "/migration_dimensions" && MIGRATION_DIMENSIONS.includes(missing)) return "migration_dimension_required";
|
|
65
|
-
if (/^\/migration_dimensions\/[^/]+\/scenario_ids$/.test(pointer) && error.keyword === "minItems") return "migration_dimension_scenario_required";
|
|
66
|
-
if (/^\/scenarios\/\d+\/evidence_method$/.test(pointer)) return "runtime_evidence_method_invalid";
|
|
67
|
-
if (/^\/adoption_mappings\/\d+\/consumer_target/.test(pointer)) return "adoption_consumer_target_invalid";
|
|
68
|
-
if (/^\/adoption_mappings\/\d+\/stylegallery\/revision$/.test(pointer)) return "adoption_stylegallery_revision_unpinned";
|
|
69
|
-
if (/^\/adoption_mappings\/\d+\/debt\/\d+/.test(pointer)) return "adoption_debt_incomplete";
|
|
70
|
-
return "consumer_conformance_schema_invalid";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function consumerConformanceSchemaFindings(errors, recordPath) {
|
|
74
|
-
return uniqueFindings((errors ?? []).map((error) => finding(
|
|
75
|
-
schemaCode(error),
|
|
76
|
-
`${error.instancePath || "/"} ${error.message ?? "does not satisfy the schema"}`,
|
|
77
|
-
recordPath,
|
|
78
|
-
)));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function validateConsumerConformanceSemantics(value, recordPath) {
|
|
82
|
-
const findings = [];
|
|
83
|
-
const add = (code, message) => findings.push(finding(code, message, recordPath));
|
|
84
|
-
if (!isPlainObject(value)) {
|
|
85
|
-
add("consumer_conformance_record_invalid", "consumer conformance record must be a JSON object");
|
|
86
|
-
return findings;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const scenarios = Array.isArray(value.scenarios) ? value.scenarios.filter(isPlainObject) : [];
|
|
90
|
-
const scenarioIds = scenarios.map((scenario) => scenario.id).filter((id) => typeof id === "string");
|
|
91
|
-
const scenarioSet = new Set(scenarioIds);
|
|
92
|
-
if (scenarioIds.length !== scenarioSet.size) add("runtime_scenario_id_duplicate", "runtime scenario IDs must be unique");
|
|
93
|
-
for (const scenario of scenarios) {
|
|
94
|
-
if (!RUNTIME_EVIDENCE_METHODS.includes(scenario.evidence_method)) add("runtime_evidence_method_invalid", "runtime evidence must use unit, integration, or browser execution");
|
|
95
|
-
if (["unit", "integration"].includes(scenario.evidence_method) && scenario.argv?.[0] !== "node") add("runtime_command_executable_invalid", "unit and integration evidence must use the governed Node runtime");
|
|
96
|
-
if (scenario.exit_code !== 0) add("runtime_evidence_exit_nonzero", `scenario ${scenario.id ?? "<unknown>"} must record exit code zero`);
|
|
97
|
-
if (!isNormalizedRepositoryPath(scenario.result_artifact, { jsonOnly: true })) add("runtime_result_artifact_invalid", `scenario ${scenario.id ?? "<unknown>"} requires a normalized JSON result artifact`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const dimensions = isPlainObject(value.migration_dimensions) ? value.migration_dimensions : {};
|
|
101
|
-
for (const dimensionName of MIGRATION_DIMENSIONS) {
|
|
102
|
-
if (!Object.hasOwn(dimensions, dimensionName)) {
|
|
103
|
-
add("migration_dimension_required", `migration dimension ${dimensionName} must be explicitly classified`);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const dimension = dimensions[dimensionName];
|
|
107
|
-
if (!isPlainObject(dimension)) continue;
|
|
108
|
-
if (dimension.status === "applicable") {
|
|
109
|
-
if (!Array.isArray(dimension.scenario_ids) || dimension.scenario_ids.length === 0) {
|
|
110
|
-
add("migration_dimension_scenario_required", `applicable migration dimension ${dimensionName} requires a runtime scenario`);
|
|
111
|
-
} else {
|
|
112
|
-
for (const scenarioId of dimension.scenario_ids) {
|
|
113
|
-
if (!scenarioSet.has(scenarioId)) add("migration_dimension_scenario_unknown", `migration dimension ${dimensionName} references unknown scenario ${scenarioId}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} else if (dimension.status === "not_applicable" && !isSentence(dimension.reason)) {
|
|
117
|
-
add("migration_dimension_reason_invalid", `not-applicable migration dimension ${dimensionName} requires a sentence reason`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const mappings = Array.isArray(value.adoption_mappings) ? value.adoption_mappings.filter(isPlainObject) : [];
|
|
122
|
-
for (const [index, mapping] of mappings.entries()) {
|
|
123
|
-
if (!isConsumerTarget(mapping.consumer_target)) add("adoption_consumer_target_invalid", `adoption mapping ${index} requires a normalized local component, selector, or module identity`);
|
|
124
|
-
if (!revisionPattern.test(mapping.stylegallery?.revision ?? "")) add("adoption_stylegallery_revision_unpinned", `adoption mapping ${index} requires a full StyleGallery revision`);
|
|
125
|
-
for (const scenarioId of Array.isArray(mapping.scenario_ids) ? mapping.scenario_ids : []) {
|
|
126
|
-
if (!scenarioSet.has(scenarioId)) add("adoption_scenario_unknown", `adoption mapping ${index} references unknown scenario ${scenarioId}`);
|
|
127
|
-
}
|
|
128
|
-
for (const debt of Array.isArray(mapping.debt) ? mapping.debt : []) {
|
|
129
|
-
if (!isPlainObject(debt) || typeof debt.owner !== "string" || debt.owner.length === 0 || !isSentence(debt.review_trigger) || typeof debt.review_by !== "string") {
|
|
130
|
-
add("adoption_debt_incomplete", `adoption mapping ${index} debt requires owner, review trigger, and review-by date`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (value.page_evidence?.status === "applicable" && !isNormalizedRepositoryPath(value.page_evidence.manifest, { jsonOnly: true })) {
|
|
136
|
-
add("page_evidence_manifest_path_invalid", "applicable page evidence requires a normalized JSON manifest path");
|
|
137
|
-
}
|
|
138
|
-
return uniqueFindings(findings);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function validateConsumerPageIntentSemantics(value, recordPath) {
|
|
142
|
-
if (!isPlainObject(value) || value.page_evidence?.status !== "applicable") return [];
|
|
143
|
-
const findings = [];
|
|
144
|
-
const add = (code, message) => findings.push(finding(code, message, recordPath));
|
|
145
|
-
const scenarios = Array.isArray(value.scenarios) ? value.scenarios.filter(isPlainObject) : [];
|
|
146
|
-
const browserIds = scenarios.filter((scenario) => scenario.evidence_method === "browser").map((scenario) => scenario.id);
|
|
147
|
-
if (browserIds.length === 0) {
|
|
148
|
-
add("page_evidence_runtime_scenario_required", "applicable page evidence requires at least one browser runtime scenario");
|
|
149
|
-
return findings;
|
|
150
|
-
}
|
|
151
|
-
const mappingIds = new Set((Array.isArray(value.adoption_mappings) ? value.adoption_mappings : [])
|
|
152
|
-
.flatMap((mapping) => Array.isArray(mapping?.scenario_ids) ? mapping.scenario_ids : []));
|
|
153
|
-
for (const scenarioId of browserIds) {
|
|
154
|
-
if (!mappingIds.has(scenarioId)) add("page_evidence_adoption_mapping_missing", `browser scenario ${scenarioId} requires an adoption mapping`);
|
|
155
|
-
}
|
|
156
|
-
return uniqueFindings(findings);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function validateConsumerPageEvidenceSemantics(value, manifest, recordPath) {
|
|
160
|
-
if (!isPlainObject(value) || !isPlainObject(manifest) || value.page_evidence?.status !== "applicable") return [];
|
|
161
|
-
const findings = [];
|
|
162
|
-
const add = (code, message) => findings.push(finding(code, message, recordPath));
|
|
163
|
-
const scenarios = Array.isArray(value.scenarios) ? value.scenarios.filter(isPlainObject) : [];
|
|
164
|
-
const scenarioById = new Map(scenarios.map((scenario) => [scenario.id, scenario]));
|
|
165
|
-
const browserScenarios = scenarios.filter((scenario) => scenario.evidence_method === "browser");
|
|
166
|
-
const browserIds = browserScenarios.map((scenario) => scenario.id);
|
|
167
|
-
const intendedIds = manifest.session?.intended_scenario_ids;
|
|
168
|
-
if (!sameStringSet(browserIds, intendedIds)) add("page_evidence_intent_scenario_mismatch", "page session intent must exactly match the conformance browser scenario catalog");
|
|
169
|
-
|
|
170
|
-
const sourcePaths = manifest.session?.source?.files?.map((entry) => entry.path);
|
|
171
|
-
if (!sameStringSet(value.consumer?.relevant_sources, sourcePaths)) add("page_evidence_source_set_mismatch", "conformance relevant sources must exactly match the page session source set");
|
|
172
|
-
if (value.consumer?.repository !== manifest.repository || value.consumer?.repository !== manifest.session?.repository || value.consumer?.repository !== manifest.run?.repository) {
|
|
173
|
-
add("page_evidence_repository_mismatch", "conformance, page session, manifest, and run repositories must agree");
|
|
174
|
-
}
|
|
175
|
-
if (value.consumer?.revision !== manifest.revision || value.consumer?.revision !== manifest.session?.revision || value.consumer?.revision !== manifest.run?.revision) {
|
|
176
|
-
add("page_evidence_revision_mismatch", "conformance, page session, manifest, and run revisions must agree");
|
|
177
|
-
}
|
|
178
|
-
if (typeof manifest.review_by !== "string" || manifest.review_by.length === 0) add("page_evidence_review_by_required", "page evidence manifest requires review_by");
|
|
179
|
-
|
|
180
|
-
const pageScenarioById = new Map((Array.isArray(manifest.scenarios) ? manifest.scenarios : []).map((scenario) => [scenario.id, scenario]));
|
|
181
|
-
const manifestDirectory = value.page_evidence.manifest.split("/").slice(0, -1).join("/");
|
|
182
|
-
for (const scenario of browserScenarios) {
|
|
183
|
-
if (scenario.source_digest !== manifest.session?.source?.sha256) add("page_evidence_source_mismatch", `browser scenario ${scenario.id} source digest must equal the page session source digest`);
|
|
184
|
-
if (scenario.session_id !== manifest.session?.session_id) add("page_evidence_session_mismatch", `browser scenario ${scenario.id} session identity must equal the page session`);
|
|
185
|
-
if (scenario.run_id !== manifest.run?.id) add("page_evidence_run_mismatch", `browser scenario ${scenario.id} run identity must equal the page run`);
|
|
186
|
-
const runnerPath = pageScenarioById.get(scenario.id)?.runner_result?.path;
|
|
187
|
-
if (scenario.result_artifact !== (runnerPath ? `${manifestDirectory}/${runnerPath}` : undefined)) add("page_evidence_result_artifact_mismatch", `browser scenario ${scenario.id} result artifact must equal its page runner result`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const dimensions = isPlainObject(value.migration_dimensions) ? Object.values(value.migration_dimensions) : [];
|
|
191
|
-
for (const scenarioId of dimensions.flatMap((dimension) => dimension?.status === "applicable" && Array.isArray(dimension.scenario_ids) ? dimension.scenario_ids : [])) {
|
|
192
|
-
if (scenarioById.get(scenarioId)?.evidence_method === "browser" && !intendedIds?.includes(scenarioId)) add("page_evidence_dimension_scenario_missing", `browser scenario ${scenarioId} used by an applicable dimension is absent from page intent`);
|
|
193
|
-
}
|
|
194
|
-
for (const scenarioId of (Array.isArray(value.adoption_mappings) ? value.adoption_mappings : []).flatMap((mapping) => Array.isArray(mapping?.scenario_ids) ? mapping.scenario_ids : [])) {
|
|
195
|
-
if (scenarioById.get(scenarioId)?.evidence_method === "browser" && !intendedIds?.includes(scenarioId)) add("page_evidence_adoption_scenario_missing", `browser scenario ${scenarioId} used by an adoption mapping is absent from page intent`);
|
|
196
|
-
}
|
|
197
|
-
return uniqueFindings(findings);
|
|
198
|
-
}
|