stylegallery 0.1.0 → 0.1.1

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.
Files changed (230) hide show
  1. package/AGENTS.md +4 -1
  2. package/DOMAINS.md +2 -6
  3. package/GOVERNANCE.md +10 -9
  4. package/README.md +23 -127
  5. package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
  6. package/consumer-reference/agent-native/v2/material-registry.json +1 -0
  7. package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
  8. package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
  9. package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
  10. package/consumer-reference/contract.md +1 -1
  11. package/consumer-reference/index.md +5 -3
  12. package/layout/index.md +124 -0
  13. package/log.md +51 -0
  14. package/package.json +181 -22
  15. package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
  16. package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
  17. package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
  18. package/scripts/agent-native/v2/material-admission.mjs +295 -0
  19. package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
  20. package/scripts/agent-native/v2/material-context.mjs +251 -0
  21. package/scripts/agent-native/v2/material-identity.mjs +71 -0
  22. package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
  23. package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
  24. package/scripts/agent-native/v2/material-queries.mjs +398 -0
  25. package/scripts/agent-native/v2/material-registry.mjs +319 -0
  26. package/scripts/sg-material-mcp.mjs +98 -0
  27. package/scripts/sg-material.mjs +27 -0
  28. package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
  29. package/consumer-reference/baselines/calibration.json +0 -260
  30. package/consumer-reference/baselines/manifest.json +0 -21
  31. package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
  32. package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
  33. package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
  34. package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
  35. package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
  36. package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
  37. package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
  38. package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
  39. package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
  40. package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
  41. package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
  42. package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
  43. package/consumer-reference/fixtures/promotion/manifest.json +0 -15
  44. package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
  45. package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
  46. package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
  47. package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
  48. package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
  49. package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
  50. package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
  51. package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
  52. package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
  53. package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
  54. package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
  55. package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
  56. package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
  57. package/consumer-reference/fixtures/valid-deprecated.json +0 -21
  58. package/consumer-reference/fixtures/valid-experimental.json +0 -19
  59. package/consumer-reference/generated/manifest.json +0 -40
  60. package/consumer-reference/generated/tokens.css +0 -12
  61. package/consumer-reference/policies/shared-experimental.json +0 -37
  62. package/consumer-reference/schema/ax-evidence.schema.json +0 -30
  63. package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
  64. package/consumer-reference/schema/calibration-record.schema.json +0 -190
  65. package/consumer-reference/schema/capture-session.schema.json +0 -161
  66. package/consumer-reference/schema/component-state.schema.json +0 -150
  67. package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
  68. package/consumer-reference/schema/dom-evidence.schema.json +0 -38
  69. package/consumer-reference/schema/evidence-record.schema.json +0 -81
  70. package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
  71. package/consumer-reference/schema/item.schema.json +0 -188
  72. package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
  73. package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
  74. package/consumer-reference/schema/portable-tokens.schema.json +0 -104
  75. package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
  76. package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
  77. package/consumer-reference/schema/visual-evidence.schema.json +0 -31
  78. package/design-engineering/consumer-migration-readiness.md +0 -124
  79. package/design-engineering/interface-craft.md +0 -107
  80. package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
  81. package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
  82. package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
  83. package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
  84. package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
  85. package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
  86. package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
  87. package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
  88. package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
  89. package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
  90. package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
  91. package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
  92. package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
  93. package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
  94. package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
  95. package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
  96. package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
  97. package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
  98. package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
  99. package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
  100. package/game-ui/classification.md +0 -82
  101. package/game-ui/reference-record.md +0 -62
  102. package/game-ui/screen-hierarchy.md +0 -107
  103. package/game-ui/unity/architecture.md +0 -166
  104. package/game-ui/unity/cli-loop.md +0 -84
  105. package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
  106. package/game-ui/unity/org-wiki.md +0 -146
  107. package/game-ui/unity/repository-map.md +0 -102
  108. package/game-ui/unity/ui-systems.md +0 -85
  109. package/motion/practice-reference.md +0 -145
  110. package/motion/review-workflow.md +0 -86
  111. package/motion/vocabulary.md +0 -82
  112. package/platform-guides/apple-interaction.md +0 -89
  113. package/scripts/agent-native/events.mjs +0 -113
  114. package/scripts/agent-native/immutable-store.mjs +0 -102
  115. package/scripts/artifact-metadata.mjs +0 -20
  116. package/scripts/audit-evidence-freshness.mjs +0 -308
  117. package/scripts/baseline-contract.mjs +0 -189
  118. package/scripts/baseline-schema-parity.mjs +0 -40
  119. package/scripts/build-reference-artifacts.mjs +0 -127
  120. package/scripts/calibration-raw-contract.mjs +0 -172
  121. package/scripts/capture-session-contract.mjs +0 -187
  122. package/scripts/component-state-artifact-fixture.mjs +0 -44
  123. package/scripts/component-state-contract.mjs +0 -99
  124. package/scripts/component-state-semantics.mjs +0 -128
  125. package/scripts/component-state-workflow-contract.mjs +0 -67
  126. package/scripts/component-state-workflow-negative-cases.mjs +0 -64
  127. package/scripts/consumer-conformance-contract.mjs +0 -198
  128. package/scripts/consumer-evidence-governance-contract.mjs +0 -210
  129. package/scripts/consumer-reference-case-runner.mjs +0 -93
  130. package/scripts/consumer-reference-schema.mjs +0 -155
  131. package/scripts/create-component-state-session.mjs +0 -100
  132. package/scripts/create-page-evidence-session.mjs +0 -139
  133. package/scripts/domain-document-boundaries.mjs +0 -40
  134. package/scripts/domain-validation-checks.mjs +0 -141
  135. package/scripts/evidence-artifact-contract.mjs +0 -191
  136. package/scripts/finalize-component-state-evidence.mjs +0 -165
  137. package/scripts/finalize-page-evidence.mjs +0 -197
  138. package/scripts/generate-consumer-reference-evidence.mjs +0 -154
  139. package/scripts/generate-patterns.mjs +0 -218
  140. package/scripts/governance-matrix-contract.mjs +0 -73
  141. package/scripts/governance-matrix-negative-cases.mjs +0 -114
  142. package/scripts/governance-policy-contract.mjs +0 -49
  143. package/scripts/governance-test-fixture.mjs +0 -321
  144. package/scripts/governance-workflow-negative-cases.mjs +0 -241
  145. package/scripts/governed-profile-registry.mjs +0 -125
  146. package/scripts/json-schema-formats.mjs +0 -31
  147. package/scripts/markdown-structure.mjs +0 -45
  148. package/scripts/page-artifact-metadata.mjs +0 -124
  149. package/scripts/page-evidence-contract.mjs +0 -297
  150. package/scripts/page-evidence-fixture.mjs +0 -271
  151. package/scripts/pattern-data.mjs +0 -325
  152. package/scripts/profile-record-contract.mjs +0 -140
  153. package/scripts/promotion-attestation-contract.mjs +0 -91
  154. package/scripts/promotion-boundary-test-contract.mjs +0 -92
  155. package/scripts/promotion-fixture-inventory.mjs +0 -109
  156. package/scripts/promotion-governance-contract.mjs +0 -28
  157. package/scripts/reference-artifact-path-contract.mjs +0 -28
  158. package/scripts/reference-profile-contract.mjs +0 -114
  159. package/scripts/reference-token-contract.mjs +0 -156
  160. package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
  161. package/scripts/summarize-sentinel-calibration.mjs +0 -98
  162. package/scripts/test-agent-adapter-conformance.mjs +0 -250
  163. package/scripts/test-agent-conformance-receipt.mjs +0 -106
  164. package/scripts/test-agent-events.mjs +0 -81
  165. package/scripts/test-agent-execution-effects.mjs +0 -114
  166. package/scripts/test-agent-execution-fixtures.mjs +0 -140
  167. package/scripts/test-agent-execution-kernel.mjs +0 -39
  168. package/scripts/test-agent-execution-learning.mjs +0 -130
  169. package/scripts/test-agent-execution-lifecycle.mjs +0 -143
  170. package/scripts/test-agent-execution-safety.mjs +0 -158
  171. package/scripts/test-agent-execution-views.mjs +0 -117
  172. package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
  173. package/scripts/test-agent-knowledge-kernel.mjs +0 -183
  174. package/scripts/test-agent-learning.mjs +0 -246
  175. package/scripts/test-agent-mcp.mjs +0 -246
  176. package/scripts/test-agent-native-schemas.mjs +0 -215
  177. package/scripts/test-agent-projections.mjs +0 -249
  178. package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
  179. package/scripts/test-agent-registry-mutations.mjs +0 -110
  180. package/scripts/test-audit-evidence-freshness.mjs +0 -177
  181. package/scripts/test-component-state-sentinel.mjs +0 -35
  182. package/scripts/test-component-state-source-contract.mjs +0 -83
  183. package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
  184. package/scripts/test-consumer-reference-sentinel.mjs +0 -31
  185. package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
  186. package/scripts/test-json-cli-output.mjs +0 -151
  187. package/scripts/test-json-schema-formats.mjs +0 -40
  188. package/scripts/test-reference-adapters.mjs +0 -199
  189. package/scripts/test-renderer-purity.mjs +0 -34
  190. package/scripts/test-sg-cli.mjs +0 -129
  191. package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
  192. package/scripts/test-validate-baseline-manifest.mjs +0 -234
  193. package/scripts/test-validate-component-state-artifacts.mjs +0 -243
  194. package/scripts/test-validate-component-state.mjs +0 -106
  195. package/scripts/test-validate-consumer-conformance.mjs +0 -427
  196. package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
  197. package/scripts/test-validate-consumer-reference.mjs +0 -263
  198. package/scripts/test-validate-domains.mjs +0 -357
  199. package/scripts/test-validate-governance.mjs +0 -292
  200. package/scripts/test-validate-ia.mjs +0 -262
  201. package/scripts/test-validate-okf.mjs +0 -73
  202. package/scripts/test-validate-page-evidence.mjs +0 -302
  203. package/scripts/test-validate-patterns.mjs +0 -157
  204. package/scripts/test-validate-promotion-rfc.mjs +0 -250
  205. package/scripts/test-validate-reference-profiles.mjs +0 -170
  206. package/scripts/test-validate-unity-org-wiki.mjs +0 -57
  207. package/scripts/test-validate-unity-source-contracts.mjs +0 -67
  208. package/scripts/test-validate-vocabulary.mjs +0 -174
  209. package/scripts/test-validate-webpage-workflow.mjs +0 -293
  210. package/scripts/validate-baseline-manifest.mjs +0 -44
  211. package/scripts/validate-catalog.mjs +0 -65
  212. package/scripts/validate-component-state.mjs +0 -141
  213. package/scripts/validate-consumer-conformance.mjs +0 -477
  214. package/scripts/validate-consumer-reference.mjs +0 -288
  215. package/scripts/validate-domains.mjs +0 -237
  216. package/scripts/validate-governance.mjs +0 -248
  217. package/scripts/validate-ia.mjs +0 -136
  218. package/scripts/validate-links.mjs +0 -66
  219. package/scripts/validate-okf.mjs +0 -104
  220. package/scripts/validate-page-evidence.mjs +0 -191
  221. package/scripts/validate-patterns.mjs +0 -246
  222. package/scripts/validate-promotion-rfc.mjs +0 -224
  223. package/scripts/validate-reference-artifacts.mjs +0 -125
  224. package/scripts/validate-renderer-purity.mjs +0 -56
  225. package/scripts/validate-unity-org-wiki.mjs +0 -146
  226. package/scripts/validate-unity-source-contracts.mjs +0 -107
  227. package/scripts/validate-vocabulary.mjs +0 -115
  228. package/scripts/validate-webpage-workflow.mjs +0 -254
  229. package/scripts/visual-expectation-contract.mjs +0 -20
  230. 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
- }