stylegallery 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/AGENTS.md +156 -0
  2. package/CATALOG.md +69 -0
  3. package/DOMAINS.md +65 -0
  4. package/GOVERNANCE.md +222 -0
  5. package/GUIDE.md +59 -0
  6. package/README.md +313 -0
  7. package/consumer-reference/adapters/style-dictionary.config.mjs +36 -0
  8. package/consumer-reference/agent-native/README.md +157 -0
  9. package/consumer-reference/agent-native/registry.json +640 -0
  10. package/consumer-reference/agent-native/schema/agent-native.schema.json +848 -0
  11. package/consumer-reference/agent-native/schema/epistemic.schema.json +14 -0
  12. package/consumer-reference/agent-native/schema/execution.schema.json +12 -0
  13. package/consumer-reference/agent-native/schema/identity.schema.json +10 -0
  14. package/consumer-reference/agent-native/schema/learning.schema.json +12 -0
  15. package/consumer-reference/agent-native/schema/manifest.schema.json +6 -0
  16. package/consumer-reference/agent-native/schema/operation.schema.json +12 -0
  17. package/consumer-reference/agent-native/schema/protocol-binding.schema.json +9 -0
  18. package/consumer-reference/agent-native/schema/retrieval.schema.json +10 -0
  19. package/consumer-reference/baselines/calibration.json +260 -0
  20. package/consumer-reference/baselines/manifest.json +21 -0
  21. package/consumer-reference/contract.md +83 -0
  22. package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +282 -0
  23. package/consumer-reference/fixtures/consumer-conformance/manifest.json +102 -0
  24. package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +32 -0
  25. package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +18 -0
  26. package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +142 -0
  27. package/consumer-reference/fixtures/invalid-stable-ended-support.json +19 -0
  28. package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +26 -0
  29. package/consumer-reference/fixtures/promotion/invalid-count-only.json +29 -0
  30. package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +25 -0
  31. package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +25 -0
  32. package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +45 -0
  33. package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +26 -0
  34. package/consumer-reference/fixtures/promotion/manifest.json +15 -0
  35. package/consumer-reference/fixtures/promotion/valid-deferred-example.json +67 -0
  36. package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +67 -0
  37. package/consumer-reference/fixtures/token-portability/invalid-cycle.json +7 -0
  38. package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +6 -0
  39. package/consumer-reference/fixtures/token-portability/invalid-extends.json +4 -0
  40. package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +7 -0
  41. package/consumer-reference/fixtures/token-portability/invalid-resolver.json +9 -0
  42. package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +10 -0
  43. package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +6 -0
  44. package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +6 -0
  45. package/consumer-reference/fixtures/token-portability/valid-border.json +14 -0
  46. package/consumer-reference/fixtures/token-portability/valid-reference.json +32 -0
  47. package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +7 -0
  48. package/consumer-reference/fixtures/valid-deprecated.json +21 -0
  49. package/consumer-reference/fixtures/valid-experimental.json +19 -0
  50. package/consumer-reference/generated/manifest.json +40 -0
  51. package/consumer-reference/generated/tokens.css +12 -0
  52. package/consumer-reference/index.md +32 -0
  53. package/consumer-reference/policies/shared-experimental.json +37 -0
  54. package/consumer-reference/schema/ax-evidence.schema.json +30 -0
  55. package/consumer-reference/schema/baseline-manifest.schema.json +55 -0
  56. package/consumer-reference/schema/calibration-record.schema.json +190 -0
  57. package/consumer-reference/schema/capture-session.schema.json +161 -0
  58. package/consumer-reference/schema/component-state.schema.json +150 -0
  59. package/consumer-reference/schema/consumer-conformance-record.schema.json +365 -0
  60. package/consumer-reference/schema/dom-evidence.schema.json +38 -0
  61. package/consumer-reference/schema/evidence-record.schema.json +81 -0
  62. package/consumer-reference/schema/fixture-manifest.schema.json +32 -0
  63. package/consumer-reference/schema/item.schema.json +188 -0
  64. package/consumer-reference/schema/page-evidence-manifest.schema.json +216 -0
  65. package/consumer-reference/schema/page-evidence-session.schema.json +119 -0
  66. package/consumer-reference/schema/portable-tokens.schema.json +104 -0
  67. package/consumer-reference/schema/promotion-rfc.schema.json +219 -0
  68. package/consumer-reference/schema/runtime-evidence-manifest.schema.json +33 -0
  69. package/consumer-reference/schema/visual-evidence.schema.json +31 -0
  70. package/design-engineering/consumer-migration-readiness.md +124 -0
  71. package/design-engineering/index.md +28 -0
  72. package/design-engineering/interface-craft.md +107 -0
  73. package/design-engineering/reference-profiles/external-adaptation/index.md +12 -0
  74. package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +125 -0
  75. package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +3504 -0
  76. package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +65 -0
  77. package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +23 -0
  78. package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +19 -0
  79. package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +21 -0
  80. package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +12 -0
  81. package/design-engineering/reference-profiles/governed-local/editorial/profile.json +43 -0
  82. package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +195 -0
  83. package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +17 -0
  84. package/design-engineering/reference-profiles/governed-local/index.md +26 -0
  85. package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +125 -0
  86. package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +3504 -0
  87. package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +65 -0
  88. package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +23 -0
  89. package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +19 -0
  90. package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +21 -0
  91. package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +12 -0
  92. package/design-engineering/reference-profiles/governed-local/terminal/profile.json +43 -0
  93. package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +195 -0
  94. package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +17 -0
  95. package/design-engineering/reference-profiles/index.md +19 -0
  96. package/game-ui/classification.md +82 -0
  97. package/game-ui/index.md +35 -0
  98. package/game-ui/reference-record.md +62 -0
  99. package/game-ui/screen-hierarchy.md +107 -0
  100. package/game-ui/unity/architecture.md +166 -0
  101. package/game-ui/unity/cli-loop.md +84 -0
  102. package/game-ui/unity/data/unity-technologies-public-repositories.json +26654 -0
  103. package/game-ui/unity/org-wiki.md +146 -0
  104. package/game-ui/unity/repository-map.md +102 -0
  105. package/game-ui/unity/ui-systems.md +85 -0
  106. package/guides/decision-tree.md +149 -0
  107. package/guides/documentation-mode-taxonomy.md +157 -0
  108. package/guides/layout-brief.md +220 -0
  109. package/guides/vocabulary.md +148 -0
  110. package/guides/webpage-generation-workflow.md +185 -0
  111. package/index.md +36 -0
  112. package/layout/index.md +26 -0
  113. package/motion/index.md +24 -0
  114. package/motion/practice-reference.md +145 -0
  115. package/motion/review-workflow.md +86 -0
  116. package/motion/vocabulary.md +82 -0
  117. package/package.json +78 -0
  118. package/patterns/centering/center.md +87 -0
  119. package/patterns/centering/index.md +6 -0
  120. package/patterns/centering/super-center.md +89 -0
  121. package/patterns/containment/box.md +87 -0
  122. package/patterns/containment/clamped-card.md +86 -0
  123. package/patterns/containment/content-limiter.md +87 -0
  124. package/patterns/containment/fluid-styles.md +86 -0
  125. package/patterns/containment/index.md +8 -0
  126. package/patterns/grid-repetition/card-grid.md +88 -0
  127. package/patterns/grid-repetition/columns.md +87 -0
  128. package/patterns/grid-repetition/dense-grid.md +88 -0
  129. package/patterns/grid-repetition/grid-wrapper.md +89 -0
  130. package/patterns/grid-repetition/index.md +12 -0
  131. package/patterns/grid-repetition/masonry-approx.md +87 -0
  132. package/patterns/grid-repetition/page-grid.md +90 -0
  133. package/patterns/grid-repetition/ram-grid.md +88 -0
  134. package/patterns/grid-repetition/twelve-span-grid.md +92 -0
  135. package/patterns/in-line-grouping/badge-list.md +94 -0
  136. package/patterns/in-line-grouping/breadcrumb.md +89 -0
  137. package/patterns/in-line-grouping/cluster.md +89 -0
  138. package/patterns/in-line-grouping/deconstructed-pancake.md +92 -0
  139. package/patterns/in-line-grouping/index.md +13 -0
  140. package/patterns/in-line-grouping/pagination.md +89 -0
  141. package/patterns/in-line-grouping/reel.md +90 -0
  142. package/patterns/in-line-grouping/split-nav.md +97 -0
  143. package/patterns/in-line-grouping/tab-strip.md +88 -0
  144. package/patterns/in-line-grouping/wrap-row.md +89 -0
  145. package/patterns/index.md +13 -0
  146. package/patterns/media-fit/frame.md +95 -0
  147. package/patterns/media-fit/icon-frame.md +87 -0
  148. package/patterns/media-fit/index.md +6 -0
  149. package/patterns/overlay-exception/imposter.md +95 -0
  150. package/patterns/overlay-exception/index.md +6 -0
  151. package/patterns/overlay-exception/overlay-stack.md +89 -0
  152. package/patterns/split-sidebar/index.md +12 -0
  153. package/patterns/split-sidebar/list-detail.md +95 -0
  154. package/patterns/split-sidebar/main-with-rail.md +95 -0
  155. package/patterns/split-sidebar/media-object.md +102 -0
  156. package/patterns/split-sidebar/sidebar.md +101 -0
  157. package/patterns/split-sidebar/split-screen.md +92 -0
  158. package/patterns/split-sidebar/sticky-aside.md +97 -0
  159. package/patterns/split-sidebar/supporting-pane.md +95 -0
  160. package/patterns/split-sidebar/switcher.md +93 -0
  161. package/patterns/stacking/feed.md +88 -0
  162. package/patterns/stacking/index.md +8 -0
  163. package/patterns/stacking/line-up.md +95 -0
  164. package/patterns/stacking/stack.md +87 -0
  165. package/patterns/stacking/step-nav.md +88 -0
  166. package/patterns/viewport-shell/cover.md +90 -0
  167. package/patterns/viewport-shell/fixed-sidenav-shell.md +97 -0
  168. package/patterns/viewport-shell/holy-grail.md +112 -0
  169. package/patterns/viewport-shell/index.md +11 -0
  170. package/patterns/viewport-shell/panel-layout.md +95 -0
  171. package/patterns/viewport-shell/scroll-body-shell.md +93 -0
  172. package/patterns/viewport-shell/sticky-footer.md +92 -0
  173. package/patterns/viewport-shell/sticky-header.md +86 -0
  174. package/platform-guides/apple-interaction.md +89 -0
  175. package/platform-guides/index.md +26 -0
  176. package/quality/claim-records/index.md +3 -0
  177. package/quality/claim-records/samples.md +92 -0
  178. package/quality/claim-records/stylegallery-multidomain-scope.md +42 -0
  179. package/quality/claims.md +77 -0
  180. package/quality/evidence/accessibility.md +58 -0
  181. package/quality/evidence/consumer-migration.md +42 -0
  182. package/quality/evidence/design-rationale.md +34 -0
  183. package/quality/evidence/executable-evidence.md +79 -0
  184. package/quality/evidence/families.md +56 -0
  185. package/quality/evidence/hci-evaluation.md +36 -0
  186. package/quality/evidence/index.md +16 -0
  187. package/quality/evidence/pattern-contract-audit.md +96 -0
  188. package/quality/evidence/search-metadata-polyhierarchy.md +86 -0
  189. package/quality/evidence/sources.md +43 -0
  190. package/quality/evidence/tokens.md +33 -0
  191. package/quality/evidence/visual-qa.md +41 -0
  192. package/quality/gates/accessibility-evidence.md +60 -0
  193. package/quality/gates/consumer-migration-evidence.md +53 -0
  194. package/quality/gates/design-claim.md +56 -0
  195. package/quality/gates/harmony-evaluation.md +78 -0
  196. package/quality/gates/index.md +13 -0
  197. package/quality/gates/layout.md +52 -0
  198. package/quality/gates/rationale.md +53 -0
  199. package/quality/gates/visual-evidence.md +47 -0
  200. package/quality/index.md +55 -0
  201. package/quality/principles.md +58 -0
  202. package/recipes/article-page.md +52 -0
  203. package/recipes/command-surface.md +55 -0
  204. package/recipes/dashboard.md +56 -0
  205. package/recipes/form-flow.md +55 -0
  206. package/recipes/homepage.md +89 -0
  207. package/recipes/index.md +13 -0
  208. package/recipes/list-detail.md +55 -0
  209. package/recipes/primitive-to-recipe-matrix.md +83 -0
  210. package/recipes/saas-settings.md +52 -0
  211. package/scripts/agent-native/a2a-projection.mjs +248 -0
  212. package/scripts/agent-native/agui-projection.mjs +178 -0
  213. package/scripts/agent-native/canonical-json.mjs +109 -0
  214. package/scripts/agent-native/cli-adapter.mjs +68 -0
  215. package/scripts/agent-native/events.mjs +113 -0
  216. package/scripts/agent-native/execution.mjs +227 -0
  217. package/scripts/agent-native/fixture.mjs +159 -0
  218. package/scripts/agent-native/identity.mjs +184 -0
  219. package/scripts/agent-native/immutable-store.mjs +102 -0
  220. package/scripts/agent-native/knowledge.mjs +168 -0
  221. package/scripts/agent-native/learning-records.mjs +244 -0
  222. package/scripts/agent-native/learning.mjs +247 -0
  223. package/scripts/agent-native/mcp-adapter.mjs +173 -0
  224. package/scripts/agent-native/queries.mjs +173 -0
  225. package/scripts/agent-native/registry.mjs +154 -0
  226. package/scripts/agent-native/retrieval.mjs +171 -0
  227. package/scripts/agent-native/self-description.mjs +160 -0
  228. package/scripts/artifact-metadata.mjs +20 -0
  229. package/scripts/audit-evidence-freshness.mjs +308 -0
  230. package/scripts/baseline-contract.mjs +189 -0
  231. package/scripts/baseline-schema-parity.mjs +40 -0
  232. package/scripts/build-reference-artifacts.mjs +127 -0
  233. package/scripts/calibration-raw-contract.mjs +172 -0
  234. package/scripts/capture-session-contract.mjs +187 -0
  235. package/scripts/component-state-artifact-fixture.mjs +44 -0
  236. package/scripts/component-state-contract.mjs +99 -0
  237. package/scripts/component-state-semantics.mjs +128 -0
  238. package/scripts/component-state-workflow-contract.mjs +67 -0
  239. package/scripts/component-state-workflow-negative-cases.mjs +64 -0
  240. package/scripts/consumer-conformance-contract.mjs +198 -0
  241. package/scripts/consumer-evidence-governance-contract.mjs +210 -0
  242. package/scripts/consumer-reference-case-runner.mjs +93 -0
  243. package/scripts/consumer-reference-schema.mjs +155 -0
  244. package/scripts/create-component-state-session.mjs +100 -0
  245. package/scripts/create-page-evidence-session.mjs +139 -0
  246. package/scripts/domain-document-boundaries.mjs +40 -0
  247. package/scripts/domain-validation-checks.mjs +141 -0
  248. package/scripts/evidence-artifact-contract.mjs +191 -0
  249. package/scripts/finalize-component-state-evidence.mjs +165 -0
  250. package/scripts/finalize-page-evidence.mjs +197 -0
  251. package/scripts/generate-consumer-reference-evidence.mjs +154 -0
  252. package/scripts/generate-patterns.mjs +218 -0
  253. package/scripts/governance-matrix-contract.mjs +73 -0
  254. package/scripts/governance-matrix-negative-cases.mjs +114 -0
  255. package/scripts/governance-policy-contract.mjs +49 -0
  256. package/scripts/governance-test-fixture.mjs +321 -0
  257. package/scripts/governance-workflow-negative-cases.mjs +241 -0
  258. package/scripts/governed-profile-registry.mjs +125 -0
  259. package/scripts/json-schema-formats.mjs +31 -0
  260. package/scripts/markdown-structure.mjs +45 -0
  261. package/scripts/page-artifact-metadata.mjs +124 -0
  262. package/scripts/page-evidence-contract.mjs +297 -0
  263. package/scripts/page-evidence-fixture.mjs +271 -0
  264. package/scripts/pattern-data.mjs +325 -0
  265. package/scripts/profile-record-contract.mjs +140 -0
  266. package/scripts/promotion-attestation-contract.mjs +91 -0
  267. package/scripts/promotion-boundary-test-contract.mjs +92 -0
  268. package/scripts/promotion-fixture-inventory.mjs +109 -0
  269. package/scripts/promotion-governance-contract.mjs +28 -0
  270. package/scripts/reference-artifact-path-contract.mjs +28 -0
  271. package/scripts/reference-profile-contract.mjs +114 -0
  272. package/scripts/reference-token-contract.mjs +156 -0
  273. package/scripts/run-consumer-page-evidence-ci.mjs +170 -0
  274. package/scripts/sg-mcp.mjs +16 -0
  275. package/scripts/sg.mjs +27 -0
  276. package/scripts/strict-json.mjs +72 -0
  277. package/scripts/summarize-sentinel-calibration.mjs +98 -0
  278. package/scripts/test-agent-adapter-conformance.mjs +250 -0
  279. package/scripts/test-agent-conformance-receipt.mjs +106 -0
  280. package/scripts/test-agent-events.mjs +81 -0
  281. package/scripts/test-agent-execution-effects.mjs +114 -0
  282. package/scripts/test-agent-execution-fixtures.mjs +140 -0
  283. package/scripts/test-agent-execution-kernel.mjs +39 -0
  284. package/scripts/test-agent-execution-learning.mjs +130 -0
  285. package/scripts/test-agent-execution-lifecycle.mjs +143 -0
  286. package/scripts/test-agent-execution-safety.mjs +158 -0
  287. package/scripts/test-agent-execution-views.mjs +117 -0
  288. package/scripts/test-agent-identity-integrity-cases.mjs +160 -0
  289. package/scripts/test-agent-knowledge-kernel.mjs +183 -0
  290. package/scripts/test-agent-learning.mjs +246 -0
  291. package/scripts/test-agent-mcp.mjs +246 -0
  292. package/scripts/test-agent-native-schemas.mjs +215 -0
  293. package/scripts/test-agent-projections.mjs +249 -0
  294. package/scripts/test-agent-registry-mcp-integrity.mjs +215 -0
  295. package/scripts/test-agent-registry-mutations.mjs +110 -0
  296. package/scripts/test-audit-evidence-freshness.mjs +177 -0
  297. package/scripts/test-component-state-sentinel.mjs +35 -0
  298. package/scripts/test-component-state-source-contract.mjs +83 -0
  299. package/scripts/test-consumer-conformance-sentinel.mjs +165 -0
  300. package/scripts/test-consumer-reference-sentinel.mjs +31 -0
  301. package/scripts/test-generate-consumer-reference-evidence.mjs +124 -0
  302. package/scripts/test-json-cli-output.mjs +151 -0
  303. package/scripts/test-json-schema-formats.mjs +40 -0
  304. package/scripts/test-reference-adapters.mjs +199 -0
  305. package/scripts/test-renderer-purity.mjs +34 -0
  306. package/scripts/test-sg-cli.mjs +129 -0
  307. package/scripts/test-summarize-sentinel-calibration.mjs +226 -0
  308. package/scripts/test-validate-baseline-manifest.mjs +234 -0
  309. package/scripts/test-validate-component-state-artifacts.mjs +243 -0
  310. package/scripts/test-validate-component-state.mjs +106 -0
  311. package/scripts/test-validate-consumer-conformance.mjs +427 -0
  312. package/scripts/test-validate-consumer-reference-cli.mjs +69 -0
  313. package/scripts/test-validate-consumer-reference.mjs +263 -0
  314. package/scripts/test-validate-domains.mjs +357 -0
  315. package/scripts/test-validate-governance.mjs +292 -0
  316. package/scripts/test-validate-ia.mjs +262 -0
  317. package/scripts/test-validate-okf.mjs +73 -0
  318. package/scripts/test-validate-page-evidence.mjs +302 -0
  319. package/scripts/test-validate-patterns.mjs +157 -0
  320. package/scripts/test-validate-promotion-rfc.mjs +250 -0
  321. package/scripts/test-validate-reference-profiles.mjs +170 -0
  322. package/scripts/test-validate-unity-org-wiki.mjs +57 -0
  323. package/scripts/test-validate-unity-source-contracts.mjs +67 -0
  324. package/scripts/test-validate-vocabulary.mjs +174 -0
  325. package/scripts/test-validate-webpage-workflow.mjs +293 -0
  326. package/scripts/validate-baseline-manifest.mjs +44 -0
  327. package/scripts/validate-catalog.mjs +65 -0
  328. package/scripts/validate-component-state.mjs +141 -0
  329. package/scripts/validate-consumer-conformance.mjs +477 -0
  330. package/scripts/validate-consumer-reference.mjs +288 -0
  331. package/scripts/validate-domains.mjs +237 -0
  332. package/scripts/validate-governance.mjs +248 -0
  333. package/scripts/validate-ia.mjs +136 -0
  334. package/scripts/validate-links.mjs +66 -0
  335. package/scripts/validate-okf.mjs +104 -0
  336. package/scripts/validate-page-evidence.mjs +191 -0
  337. package/scripts/validate-patterns.mjs +246 -0
  338. package/scripts/validate-promotion-rfc.mjs +224 -0
  339. package/scripts/validate-reference-artifacts.mjs +125 -0
  340. package/scripts/validate-renderer-purity.mjs +56 -0
  341. package/scripts/validate-unity-org-wiki.mjs +146 -0
  342. package/scripts/validate-unity-source-contracts.mjs +107 -0
  343. package/scripts/validate-vocabulary.mjs +115 -0
  344. package/scripts/validate-webpage-workflow.mjs +254 -0
  345. package/scripts/visual-expectation-contract.mjs +20 -0
  346. package/scripts/workflow-action-contract.mjs +163 -0
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ canonicalSourceManifest,
8
+ canonicalIntended,
9
+ readCaptureSession,
10
+ sameJson,
11
+ sourceManifestMatches,
12
+ withinSession,
13
+ } from "./capture-session-contract.mjs";
14
+ import { compileSchemas, readRecord, validateEvidenceArtifacts, validateProfile } from "./component-state-contract.mjs";
15
+ import { resolveProfileRecords } from "./profile-record-contract.mjs";
16
+ import { visualExpectationFor } from "./visual-expectation-contract.mjs";
17
+
18
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
19
+ const options = { artifactRoot: undefined, json: false, root: path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), runtimeManifest: undefined };
20
+ const failures = [];
21
+
22
+ function validateCommittedVisualExpectations(resolved) {
23
+ const evidenceRecord = resolved.records.evidence[0];
24
+ const statesRecord = resolved.records.states[0];
25
+ if (!evidenceRecord || !statesRecord) return;
26
+ const scenarios = new Map((statesRecord.value.scenarios ?? []).map((scenario) => [scenario.id, scenario]));
27
+ for (const pass of (evidenceRecord.value.passes ?? []).filter((candidate) => candidate.channel === "visual")) {
28
+ const scenario = scenarios.get(pass.scenario_id);
29
+ if (!scenario) continue;
30
+ try {
31
+ const expected = visualExpectationFor(scenario, pass.environment, statesRecord.value.visual_environments ?? []);
32
+ const actual = pass.artifact ?? {};
33
+ if (actual.sha256 !== expected.sha256 || actual.width !== expected.width || actual.height !== expected.height) {
34
+ failures.push({ code: "evidence_visual_expectation_mismatch", message: `${pass.id} committed visual metadata differs from its canonical environment expectation`, path: evidenceRecord.path });
35
+ }
36
+ } catch (error) {
37
+ failures.push({ code: "evidence_visual_expectation_invalid", message: error instanceof Error ? error.message : String(error), path: evidenceRecord.path });
38
+ }
39
+ }
40
+ }
41
+
42
+ for (let index = 2; index < process.argv.length; index += 1) {
43
+ const argument = process.argv[index];
44
+ if (argument === "--json") options.json = true;
45
+ else if (["--artifact-root", "--runtime-manifest", "--root"].includes(argument)) {
46
+ const value = process.argv[index + 1];
47
+ if (!value) failures.push({ code: "argument_value_required", message: `${argument} requires a path`, path: "<cli>" });
48
+ else {
49
+ if (argument === "--artifact-root") options.artifactRoot = path.resolve(value);
50
+ if (argument === "--runtime-manifest") options.runtimeManifest = path.resolve(value);
51
+ if (argument === "--root") options.root = path.resolve(value);
52
+ index += 1;
53
+ }
54
+ } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "<cli>" });
55
+ }
56
+
57
+ const schemas = compileSchemas(path.join(repositoryRoot, "consumer-reference/schema"));
58
+ let runtimeManifest;
59
+ let capture;
60
+ let canonicalSource;
61
+ if (!options.runtimeManifest) {
62
+ try { canonicalSource = canonicalSourceManifest(repositoryRoot, options.root); }
63
+ catch (error) { failures.push({ code: "capture_source_unreadable", message: error instanceof Error ? error.message : String(error), path: options.root }); }
64
+ }
65
+ if (options.runtimeManifest) {
66
+ runtimeManifest = readRecord(options.runtimeManifest, failures);
67
+ if (!options.artifactRoot) failures.push({ code: "runtime_manifest_artifact_root_required", message: "--runtime-manifest requires --artifact-root", path: "<cli>" });
68
+ if (runtimeManifest && !schemas.runtime(runtimeManifest)) for (const error of schemas.runtime.errors ?? []) failures.push({ code: "runtime_manifest_schema_invalid", message: `${error.instancePath || "/"} ${error.message}`, path: options.runtimeManifest });
69
+ if (options.artifactRoot) capture = readCaptureSession(path.join(options.artifactRoot, "capture-session.json"), schemas.capture, failures);
70
+ if (capture && runtimeManifest) {
71
+ const session = runtimeManifest.session;
72
+ if (!session || !sameJson(session.receipt, capture.artifact) || session.receipt_sha256 !== capture.digest) failures.push({ code: "capture_session_receipt_mismatch", message: "manifest receipt metadata does not match capture-session.json", path: options.runtimeManifest });
73
+ const expectedLink = capture.link;
74
+ for (const key of ["session_id", "nonce", "started_at", "revision", "branch", "attempt", "environment", "source"]) if (!sameJson(session?.[key], expectedLink[key])) failures.push({ code: "capture_session_mismatch", message: `manifest ${key} differs from receipt`, path: options.runtimeManifest });
75
+ for (const key of ["repository", "intended"]) if (!sameJson(session?.[key], capture.receipt[key])) failures.push({ code: "capture_session_mismatch", message: `manifest ${key} differs from receipt`, path: options.runtimeManifest });
76
+ if (runtimeManifest.recorded_at !== session?.completed_at) failures.push({ code: "evidence_recorded_at_mismatch", message: "manifest recorded_at must equal session completed_at", path: options.runtimeManifest });
77
+ if (!withinSession(session?.completed_at, capture.receipt.started_at, session?.completed_at)) failures.push({ code: "capture_session_time_outside", message: "completed_at precedes session start", path: options.runtimeManifest });
78
+ const canonical = canonicalIntended(options.root, failures);
79
+ if (!sameJson(capture.receipt.intended, canonical)) failures.push({ code: "capture_session_intent_mismatch", message: "receipt intent differs from current canonical records", path: options.runtimeManifest });
80
+ if (!capture.receipt.source) failures.push({ code: "capture_source_missing", message: "capture receipt must include its canonical source manifest", path: options.runtimeManifest });
81
+ else if (!sourceManifestMatches(capture.receipt.source, repositoryRoot, options.root)) failures.push({ code: "capture_source_drift", message: "current capture sources differ from the receipt source manifest", path: options.runtimeManifest });
82
+ const run = runtimeManifest.run;
83
+ if (run?.id !== capture.receipt.session_id || run?.repository !== capture.receipt.repository || run?.revision !== capture.receipt.revision || run?.attempt !== capture.receipt.attempt) failures.push({ code: "capture_session_mismatch", message: "manifest run differs from receipt", path: options.runtimeManifest });
84
+ for (const record of runtimeManifest.records ?? []) {
85
+ for (const pass of record.passes ?? []) {
86
+ if (!sameJson(pass.environment, runtimeManifest.environment) || !sameJson(pass.run, run)) failures.push({ code: "runtime_manifest_identity_mismatch", message: `${pass.id} does not match the closed manifest identity`, path: options.runtimeManifest });
87
+ if (!sameJson(pass.session, capture.link) || !sameJson(pass.environment, capture.receipt.environment) || !sameJson(pass.run, run)) failures.push({ code: "capture_session_mismatch", message: `${pass.id} pass identity differs from receipt`, path: options.runtimeManifest });
88
+ }
89
+ }
90
+ }
91
+ } else if (options.artifactRoot) failures.push({ code: "runtime_manifest_required", message: "artifact validation requires --runtime-manifest", path: "<cli>" });
92
+
93
+ const profiles = fs.existsSync(options.root)
94
+ ? fs.readdirSync(options.root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && fs.existsSync(path.join(options.root, entry.name, "profile.json"))).map((entry) => entry.name).sort()
95
+ : [];
96
+ if (profiles.length === 0) failures.push({ code: "profile_root_empty", message: "governed-local root must contain profiles", path: options.root });
97
+ for (const profile of profiles) {
98
+ const profileRoot = path.join(options.root, profile);
99
+ failures.push(...validateProfile(profileRoot, schemas));
100
+ if (!runtimeManifest) {
101
+ const resolved = resolveProfileRecords(profileRoot, failures);
102
+ if (resolved) validateCommittedVisualExpectations(resolved);
103
+ const evidenceRecord = resolved?.records.evidence[0];
104
+ if (canonicalSource) {
105
+ for (const pass of evidenceRecord?.value.passes ?? []) {
106
+ if (!pass.session?.source) failures.push({ code: "capture_source_missing", message: `${pass.id} committed evidence must include its canonical source manifest`, path: evidenceRecord.path });
107
+ else if (!sameJson(pass.session.source, canonicalSource)) failures.push({ code: "capture_source_drift", message: `${pass.id} committed evidence source differs from current canonical capture sources`, path: evidenceRecord.path });
108
+ }
109
+ }
110
+ }
111
+ const profileId = JSON.parse(fs.readFileSync(path.join(profileRoot, "profile.json"), "utf8")).id;
112
+ const matching = runtimeManifest?.records?.filter((record) => record.profile_id === profileId) ?? [];
113
+ if (runtimeManifest && matching.length !== 1) failures.push({ code: "runtime_manifest_profile_count", message: `${profileId} requires exactly one runtime evidence record`, path: options.runtimeManifest });
114
+ if (options.artifactRoot && runtimeManifest) failures.push(...validateEvidenceArtifacts(profileRoot, options.artifactRoot, matching[0], schemas, runtimeManifest.session));
115
+ }
116
+ if (runtimeManifest) {
117
+ const known = new Set(profiles.map((profile) => JSON.parse(fs.readFileSync(path.join(options.root, profile, "profile.json"), "utf8")).id));
118
+ for (const record of runtimeManifest.records ?? []) if (!known.has(record.profile_id)) failures.push({ code: "runtime_manifest_profile_unknown", message: `${record.profile_id} is not a governed profile`, path: options.runtimeManifest });
119
+ }
120
+ if (runtimeManifest && options.artifactRoot) {
121
+ const passes = (runtimeManifest.records ?? []).flatMap((record) => record.passes ?? []);
122
+ const passArtifacts = new Set(passes.map((pass) => pass.artifact?.path).filter(Boolean));
123
+ if (passArtifacts.size !== 30) failures.push({ code: "runtime_channel_count_mismatch", message: `manifest requires exactly 30 unique channel artifacts, found ${passArtifacts.size}`, path: options.runtimeManifest });
124
+ const expected = new Set(passArtifacts);
125
+ for (const pass of passes) if (pass.channel === "visual" && pass.artifact?.path?.endsWith(".png")) expected.add(pass.artifact.path.replace(/\.png$/, ".visual.json"));
126
+ if (expected.size !== 40) failures.push({ code: "runtime_artifact_count_mismatch", message: `manifest requires exactly 40 runtime files, found ${expected.size}`, path: options.runtimeManifest });
127
+ const runtimeRoot = path.join(options.artifactRoot, "runtime");
128
+ const actual = fs.existsSync(runtimeRoot) ? fs.readdirSync(runtimeRoot, { withFileTypes: true }) : [];
129
+ for (const entry of actual) {
130
+ const reference = `runtime/${entry.name}`;
131
+ if (!entry.isFile() || !expected.has(reference)) failures.push({ code: "runtime_artifact_unmanifested", message: `${reference} is not a declared regular session artifact`, path: runtimeRoot });
132
+ }
133
+ for (const reference of expected) if (!fs.existsSync(path.join(options.artifactRoot, reference))) failures.push({ code: "runtime_artifact_missing", message: `${reference} is missing`, path: runtimeRoot });
134
+ }
135
+
136
+ const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()];
137
+ const result = { checkedProfiles: profiles.length, failures: uniqueFailures, ok: uniqueFailures.length === 0, warnings: [] };
138
+ if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
139
+ else if (result.ok) process.stdout.write(`ok: ${profiles.length} governed-local component profiles\n`);
140
+ else process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.path}: ${failure.message}`).join("\n")}\n`);
141
+ if (!result.ok) process.exitCode = 1;
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { isDeepStrictEqual } from "node:util";
8
+ import { fileURLToPath } from "node:url";
9
+ import Ajv2020 from "ajv/dist/2020.js";
10
+ import {
11
+ MIGRATION_DIMENSIONS,
12
+ consumerConformanceSchemaFindings,
13
+ isNormalizedRepositoryPath,
14
+ validateConsumerConformanceSemantics,
15
+ validateConsumerPageEvidenceSemantics,
16
+ validateConsumerPageIntentSemantics,
17
+ } from "./consumer-conformance-contract.mjs";
18
+ import { parseStrictJson } from "./strict-json.mjs";
19
+ import { addDateTimeFormat } from "./json-schema-formats.mjs";
20
+ import {
21
+ addSchemaFindings,
22
+ compilePageEvidenceSchemas,
23
+ digest,
24
+ gitIdentity,
25
+ normalizeReference,
26
+ PAGE_EVIDENCE_MANIFEST,
27
+ readJsonFile,
28
+ resolveContained,
29
+ } from "./page-evidence-contract.mjs";
30
+ import { MAX_ARTIFACT_FILE_BYTES } from "./page-artifact-metadata.mjs";
31
+
32
+ const styleGalleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
33
+ const schemaPath = path.join(styleGalleryRoot, "consumer-reference", "schema", "consumer-conformance-record.schema.json");
34
+ const pageSchemaRoot = path.join(styleGalleryRoot, "consumer-reference", "schema");
35
+ const pageValidator = path.join(styleGalleryRoot, "scripts", "validate-page-evidence.mjs");
36
+ const runtimeTimeout = Math.min(120_000, Math.max(100, Number.parseInt(process.env.STYLEGALLERY_RUNTIME_TIMEOUT_MS ?? "120000", 10) || 120_000));
37
+
38
+ function finding(code, message, recordPath) {
39
+ return { code, message, path: recordPath };
40
+ }
41
+
42
+ function parseArguments(argv) {
43
+ const options = { artifactRoot: undefined, json: false, priorManifest: undefined, record: undefined, root: process.cwd() };
44
+ const failures = [];
45
+ for (let index = 2; index < argv.length; index += 1) {
46
+ const argument = argv[index];
47
+ if (argument === "--json") options.json = true;
48
+ else if (["--artifact-root", "--prior-manifest", "--record", "--root"].includes(argument)) {
49
+ const value = argv[index + 1];
50
+ if (!value || value.startsWith("--")) failures.push(finding("argument_value_required", `${argument} requires a value`, "<cli>"));
51
+ else {
52
+ if (argument === "--artifact-root") options.artifactRoot = value;
53
+ if (argument === "--prior-manifest") options.priorManifest = value;
54
+ if (argument === "--record") options.record = value;
55
+ if (argument === "--root") options.root = value;
56
+ index += 1;
57
+ }
58
+ } else failures.push(finding("argument_unknown", `unsupported argument ${argument}`, "<cli>"));
59
+ }
60
+ if (!options.record) failures.push(finding("argument_value_required", "--record requires a normalized JSON path", "<cli>"));
61
+ return { failures, options };
62
+ }
63
+
64
+ function containedRecord(rootArgument, recordArgument, failures) {
65
+ const root = path.resolve(process.cwd(), rootArgument);
66
+ if (!fs.existsSync(root) || !fs.lstatSync(root).isDirectory() || fs.realpathSync(root) !== root) {
67
+ failures.push(finding("consumer_conformance_root_invalid", "consumer root must be a real directory without filesystem redirects", rootArgument));
68
+ return undefined;
69
+ }
70
+ if (!isNormalizedRepositoryPath(recordArgument, { jsonOnly: true })) {
71
+ failures.push(finding("consumer_conformance_record_path_invalid", "--record must be a normalized repository-relative JSON path", recordArgument));
72
+ return undefined;
73
+ }
74
+ const resolved = path.resolve(root, recordArgument);
75
+ const relative = path.relative(root, resolved);
76
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || !fs.existsSync(resolved)) {
77
+ failures.push(finding("consumer_conformance_record_path_invalid", "consumer conformance record must exist inside the consumer root", recordArgument));
78
+ return undefined;
79
+ }
80
+ let current = root;
81
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
82
+ current = path.join(current, segment);
83
+ if (fs.lstatSync(current).isSymbolicLink()) {
84
+ failures.push(finding("consumer_conformance_record_path_invalid", "consumer conformance record must not traverse a symlink", recordArgument));
85
+ return undefined;
86
+ }
87
+ }
88
+ if (!fs.lstatSync(resolved).isFile() || fs.realpathSync(resolved) !== resolved) {
89
+ failures.push(finding("consumer_conformance_record_path_invalid", "consumer conformance record must be a contained regular file", recordArgument));
90
+ return undefined;
91
+ }
92
+ return { recordPath: relative.split(path.sep).join("/"), resolved, root };
93
+ }
94
+
95
+ function readStrictJson(file, code, displayPath, failures) {
96
+ try {
97
+ if (fs.lstatSync(file).size > MAX_ARTIFACT_FILE_BYTES) throw new Error(`file exceeds ${MAX_ARTIFACT_FILE_BYTES} bytes`);
98
+ return parseStrictJson(fs.readFileSync(file, "utf8"));
99
+ } catch (error) {
100
+ failures.push(finding(code, error instanceof Error ? error.message : String(error), displayPath));
101
+ return undefined;
102
+ }
103
+ }
104
+
105
+ function uniqueFindings(findings) {
106
+ return [...new Map(findings.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()];
107
+ }
108
+
109
+ function runGit(root, args, encoding = "utf8") {
110
+ return spawnSync("git", ["-c", `safe.directory=${path.resolve(root)}`, ...args], { cwd: root, encoding });
111
+ }
112
+
113
+ function firstFilesystemRedirect(root) {
114
+ const pending = [root];
115
+ while (pending.length > 0) {
116
+ const directory = pending.pop();
117
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
118
+ const target = path.join(directory, entry.name);
119
+ const stat = fs.lstatSync(target);
120
+ if (stat.isSymbolicLink()) return path.relative(root, target).split(path.sep).join("/");
121
+ if (stat.isDirectory()) pending.push(target);
122
+ }
123
+ }
124
+ return undefined;
125
+ }
126
+
127
+ function executePinnedScenario(root, revision, scenario, expectedResult, failures) {
128
+ if (!Array.isArray(scenario?.argv) || scenario.argv.length === 0 || scenario.argv.some((argument) => typeof argument !== "string" || argument.length === 0)) return false;
129
+ const [executable, ...args] = scenario.argv;
130
+ if (executable !== "node") {
131
+ failures.push(finding("runtime_command_executable_invalid", `scenario ${scenario.id ?? "<unknown>"} must use the governed Node runtime`, scenario.id ?? "<scenario>"));
132
+ return undefined;
133
+ }
134
+ const entrypoint = args[0];
135
+ if (!isNormalizedRepositoryPath(entrypoint ?? "") || entrypoint.startsWith("-") || !/\.(?:c?js|mjs)$/.test(entrypoint)) {
136
+ failures.push(finding("runtime_command_argv_unsafe", `scenario ${scenario.id ?? "<unknown>"} may not override the governed Node permission boundary`, scenario.id ?? "<scenario>"));
137
+ return undefined;
138
+ }
139
+ const temporaryRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "stylegallery-consumer-command-")));
140
+ const checkout = path.join(temporaryRoot, "checkout");
141
+ let worktreeAdded = false;
142
+ try {
143
+ const add = runGit(root, ["worktree", "add", "--detach", checkout, revision]);
144
+ if (add.status !== 0) {
145
+ failures.push(finding("runtime_command_checkout_failed", `scenario ${scenario.id ?? "<unknown>"} could not check out the pinned revision`, scenario.id ?? "<scenario>"));
146
+ return false;
147
+ }
148
+ worktreeAdded = true;
149
+ const redirect = firstFilesystemRedirect(checkout);
150
+ if (redirect) {
151
+ failures.push(finding("runtime_command_checkout_symlink", `scenario ${scenario.id ?? "<unknown>"} checkout contains filesystem redirect ${redirect}`, redirect));
152
+ return undefined;
153
+ }
154
+ const resultFile = path.resolve(checkout, scenario.result_artifact);
155
+ fs.rmSync(resultFile, { force: true });
156
+ const child = spawnSync(process.execPath, ["--permission", `--allow-fs-read=${checkout}`, `--allow-fs-write=${checkout}`, ...args], {
157
+ cwd: checkout,
158
+ env: {
159
+ CI: "1",
160
+ LANG: "C",
161
+ LC_ALL: "C",
162
+ PATH: process.env.PATH ?? "",
163
+ STYLEGALLERY_RESULT_ARTIFACT: scenario.result_artifact,
164
+ STYLEGALLERY_RUNTIME_CONTEXT: JSON.stringify(expectedResult),
165
+ },
166
+ killSignal: "SIGKILL",
167
+ shell: false,
168
+ stdio: "ignore",
169
+ timeout: runtimeTimeout,
170
+ });
171
+ if (child.error || child.status !== 0) {
172
+ const outcome = child.error?.code === "ETIMEDOUT" ? "timed out" : child.signal ? `received ${child.signal}` : `exited ${child.status ?? "without a status"}`;
173
+ failures.push(finding("runtime_command_failed", `scenario ${scenario.id ?? "<unknown>"} ${outcome} at the pinned revision`, scenario.id ?? "<scenario>"));
174
+ return undefined;
175
+ }
176
+ try {
177
+ const stat = fs.lstatSync(resultFile);
178
+ if (stat.size > MAX_ARTIFACT_FILE_BYTES) throw new Error(`result exceeds ${MAX_ARTIFACT_FILE_BYTES} bytes`);
179
+ if (!stat.isFile() || stat.isSymbolicLink() || !fs.realpathSync(resultFile).startsWith(`${fs.realpathSync(checkout)}${path.sep}`)) throw new Error("result is not a contained regular file");
180
+ return parseStrictJson(fs.readFileSync(resultFile, "utf8"));
181
+ } catch (error) {
182
+ failures.push(finding("runtime_command_result_missing", `scenario ${scenario.id ?? "<unknown>"} did not produce a valid result artifact: ${error instanceof Error ? error.message : String(error)}`, scenario.result_artifact));
183
+ return undefined;
184
+ }
185
+ } finally {
186
+ if (worktreeAdded) {
187
+ const removed = runGit(root, ["worktree", "remove", "--force", "--force", checkout]);
188
+ if (removed.status !== 0) {
189
+ try { fs.rmSync(checkout, { force: true, recursive: true }); } catch {}
190
+ runGit(root, ["worktree", "prune", "--expire", "now"]);
191
+ const registered = runGit(root, ["worktree", "list", "--porcelain"]);
192
+ if (registered.status !== 0 || registered.stdout.includes(checkout)) failures.push(finding("runtime_command_cleanup_failed", `scenario ${scenario.id ?? "<unknown>"} worktree cleanup failed`, scenario.id ?? "<scenario>"));
193
+ }
194
+ }
195
+ fs.rmSync(temporaryRoot, { force: true, recursive: true });
196
+ }
197
+ }
198
+
199
+ function remoteIdentity(remote) {
200
+ const cleaned = remote.trim().replace(/\.git$/, "");
201
+ const scp = cleaned.match(/^[^@]+@[^:]+:(.+)$/);
202
+ if (scp) return scp[1];
203
+ try { return new URL(cleaned).pathname.replace(/^\//, ""); }
204
+ catch { return undefined; }
205
+ }
206
+
207
+ function pinnedSourceManifest(root, revision, references, failures) {
208
+ if (!Array.isArray(references) || references.length === 0 || new Set(references).size !== references.length) return undefined;
209
+ const revisionCheck = runGit(root, ["cat-file", "-e", `${revision}^{commit}`]);
210
+ if (revisionCheck.status !== 0) {
211
+ failures.push(finding("consumer_revision_unknown", "consumer revision must resolve to a commit in the repository", revision));
212
+ return undefined;
213
+ }
214
+ const files = [];
215
+ for (const reference of [...references].sort()) {
216
+ if (!isNormalizedRepositoryPath(reference)) continue;
217
+ const tree = runGit(root, ["ls-tree", "-z", revision, "--", reference], "buffer");
218
+ const entry = tree.status === 0 ? tree.stdout.toString("utf8").split("\0").find((line) => line.endsWith(`\t${reference}`)) : undefined;
219
+ if (!entry) {
220
+ failures.push(finding("consumer_source_missing", "relevant source is missing from the pinned revision", reference));
221
+ continue;
222
+ }
223
+ const [metadata] = entry.split("\t", 1);
224
+ const [mode, type] = metadata.split(" ");
225
+ if (mode === "120000") {
226
+ failures.push(finding("consumer_source_symlink", "relevant source must not be a symbolic link", reference));
227
+ continue;
228
+ }
229
+ if (type !== "blob") {
230
+ failures.push(finding("consumer_source_type_invalid", "relevant source must be a regular file", reference));
231
+ continue;
232
+ }
233
+ const blob = runGit(root, ["show", `${revision}:${reference}`], "buffer");
234
+ if (blob.status !== 0) {
235
+ failures.push(finding("consumer_source_unreadable", "relevant source could not be read from the pinned revision", reference));
236
+ continue;
237
+ }
238
+ files.push({ byte_length: blob.stdout.length, path: reference, sha256: digest(blob.stdout) });
239
+ }
240
+ return files.length === references.length ? { files, sha256: digest(Buffer.from(JSON.stringify(files))) } : undefined;
241
+ }
242
+
243
+ function markdownAnchors(bytes) {
244
+ const occurrences = new Map();
245
+ return bytes.toString("utf8").split("\n").flatMap((line) => {
246
+ const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/)?.[1];
247
+ if (!heading) return [];
248
+ const base = heading
249
+ .replace(/`([^`]*)`/g, "$1")
250
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
251
+ .toLowerCase()
252
+ .replace(/[^a-z0-9 _-]/g, "")
253
+ .trim()
254
+ .replace(/\s+/g, "-");
255
+ const occurrence = occurrences.get(base) ?? 0;
256
+ occurrences.set(base, occurrence + 1);
257
+ return [occurrence === 0 ? `#${base}` : `#${base}-${occurrence}`];
258
+ });
259
+ }
260
+
261
+ function validateStyleGallerySources(value, recordPath, failures) {
262
+ const mappings = Array.isArray(value?.adoption_mappings) ? value.adoption_mappings : [];
263
+ let checked = 0;
264
+ for (const [index, mapping] of mappings.entries()) {
265
+ const source = mapping?.stylegallery;
266
+ if (!source || !/^[a-f0-9]{40}$/.test(source.revision ?? "") || !isNormalizedRepositoryPath(source.path)) continue;
267
+ const revision = runGit(styleGalleryRoot, ["cat-file", "-e", `${source.revision}^{commit}`]);
268
+ if (revision.status !== 0) {
269
+ failures.push(finding("adoption_stylegallery_revision_unknown", `adoption mapping ${index} revision does not resolve in StyleGallery`, recordPath));
270
+ continue;
271
+ }
272
+ const tree = runGit(styleGalleryRoot, ["ls-tree", "-z", source.revision, "--", source.path], "buffer");
273
+ const entry = tree.status === 0 ? tree.stdout.toString("utf8").split("\0").find((line) => line.endsWith(`\t${source.path}`)) : undefined;
274
+ if (!entry) {
275
+ failures.push(finding("adoption_stylegallery_path_missing", `adoption mapping ${index} path is missing from its pinned StyleGallery revision`, source.path));
276
+ continue;
277
+ }
278
+ const [metadata] = entry.split("\t", 1);
279
+ const [mode, type] = metadata.split(" ");
280
+ if (mode === "120000" || type !== "blob") {
281
+ failures.push(finding("adoption_stylegallery_path_invalid", `adoption mapping ${index} path must be a regular StyleGallery file`, source.path));
282
+ continue;
283
+ }
284
+ const blob = runGit(styleGalleryRoot, ["show", `${source.revision}:${source.path}`], "buffer");
285
+ if (blob.status !== 0) {
286
+ failures.push(finding("adoption_stylegallery_path_unreadable", `adoption mapping ${index} path could not be read from StyleGallery`, source.path));
287
+ continue;
288
+ }
289
+ if (!markdownAnchors(blob.stdout).includes(source.anchor)) {
290
+ failures.push(finding("adoption_stylegallery_anchor_missing", `adoption mapping ${index} anchor is missing from its pinned StyleGallery file`, `${source.path}${source.anchor}`));
291
+ continue;
292
+ }
293
+ checked += 1;
294
+ }
295
+ return checked;
296
+ }
297
+
298
+ function validateRuntimeEvidence({ recordPath, root, validateResult, value }, failures) {
299
+ const consumer = value?.consumer;
300
+ const scenarios = Array.isArray(value?.scenarios) ? value.scenarios : [];
301
+ if (!consumer || typeof consumer.repository !== "string" || typeof consumer.revision !== "string" || !Array.isArray(consumer.relevant_sources)) return 0;
302
+
303
+ const gitFailures = [];
304
+ const identity = gitIdentity(root, gitFailures);
305
+ for (const issue of gitFailures) failures.push(finding("consumer_git_invalid", issue.message, issue.path));
306
+ if (identity) {
307
+ const remote = runGit(root, ["config", "--get", "remote.origin.url"]);
308
+ const repositories = new Set([identity.repository, remote.status === 0 ? remoteIdentity(remote.stdout) : undefined].filter(Boolean));
309
+ if (repositories.size === 0) failures.push(finding("consumer_repository_unverifiable", "consumer repository requires a verifiable Git remote identity", recordPath));
310
+ else if (!repositories.has(consumer.repository)) failures.push(finding("consumer_repository_mismatch", "consumer repository differs from the Git repository identity", recordPath));
311
+ }
312
+
313
+ const source = pinnedSourceManifest(root, consumer.revision, consumer.relevant_sources, failures);
314
+ if (source) {
315
+ for (const scenario of scenarios) {
316
+ if (scenario?.source_digest !== source.sha256) failures.push(finding("runtime_source_digest_mismatch", `scenario ${scenario?.id ?? "<unknown>"} source digest differs from the pinned relevant sources`, recordPath));
317
+ }
318
+ }
319
+
320
+ let checked = 0;
321
+ let executed = 0;
322
+ for (const scenario of scenarios) {
323
+ if (!isNormalizedRepositoryPath(scenario?.result_artifact, { jsonOnly: true })) continue;
324
+ const result = readJsonFile({ prefix: "runtime_result_artifact", reference: scenario.result_artifact, root }, failures);
325
+ if (!result) continue;
326
+ checked += 1;
327
+ if (scenario.evidence_method === "browser" && value.page_evidence?.status === "applicable") continue;
328
+ if (!validateResult(result.value)) {
329
+ for (const error of validateResult.errors ?? []) failures.push(finding("runtime_result_artifact_schema_invalid", `${error.instancePath || "/"} ${error.message}`, scenario.result_artifact));
330
+ continue;
331
+ }
332
+ const expected = {
333
+ argv: scenario.argv,
334
+ evidence_method: scenario.evidence_method,
335
+ exit_code: scenario.exit_code,
336
+ repository: consumer.repository,
337
+ revision: consumer.revision,
338
+ run_id: scenario.run_id,
339
+ scenario_id: scenario.id,
340
+ session_id: scenario.session_id,
341
+ source_digest: scenario.source_digest,
342
+ };
343
+ if (Object.entries(expected).some(([key, expectedValue]) => Array.isArray(expectedValue)
344
+ ? !Array.isArray(result.value[key]) || expectedValue.length !== result.value[key].length || expectedValue.some((entry, index) => result.value[key][index] !== entry)
345
+ : result.value[key] !== expectedValue)) {
346
+ failures.push(finding("runtime_result_artifact_mismatch", `scenario ${scenario.id ?? "<unknown>"} result artifact identity differs from the conformance record`, scenario.result_artifact));
347
+ }
348
+ const executedResult = identity && source ? executePinnedScenario(root, consumer.revision, scenario, expected, failures) : undefined;
349
+ if (executedResult !== undefined) {
350
+ executed += 1;
351
+ if (!validateResult(executedResult)) {
352
+ for (const error of validateResult.errors ?? []) failures.push(finding("runtime_command_result_invalid", `${error.instancePath || "/"} ${error.message}`, scenario.result_artifact));
353
+ } else if (!isDeepStrictEqual(executedResult, result.value)) {
354
+ failures.push(finding("runtime_result_artifact_execution_mismatch", `scenario ${scenario.id ?? "<unknown>"} stored result differs from the result produced at the pinned revision`, scenario.result_artifact));
355
+ }
356
+ }
357
+ }
358
+ return { checked, executed };
359
+ }
360
+
361
+ function runPageValidator({ artifactRoot, manifest, priorManifest, root }, failures) {
362
+ const args = [pageValidator, "--root", root, "--artifact-root", artifactRoot, "--manifest", manifest];
363
+ if (priorManifest) args.push("--prior-manifest", priorManifest);
364
+ args.push("--json");
365
+ const child = spawnSync(process.execPath, args, { cwd: styleGalleryRoot, encoding: "utf8" });
366
+ let report;
367
+ try {
368
+ report = parseStrictJson(child.stdout);
369
+ } catch (error) {
370
+ failures.push(finding("page_evidence_validator_output_invalid", error instanceof Error ? error.message : String(error), manifest));
371
+ return undefined;
372
+ }
373
+ if (Array.isArray(report?.failures)) failures.push(...report.failures);
374
+ if (child.status !== 0 && report?.failures?.length === 0) failures.push(finding("page_evidence_validator_failed", "page evidence validator exited without a finding", manifest));
375
+ return report;
376
+ }
377
+
378
+ function validatePageEvidence({ artifactRootArgument, priorManifestArgument, recordPath, root, value }, failures) {
379
+ if (value?.page_evidence?.status !== "applicable") return undefined;
380
+ const intentFindings = validateConsumerPageIntentSemantics(value, recordPath);
381
+ failures.push(...intentFindings);
382
+ if (intentFindings.some((entry) => entry.code === "page_evidence_runtime_scenario_required")) return undefined;
383
+ if (!artifactRootArgument) {
384
+ failures.push(finding("page_evidence_artifact_root_required", "applicable page evidence requires --artifact-root", value.page_evidence.manifest ?? "<page-evidence>"));
385
+ return undefined;
386
+ }
387
+ const artifactReference = normalizeReference(root, artifactRootArgument);
388
+ if (!artifactReference) {
389
+ failures.push(finding("page_evidence_artifact_escape", "artifact root must be a normalized path inside the consumer root", artifactRootArgument));
390
+ return undefined;
391
+ }
392
+ const artifact = resolveContained({ expectedType: "directory", prefix: "page_evidence_artifact", reference: artifactReference, root }, failures);
393
+ const expectedManifest = `${artifactReference}/${PAGE_EVIDENCE_MANIFEST}`;
394
+ if (value.page_evidence.manifest !== expectedManifest) {
395
+ failures.push(finding("page_evidence_manifest_path_mismatch", `page evidence manifest must equal ${expectedManifest}`, value.page_evidence.manifest));
396
+ return undefined;
397
+ }
398
+ const manifest = readJsonFile({ prefix: "page_evidence_manifest", reference: expectedManifest, root }, failures);
399
+ if (!artifact || !manifest) return undefined;
400
+ let schemas;
401
+ try {
402
+ schemas = compilePageEvidenceSchemas(pageSchemaRoot);
403
+ } catch (error) {
404
+ failures.push(finding("page_evidence_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema"));
405
+ return undefined;
406
+ }
407
+ if (!addSchemaFindings(schemas.manifest, manifest.value, manifest.reference, "page_evidence_manifest_schema_invalid", failures)) return undefined;
408
+ failures.push(...validateConsumerPageEvidenceSemantics(value, manifest.value, recordPath));
409
+
410
+ let priorManifest;
411
+ if (priorManifestArgument) {
412
+ const reference = normalizeReference(root, priorManifestArgument);
413
+ const prior = reference ? resolveContained({ expectedType: "file", prefix: "page_evidence_reuse_manifest", reference, root }, failures) : undefined;
414
+ if (!reference) failures.push(finding("page_evidence_reuse_manifest_escape", "prior manifest must be a normalized path inside the consumer root", priorManifestArgument));
415
+ priorManifest = prior?.file;
416
+ }
417
+ if (priorManifestArgument && !priorManifest) return undefined;
418
+ return runPageValidator({ artifactRoot: artifact.file, manifest: manifest.file, priorManifest, root }, failures);
419
+ }
420
+
421
+ const { failures: argumentFailures, options } = parseArguments(process.argv);
422
+ const failures = [...argumentFailures];
423
+ const record = options.record ? containedRecord(options.root, options.record, failures) : undefined;
424
+ const schema = readStrictJson(schemaPath, "consumer_conformance_schema_invalid", "consumer-reference/schema/consumer-conformance-record.schema.json", failures);
425
+ const value = record ? readStrictJson(record.resolved, "consumer_conformance_json_invalid", record.recordPath, failures) : undefined;
426
+ let checkedRuntimeArtifacts = 0;
427
+ let checkedRuntimeCommands = 0;
428
+ let checkedStyleGallerySources = 0;
429
+ let recordSchemaValid = false;
430
+
431
+ if (schema && value !== undefined) {
432
+ try {
433
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
434
+ addDateTimeFormat(ajv);
435
+ ajv.addSchema(schema);
436
+ const validate = ajv.getSchema(schema.$id);
437
+ const validateResult = ajv.compile({ $ref: `${schema.$id}#/$defs/runtimeResult` });
438
+ const schemaValid = validate(value);
439
+ recordSchemaValid = schemaValid;
440
+ if (!schemaValid) failures.push(...consumerConformanceSchemaFindings(validate.errors, record.recordPath));
441
+ else {
442
+ failures.push(...validateConsumerConformanceSemantics(value, record.recordPath));
443
+ checkedStyleGallerySources = validateStyleGallerySources(value, record.recordPath, failures);
444
+ const runtimeEvidence = validateRuntimeEvidence({ recordPath: record.recordPath, root: record.root, validateResult, value }, failures);
445
+ checkedRuntimeArtifacts = runtimeEvidence.checked;
446
+ checkedRuntimeCommands = runtimeEvidence.executed;
447
+ }
448
+ } catch (error) {
449
+ failures.push(finding("consumer_conformance_schema_invalid", error instanceof Error ? error.message : String(error), "consumer-reference/schema/consumer-conformance-record.schema.json"));
450
+ }
451
+ }
452
+
453
+ const pageReport = record && value !== undefined && recordSchemaValid
454
+ ? validatePageEvidence({ artifactRootArgument: options.artifactRoot, priorManifestArgument: options.priorManifest, recordPath: record.recordPath, root: record.root, value }, failures)
455
+ : undefined;
456
+
457
+ const uniqueFailures = uniqueFindings(failures);
458
+ const result = {
459
+ checkedDimensions: value && typeof value.migration_dimensions === "object" && value.migration_dimensions !== null
460
+ ? MIGRATION_DIMENSIONS.filter((name) => Object.hasOwn(value.migration_dimensions, name)).length
461
+ : 0,
462
+ checkedMappings: Array.isArray(value?.adoption_mappings) ? value.adoption_mappings.length : 0,
463
+ checkedRuntimeArtifacts,
464
+ checkedRuntimeCommands,
465
+ checkedScenarios: Array.isArray(value?.scenarios) ? value.scenarios.length : 0,
466
+ checkedStyleGallerySources,
467
+ checkedPageArtifacts: pageReport?.artifactCount ?? 0,
468
+ checkedPageScenarios: pageReport?.scenarioCount ?? 0,
469
+ failures: uniqueFailures,
470
+ ok: uniqueFailures.length === 0,
471
+ pageSessionId: pageReport?.sessionId ?? null,
472
+ record: record?.recordPath ?? options.record ?? null,
473
+ };
474
+ if (options.json) console.log(JSON.stringify(result, null, 2));
475
+ else if (result.ok) console.log(`ok: ${result.checkedDimensions} migration dimensions, ${result.checkedMappings} adoption mappings`);
476
+ else console.error(result.failures.map((entry) => `${entry.code}: ${entry.path}: ${entry.message}`).join("\n"));
477
+ process.exitCode = result.ok ? 0 : 1;