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,224 @@
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 Ajv2020 from "ajv/dist/2020.js";
7
+ import { isPlainObject } from "./consumer-reference-schema.mjs";
8
+ import { validateGovernedProfileInventory } from "./governed-profile-registry.mjs";
9
+ import { validatePromotionAttestations } from "./promotion-attestation-contract.mjs";
10
+ import { loadPromotionFixtureInventory, resolvePromotionJsonFile } from "./promotion-fixture-inventory.mjs";
11
+ import { parseStrictJson } from "./strict-json.mjs";
12
+
13
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const options = {
15
+ files: [],
16
+ fixtureRoot: path.join(repositoryRoot, "consumer-reference/fixtures/promotion"),
17
+ json: false,
18
+ policies: [],
19
+ profileRepositoryRoot: repositoryRoot,
20
+ profileRepositoryRootExplicit: false,
21
+ };
22
+ const argumentFailures = [];
23
+
24
+ for (let index = 2; index < process.argv.length; index += 1) {
25
+ const argument = process.argv[index];
26
+ if (argument === "--json") options.json = true;
27
+ else if (argument === "--file") {
28
+ const value = process.argv[index + 1];
29
+ if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--file requires a JSON path", path: "<cli>" });
30
+ else {
31
+ options.files.push(path.resolve(process.cwd(), value));
32
+ index += 1;
33
+ }
34
+ } else if (argument === "--policy") {
35
+ const value = process.argv[index + 1];
36
+ if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--policy requires a JSON path", path: "<cli>" });
37
+ else {
38
+ options.policies.push(path.resolve(process.cwd(), value));
39
+ index += 1;
40
+ }
41
+ } else if (argument === "--profile-repository-root") {
42
+ const value = process.argv[index + 1];
43
+ if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--profile-repository-root requires a repository path", path: "<cli>" });
44
+ else {
45
+ options.profileRepositoryRoot = path.resolve(process.cwd(), value);
46
+ options.profileRepositoryRootExplicit = true;
47
+ index += 1;
48
+ }
49
+ } else if (argument === "--promotion-fixture-root") {
50
+ const value = process.argv[index + 1];
51
+ if (!value || value.startsWith("--")) argumentFailures.push({ code: "argument_value_required", message: "--promotion-fixture-root requires a repository path", path: "<cli>" });
52
+ else {
53
+ options.fixtureRoot = path.resolve(process.cwd(), value);
54
+ index += 1;
55
+ }
56
+ } else argumentFailures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "<cli>" });
57
+ }
58
+
59
+ const failures = [...argumentFailures];
60
+ const warnings = [];
61
+ const fixtureInventory = loadPromotionFixtureInventory({ fixtureRoot: options.fixtureRoot, repositoryRoot });
62
+ failures.push(...fixtureInventory.failures);
63
+ let validateSchema = () => false;
64
+ try {
65
+ if (fixtureInventory.schema) validateSchema = new Ajv2020({ allErrors: true, strict: true }).compile(fixtureInventory.schema);
66
+ } catch (error) {
67
+ failures.push({ code: "promotion_fixture_inventory_invalid", message: error instanceof Error ? error.message : String(error), path: "consumer-reference/schema/promotion-rfc.schema.json" });
68
+ }
69
+ const files = options.files.length > 0
70
+ ? options.files
71
+ : options.policies.length > 0
72
+ ? []
73
+ : fixtureInventory.validFiles;
74
+ const policies = options.files.length === 0 && options.policies.length === 0
75
+ ? [fixtureInventory.policyFile].filter(Boolean)
76
+ : options.policies;
77
+
78
+ function finding(code, message, recordPath) {
79
+ return { code, message, path: recordPath };
80
+ }
81
+
82
+ function sameStringSet(actual, expected) {
83
+ return Array.isArray(actual) && actual.length === expected.length && actual.every((item) => typeof item === "string") && new Set(actual).size === expected.length && expected.every((item) => actual.includes(item));
84
+ }
85
+
86
+ function validateSemantics({ canonicalExample, value, recordPath, failures, warnings }) {
87
+ if (!isPlainObject(value)) return;
88
+ if (canonicalExample && (value.decision !== "deferred" || !Array.isArray(value.attestations) || value.attestations.length !== 0)) {
89
+ failures.push(finding("promotion_canonical_example_invalid", "canonical examples must remain deferred with zero attestations", recordPath));
90
+ }
91
+ if (typeof value.review_independence === "boolean") failures.push(finding("review_independence_boolean", "review_independence must use the canonical single_account enum", recordPath));
92
+ if (!isPlainObject(value.owner) || typeof value.owner.migration !== "string" || !isPlainObject(value.migration)) {
93
+ failures.push(finding("promotion_owner_migration_required", "promotion proposals require migration ownership and a migration plan", recordPath));
94
+ }
95
+ if (value.compatibility?.breaking === true && value.migration?.required !== true) {
96
+ failures.push(finding("promotion_migration_required", "breaking compatibility changes require an enabled migration plan", recordPath));
97
+ }
98
+ if (value.eligibility_basis === "consumer_count_only") failures.push(finding("promotion_count_only_forbidden", "consumer count cannot replace evidence and lifecycle duties", recordPath));
99
+ if (isPlainObject(value.support) && value.transition?.to === "stable" && value.support.status === "ended") {
100
+ failures.push(finding("stable_support_ended", "stable promotion requires active support", recordPath));
101
+ }
102
+ if (value.support?.status !== "active" || value.support?.capacity !== "committed") {
103
+ failures.push(finding("promotion_support_required", "promotion eligibility requires active, affirmative support capacity", recordPath));
104
+ }
105
+ if (value.transition?.from === "stable" && value.transition?.to === "shared_experimental") {
106
+ failures.push(finding("promotion_stable_relabel_forbidden", "a stable contract must be maintained, rolled back, or deprecated with migration", recordPath));
107
+ }
108
+ const transition = `${value.transition?.from ?? ""}->${value.transition?.to ?? ""}`;
109
+ if (!["consumer_local->shared_experimental", "shared_experimental->stable", "stable->shared_experimental"].includes(transition)) {
110
+ failures.push(finding("promotion_transition_invalid", `${transition} is not a governed promotion transition`, recordPath));
111
+ }
112
+ const evidence = Array.isArray(value.evidence) ? value.evidence.filter(isPlainObject) : [];
113
+ const evidenceIds = evidence.map((entry) => entry.id).filter((id) => typeof id === "string");
114
+ if (new Set(evidenceIds).size !== evidenceIds.length) failures.push(finding("promotion_evidence_duplicate", "evidence IDs must be unique", recordPath));
115
+ if (evidence.some((entry) => entry.kind === "blocking" && entry.status === "failed")) {
116
+ failures.push(finding("promotion_blocking_evidence_failed", "failed blocking evidence prevents eligibility", recordPath));
117
+ }
118
+ const regressionPassed = evidence.some((entry) => entry.kind === "regression" && entry.status === "passed");
119
+ const claimScopedPassed = evidence.some((entry) => entry.kind === "claim_scoped" && entry.status === "passed");
120
+ if (value.normative_bypass === "normative_correctness" && !regressionPassed) {
121
+ failures.push(finding("promotion_normative_regression_required", "normative bypass waives adoption count only and requires passing regression evidence", recordPath));
122
+ }
123
+ if (value.normative_bypass === "normative_correctness" && value.scope?.kind !== "normative_correctness") {
124
+ failures.push(finding("promotion_normative_scope_required", "normative bypass requires a normative correctness scope", recordPath));
125
+ }
126
+ if (value.transition?.to === "stable" && (!claimScopedPassed || !regressionPassed)) {
127
+ failures.push(finding("promotion_stable_evidence_required", "stable requires passing claim-scoped and regression evidence", recordPath));
128
+ }
129
+ const attestationResult = validatePromotionAttestations({ inventory: governedProfileInventory, profileRoot: options.profileRepositoryRoot, recordPath, value });
130
+ failures.push(...attestationResult.failures);
131
+ const counted = attestationResult.eligibleCount;
132
+ if (value.transition?.from === "consumer_local" && value.transition?.to === "shared_experimental" && value.normative_bypass === "none" && counted < 2) {
133
+ warnings.push(finding("promotion_consumer_count_below_gateway", "deferred proposal has fewer than two independent consumers", recordPath));
134
+ }
135
+ if (value.transition?.to === "stable" && (value.stable_scope_decision !== true || value.provenance?.debt_resolved !== true)) {
136
+ failures.push(finding("promotion_stable_readiness_required", "stable requires explicit scope decision and resolved provenance debt; no numeric count substitutes", recordPath));
137
+ }
138
+ }
139
+
140
+ function validatePolicy(value, recordPath, failures) {
141
+ if (!isPlainObject(value)) {
142
+ failures.push(finding("promotion_policy_shape_invalid", "promotion policy must be an object", recordPath));
143
+ return;
144
+ }
145
+ const allowed = ["artifact_mode", "boundaries", "claims", "gateway", "id", "lifecycle", "normative_bypass", "owner", "review_independence", "schema_version", "status"];
146
+ if (Object.keys(value).some((key) => !allowed.includes(key))) failures.push(finding("promotion_policy_shape_invalid", "promotion policy contains an unknown field", recordPath));
147
+ const nestedShapes = [
148
+ [value.boundaries, ["excluded_content", "promoted_content"], "boundaries"],
149
+ [value.claims, ["adopter_attestations", "promotion_occurred"], "claims"],
150
+ [value.gateway, ["applies_to", "minimum_independent_consumers", "stable_numeric_threshold", "transition"], "gateway"],
151
+ [value.gateway?.transition, ["from", "to"], "gateway.transition"],
152
+ [value.lifecycle, ["failure_actions", "stable_relabel"], "lifecycle"],
153
+ [value.normative_bypass, ["categories", "retains", "waives"], "normative_bypass"],
154
+ [value.owner, ["enforcement", "name"], "owner"],
155
+ ];
156
+ for (const [candidate, keys, label] of nestedShapes) {
157
+ if (!isPlainObject(candidate) || Object.keys(candidate).some((key) => !keys.includes(key))) failures.push(finding("promotion_policy_shape_invalid", `promotion policy ${label} shape is invalid`, recordPath));
158
+ }
159
+ if (value.review_independence !== "single_account") failures.push(finding(typeof value.review_independence === "boolean" ? "review_independence_boolean" : "promotion_policy_shape_invalid", "policy review_independence must be single_account", recordPath));
160
+ if (value.gateway?.transition?.from !== "consumer_local" || value.gateway?.transition?.to !== "shared_experimental" || value.gateway?.applies_to !== "invariant_eligibility_only" || value.gateway?.minimum_independent_consumers !== 2) {
161
+ failures.push(finding("promotion_gateway_invalid", "the >=2 gateway applies only to consumer-local invariant eligibility for shared-experimental", recordPath));
162
+ }
163
+ if (value.gateway?.stable_numeric_threshold !== null) failures.push(finding("promotion_stable_numeric_threshold_forbidden", "stable has no numeric adoption threshold", recordPath));
164
+ const categories = ["accessibility", "correctness", "security", "standards"];
165
+ const retained = ["compatibility", "deprecation", "migration", "owner_acceptance", "regression_proof", "rollback", "scope_control", "support_capacity"];
166
+ if (!sameStringSet(value.normative_bypass?.categories, categories) || !sameStringSet(value.normative_bypass?.waives, ["adoption_count"]) || !sameStringSet(value.normative_bypass?.retains, retained)) {
167
+ failures.push(finding("promotion_normative_bypass_overreach", "normative bypass may waive adoption count only", recordPath));
168
+ }
169
+ if (value.claims?.promotion_occurred !== false || value.claims?.adopter_attestations !== 0) failures.push(finding("promotion_claim_forbidden", "the policy records no promotion and zero adopter attestations", recordPath));
170
+ if (value.status !== "active") failures.push(finding("promotion_claim_forbidden", "the policy status must remain active governance, not a promoted decision", recordPath));
171
+ if (value.schema_version !== "1.0" || value.id !== "shared-experimental-policy" || value.artifact_mode !== "schema_only") failures.push(finding("promotion_policy_identity_invalid", "promotion policy identity and artifact mode are canonical", recordPath));
172
+ if (value.owner?.enforcement !== "placeholder" || value.owner?.name !== "Repository governance owner") failures.push(finding("promotion_policy_owner_invalid", "promotion policy must preserve placeholder owner truth", recordPath));
173
+ const excluded = ["component_skin", "imagery", "motion_character", "palette", "typography"];
174
+ const promoted = ["claim_scoped_evidence", "invariant_behavior", "invariant_semantics"];
175
+ if (!sameStringSet(value.boundaries?.excluded_content, excluded)) {
176
+ failures.push(finding("promotion_aesthetic_boundary_required", "aesthetic identity must remain excluded from promotion", recordPath));
177
+ }
178
+ if (!sameStringSet(value.boundaries?.promoted_content, promoted)) failures.push(finding("promotion_aesthetic_boundary_required", "promoted content must be the invariant-only canonical set", recordPath));
179
+ if (value.lifecycle?.stable_relabel !== "forbidden" || !sameStringSet(value.lifecycle?.failure_actions, ["deprecate_with_migration", "maintain", "restrict", "rollback"])) {
180
+ failures.push(finding("promotion_lifecycle_invalid", "failed stable contracts require explicit lifecycle action and migration", recordPath));
181
+ }
182
+ }
183
+
184
+ const governedProfileInventory = validateGovernedProfileInventory({ root: options.profileRepositoryRoot });
185
+ if (options.profileRepositoryRootExplicit && governedProfileInventory.failures.length > 0) {
186
+ failures.push(finding("promotion_profile_registry_invalid", "explicit governed profile repository must fully validate", "<profile-repository-root>"));
187
+ }
188
+ let checked = 0;
189
+ for (const file of files) {
190
+ const recordPath = path.relative(repositoryRoot, file) || file;
191
+ const resolvedFile = resolvePromotionJsonFile({ file, repositoryRoot }, failures);
192
+ if (!resolvedFile) continue;
193
+ let value;
194
+ try {
195
+ value = parseStrictJson(fs.readFileSync(resolvedFile, "utf8"));
196
+ checked += 1;
197
+ } catch (error) {
198
+ failures.push(finding("promotion_json_invalid", error instanceof Error ? error.message : "promotion JSON is invalid", recordPath));
199
+ continue;
200
+ }
201
+ validateSemantics({ canonicalExample: fixtureInventory.validFiles.includes(resolvedFile), failures, recordPath, value, warnings });
202
+ if (!validateSchema(value)) {
203
+ for (const error of validateSchema.errors ?? []) failures.push(finding("promotion_schema_invalid", `${error.instancePath || "/"} ${error.message}`, recordPath));
204
+ }
205
+ }
206
+ for (const file of policies) {
207
+ const recordPath = path.relative(repositoryRoot, file) || file;
208
+ const resolvedFile = resolvePromotionJsonFile({ file, repositoryRoot }, failures);
209
+ if (!resolvedFile) continue;
210
+ try {
211
+ const value = parseStrictJson(fs.readFileSync(resolvedFile, "utf8"));
212
+ checked += 1;
213
+ validatePolicy(value, recordPath, failures);
214
+ } catch (error) {
215
+ failures.push(finding("promotion_json_invalid", error instanceof Error ? error.message : "promotion policy JSON is invalid", recordPath));
216
+ }
217
+ }
218
+
219
+ const uniqueFailures = [...new Map(failures.map((entry) => [`${entry.code}:${entry.path}:${entry.message}`, entry])).values()];
220
+ const result = { checked, failures: uniqueFailures, ok: uniqueFailures.length === 0, warnings };
221
+ if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
222
+ else if (result.ok) process.stdout.write(`ok: ${checked} promotion proposal records\n`);
223
+ else process.stderr.write(`${result.failures.map((entry) => `${entry.code}: ${entry.path}: ${entry.message}`).join("\n")}\n`);
224
+ if (!result.ok) process.exitCode = 1;
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import { adapter } from "../consumer-reference/adapters/style-dictionary.config.mjs";
9
+ import { inspectArtifactPath } from "./reference-artifact-path-contract.mjs";
10
+ import { validatePortableTokens } from "./reference-token-contract.mjs";
11
+ import { parseStrictJson } from "./strict-json.mjs";
12
+
13
+ function hash(content) {
14
+ return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
15
+ }
16
+
17
+ function cssDeclarations(css) {
18
+ return [...css.matchAll(/^\s*(--[a-z0-9-]+):\s*([^;]+);\s*$/gm)].map((match) => ({ name: match[1], value: match[2].trim() }));
19
+ }
20
+
21
+ function parseArguments() {
22
+ let json = false;
23
+ let manifest = "consumer-reference/generated/manifest.json";
24
+ const failures = [];
25
+ for (let index = 2; index < process.argv.length; index += 1) {
26
+ const argument = process.argv[index];
27
+ if (argument === "--json") json = true;
28
+ else if (argument === "--manifest") {
29
+ manifest = process.argv[index + 1];
30
+ if (!manifest) failures.push({ code: "argument_value_required", message: "--manifest requires a value", path: "<cli>" });
31
+ else index += 1;
32
+ } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "<cli>" });
33
+ }
34
+ return { failures, json, manifest };
35
+ }
36
+
37
+ const options = parseArguments();
38
+ const failures = [...options.failures];
39
+ const warnings = [];
40
+ const trustRoot = process.cwd();
41
+ const manifestPath = path.resolve(trustRoot, options.manifest || "missing.json");
42
+ let manifest;
43
+ const manifestInspection = inspectArtifactPath(trustRoot, manifestPath, false);
44
+ if (!manifestInspection.ok && manifestInspection.reason === "missing") {
45
+ failures.push({ code: "artifact_manifest_missing", message: "manifest does not exist", path: options.manifest });
46
+ failures.push({ code: "artifact_output_missing", message: "output cannot be located without a manifest", path: options.manifest });
47
+ } else if (!manifestInspection.ok) {
48
+ failures.push({ code: "artifact_manifest_untrusted", message: "manifest must be a contained regular non-symlink file", path: options.manifest });
49
+ } else {
50
+ try {
51
+ manifest = parseStrictJson(fs.readFileSync(manifestPath, "utf8"));
52
+ } catch (error) {
53
+ failures.push({ code: "artifact_manifest_invalid", message: error instanceof Error ? error.message : String(error), path: options.manifest });
54
+ }
55
+ }
56
+ if (manifest !== undefined) {
57
+ const allowedManifestProperties = new Set(["adapter", "declarations", "inputHash", "outputCount", "outputFile", "outputHash", "schemaVersion", "sourceCount", "sourceFile", "warnings"]);
58
+ if (manifest.scaffold === true) failures.push({ code: "artifact_scaffold_forbidden", message: "manifest must not contain a scaffold success sentinel", path: options.manifest });
59
+ for (const key of Object.keys(manifest).filter((key) => !allowedManifestProperties.has(key))) failures.push({ code: "artifact_manifest_property_unknown", message: `manifest contains unsupported property ${key}`, path: options.manifest });
60
+ if (manifest.schemaVersion !== "1.0") failures.push({ code: "artifact_schema_version", message: "manifest schemaVersion must be 1.0", path: options.manifest });
61
+ if (manifest.adapter?.name !== adapter.name || manifest.adapter?.version !== adapter.version) failures.push({ code: "artifact_adapter_mismatch", message: "manifest adapter pin is invalid", path: options.manifest });
62
+ if (!Number.isInteger(manifest.sourceCount) || !Number.isInteger(manifest.outputCount) || manifest.sourceCount <= 0 || manifest.outputCount <= 0) failures.push({ code: "artifact_zero_count", message: "source and output counts must be positive integers", path: options.manifest });
63
+ if (manifest.sourceCount !== manifest.outputCount) failures.push({ code: "artifact_count_mismatch", message: "source and output counts differ", path: options.manifest });
64
+ if (!Array.isArray(manifest.warnings)) failures.push({ code: "artifact_warnings_invalid", message: "warnings must be an array", path: options.manifest });
65
+ else if (manifest.warnings.length > 0) failures.push({ code: "artifact_warning", message: "manifest contains adapter warnings", path: options.manifest });
66
+ const declared = Array.isArray(manifest.declarations) ? manifest.declarations : [];
67
+ if (!Array.isArray(manifest.declarations)) failures.push({ code: "artifact_declarations_invalid", message: "declarations must be an array", path: options.manifest });
68
+ const declarationNames = declared.map((item) => item?.name).filter((name) => typeof name === "string");
69
+ if (new Set(declarationNames).size !== declarationNames.length) failures.push({ code: "artifact_declaration_duplicate", message: "manifest contains duplicate declarations", path: options.manifest });
70
+ const sourcePath = typeof manifest.sourceFile === "string" ? path.resolve(path.dirname(manifestPath), manifest.sourceFile) : "";
71
+ const outputPath = typeof manifest.outputFile === "string" ? path.resolve(path.dirname(manifestPath), manifest.outputFile) : "";
72
+ const sourceInspection = sourcePath ? inspectArtifactPath(trustRoot, sourcePath, false) : { ok: false, reason: "missing" };
73
+ const outputInspection = outputPath ? inspectArtifactPath(trustRoot, outputPath, false) : { ok: false, reason: "missing" };
74
+ if (!sourceInspection.ok && sourceInspection.reason === "missing") failures.push({ code: "artifact_source_missing", message: "manifest source file does not exist", path: options.manifest });
75
+ else if (!sourceInspection.ok) failures.push({ code: "token_source_untrusted", message: "manifest source must be a contained regular non-symlink file", path: options.manifest });
76
+ else {
77
+ const sourceBytes = fs.readFileSync(sourcePath);
78
+ if (manifest.inputHash !== hash(sourceBytes)) failures.push({ code: "artifact_input_hash_mismatch", message: "input hash does not match source", path: options.manifest });
79
+ try {
80
+ const contract = validatePortableTokens(parseStrictJson(sourceBytes.toString("utf8")));
81
+ failures.push(...contract.failures);
82
+ if (contract.tokens.length !== manifest.sourceCount) failures.push({ code: "artifact_source_count_mismatch", message: "source count does not match canonical tokens", path: options.manifest });
83
+ const expected = contract.tokens.map((token) => `--${token.path.replaceAll(".", "-")}`);
84
+ for (const name of expected.filter((name) => !declarationNames.includes(name))) failures.push({ code: "artifact_declaration_missing", message: `manifest omitted ${name}`, path: options.manifest });
85
+ } catch (error) {
86
+ failures.push({ code: "artifact_source_invalid", message: error instanceof Error ? error.message : String(error), path: options.manifest });
87
+ }
88
+ }
89
+ if (!outputInspection.ok && outputInspection.reason === "missing") failures.push({ code: "artifact_output_missing", message: "manifest output file does not exist", path: options.manifest });
90
+ else if (!outputInspection.ok) failures.push({ code: "artifact_output_untrusted", message: "manifest output must be a contained regular non-symlink file", path: options.manifest });
91
+ else {
92
+ const css = fs.readFileSync(outputPath, "utf8");
93
+ const emitted = cssDeclarations(css);
94
+ const emittedNames = emitted.map((item) => item.name);
95
+ if (manifest.outputHash !== hash(css)) failures.push({ code: "artifact_output_hash_mismatch", message: "output hash does not match CSS", path: options.manifest });
96
+ if (emitted.length === 0) failures.push({ code: "artifact_zero_count", message: "CSS contains zero declarations", path: options.manifest });
97
+ if (new Set(emittedNames).size !== emittedNames.length) failures.push({ code: "artifact_declaration_duplicate", message: "CSS contains duplicate declarations", path: options.manifest });
98
+ for (const name of declarationNames.filter((name) => !emittedNames.includes(name))) failures.push({ code: "artifact_declaration_missing", message: `CSS omitted ${name}`, path: options.manifest });
99
+ for (const declaration of declared) {
100
+ const actual = emitted.find((item) => item.name === declaration?.name);
101
+ if (actual && actual.value !== declaration.value) failures.push({ code: "artifact_declaration_mismatch", message: `CSS value for ${declaration.name} differs from the manifest`, path: options.manifest });
102
+ }
103
+ if (emitted.length !== manifest.outputCount || emitted.length !== declarationNames.length) failures.push({ code: "artifact_count_mismatch", message: "CSS, manifest count, and declarations differ", path: options.manifest });
104
+ if (css.includes("[object Object]")) failures.push({ code: "artifact_object_sentinel", message: "CSS contains [object Object]", path: options.manifest });
105
+ if (/\{[a-z0-9.-]+\}|\b(?:null|undefined)\b/.test(css)) failures.push({ code: "artifact_unresolved_value", message: "CSS contains an unresolved value", path: options.manifest });
106
+ if (sourceInspection.ok) {
107
+ const rebuildRoot = fs.mkdtempSync(path.join(trustRoot, ".tmp-stylegallery-reference-validation-"));
108
+ try {
109
+ const expectedOutput = path.join(rebuildRoot, "tokens.css");
110
+ const expectedManifest = path.join(rebuildRoot, "manifest.json");
111
+ const builder = path.join(path.dirname(fileURLToPath(import.meta.url)), "build-reference-artifacts.mjs");
112
+ const child = spawnSync(process.execPath, [builder, "--source", sourcePath, "--output", expectedOutput, "--manifest", expectedManifest, "--adapter", "style-dictionary", "--fail-on-warning", "--json"], { cwd: process.cwd(), encoding: "utf8" });
113
+ if (child.status !== 0 || !fs.existsSync(expectedOutput)) failures.push({ code: "artifact_canonical_rebuild_failed", message: "canonical source could not be rebuilt with the pinned adapter", path: options.manifest });
114
+ else if (fs.readFileSync(expectedOutput, "utf8") !== css) failures.push({ code: "artifact_source_output_mismatch", message: "CSS differs from the pinned adapter output derived from canonical source", path: options.manifest });
115
+ } finally {
116
+ fs.rmSync(rebuildRoot, { force: true, recursive: true });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ const uniqueFailures = [...new Map(failures.map((item) => [`${item.code}:${item.path}:${item.message}`, item])).values()];
122
+ const report = { failures: uniqueFailures, manifest: options.manifest, ok: uniqueFailures.length === 0, warnings };
123
+ const text = options.json ? JSON.stringify(report, null, 2) : report.ok ? "ok: reference artifacts" : uniqueFailures.map((item) => `${item.code}: ${item.message}`).join("\n");
124
+ (options.json || report.ok ? console.log : console.error)(text);
125
+ process.exitCode = report.ok ? 0 : 1;
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ let renderer = "tests/helpers/render-consumer-reference.mjs";
10
+ let json = false;
11
+ const failures = [];
12
+ for (let index = 2; index < process.argv.length; index += 1) {
13
+ const argument = process.argv[index];
14
+ if (argument === "--json") json = true;
15
+ else if (argument === "--renderer" && process.argv[index + 1]) {
16
+ renderer = process.argv[index + 1];
17
+ index += 1;
18
+ } else failures.push({ code: "argument_unknown", message: `unsupported argument ${argument}`, path: "<cli>" });
19
+ }
20
+
21
+ function hashProtectedSources() {
22
+ const files = [path.join(repositoryRoot, "CATALOG.md")];
23
+ const queue = [path.join(repositoryRoot, "patterns")];
24
+ while (queue.length > 0) {
25
+ const current = queue.pop();
26
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
27
+ const target = path.join(current, entry.name);
28
+ if (entry.isDirectory()) queue.push(target);
29
+ else if (entry.isFile()) files.push(target);
30
+ }
31
+ }
32
+ const hash = crypto.createHash("sha256");
33
+ for (const file of files.sort()) hash.update(path.relative(repositoryRoot, file)).update(fs.readFileSync(file));
34
+ return hash.digest("hex");
35
+ }
36
+
37
+ const rendererPath = path.resolve(repositoryRoot, renderer);
38
+ if (!fs.existsSync(rendererPath)) failures.push({ code: "renderer_missing", message: "renderer helper does not exist", path: renderer });
39
+ else {
40
+ const source = fs.readFileSync(rendererPath, "utf8");
41
+ const imports = [...source.matchAll(/^import\s+[^;]+?\s+from\s+["']([^"']+)["'];?$/gm)].map((match) => match[1]);
42
+ if (imports.length !== 1 || imports[0] !== "../../scripts/pattern-data.mjs") failures.push({ code: "renderer_import_boundary", message: "renderer may import only scripts/pattern-data.mjs", path: renderer });
43
+ if (/generate-patterns|build-reference-artifacts/.test(source)) failures.push({ code: "renderer_generator_dependency", message: "renderer must not import or call generators", path: renderer });
44
+ if (/node:(?:fs|child_process)|\b(?:writeFile|appendFile|rmSync|renameSync|spawnSync|execSync)\b/.test(source)) failures.push({ code: "renderer_side_effect_capability", message: "renderer must not hold filesystem or process mutation capability", path: renderer });
45
+ if (failures.length === 0) {
46
+ const before = hashProtectedSources();
47
+ await import(`${pathToFileURL(rendererPath).href}?purity=${Date.now()}`);
48
+ const after = hashProtectedSources();
49
+ if (after !== before) failures.push({ code: "renderer_protected_source_mutation", message: "renderer import changed patterns or CATALOG.md", path: renderer });
50
+ }
51
+ }
52
+
53
+ const result = { failures, ok: failures.length === 0, warnings: [] };
54
+ if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
55
+ else if (!result.ok) process.stderr.write(`${failures.map((failure) => `${failure.code}: ${failure.message}`).join("\n")}\n`);
56
+ if (!result.ok) process.exitCode = 1;
@@ -0,0 +1,146 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import path from "node:path";
4
+
5
+ const root = process.cwd();
6
+ const trackedInventorySha256 = "b1438e6e8d438bab3eff2ff02f5a10e9374fdfb89b0581ffb936a42920fa2e05";
7
+ const sourceInventorySha256 = "8291b471251053a8921651eb7c791d13a4794984b73670a51cdb1116deb49657";
8
+ const snapshotTimestamp = "2026-07-14T12:59:16Z";
9
+ const expectedCounts = new Map([
10
+ ["meta-forks-mirrors", 356],
11
+ ["meta-archived-historical", 32],
12
+ ["game-ui-primary", 9],
13
+ ["graphics-rendering", 38],
14
+ ["ecs-dots", 22],
15
+ ["networking", 21],
16
+ ["xr-ar", 25],
17
+ ["ml-ai", 23],
18
+ ["robotics", 6],
19
+ ["2d", 7],
20
+ ["animation", 10],
21
+ ["audio", 3],
22
+ ["physics", 1],
23
+ ["editor-tooling", 29],
24
+ ["ci-infra", 2],
25
+ ["language-runtime", 3],
26
+ ["services-cloud", 3],
27
+ ["platform-mobile", 9],
28
+ ["samples-templates", 30],
29
+ ["other-mixed-or-insufficient-metadata", 175],
30
+ ]);
31
+
32
+ function parseArguments(arguments_) {
33
+ const options = {
34
+ inventory: path.join(root, "game-ui", "unity", "data", "unity-technologies-public-repositories.json"),
35
+ map: path.join(root, "game-ui", "unity", "repository-map.md"),
36
+ wiki: path.join(root, "game-ui", "unity", "org-wiki.md"),
37
+ };
38
+ for (let index = 0; index < arguments_.length; index += 1) {
39
+ const argument = arguments_[index];
40
+ if (argument === "--json") continue;
41
+ if (argument === "--inventory" || argument === "--map" || argument === "--wiki") {
42
+ const value = arguments_[index + 1];
43
+ if (!value) throw new Error(`${argument} requires a path`);
44
+ options[argument.slice(2)] = path.resolve(value);
45
+ index += 1;
46
+ continue;
47
+ }
48
+ throw new Error(`unknown argument: ${argument}`);
49
+ }
50
+ return options;
51
+ }
52
+
53
+ function requireCondition(condition, message) {
54
+ if (!condition) throw new Error(message);
55
+ }
56
+
57
+ function validateInventory(inventory) {
58
+ requireCondition(Array.isArray(inventory), "inventory must be a JSON array");
59
+ requireCondition(inventory.length === 804, `expected 804 repositories, got ${inventory.length}`);
60
+ const ids = new Set();
61
+ const names = new Set();
62
+ const counts = new Map([...expectedCounts.keys()].map((cluster) => [cluster, 0]));
63
+ let archivedCount = 0;
64
+ let forkCount = 0;
65
+ let templateCount = 0;
66
+
67
+ for (const repository of inventory) {
68
+ requireCondition(repository && typeof repository === "object", "repository row must be an object");
69
+ requireCondition(Number.isSafeInteger(repository.id), "repository id must be an integer");
70
+ requireCondition(typeof repository.full_name === "string", "repository full_name must be a string");
71
+ requireCondition(!ids.has(repository.id) && !names.has(repository.full_name), `duplicate repository identity: ${repository.full_name}`);
72
+ requireCondition(repository.full_name !== "tasharen/ngui", "NGUI must remain external to the Unity-Technologies inventory");
73
+ requireCondition(repository.full_name.startsWith("Unity-Technologies/"), `repository outside Unity-Technologies: ${repository.full_name}`);
74
+ requireCondition(repository.html_url === `https://github.com/${repository.full_name}`, `repository URL mismatch: ${repository.full_name}`);
75
+ requireCondition(repository.capture_timestamp === snapshotTimestamp, `snapshot timestamp mismatch: ${repository.full_name}`);
76
+ requireCondition(expectedCounts.has(repository.primary_cluster), `invalid primary_cluster: ${repository.primary_cluster}`);
77
+ requireCondition(typeof repository.ui_relevance === "string" && repository.ui_relevance.length > 0, `UI relevance missing: ${repository.full_name}`);
78
+ requireCondition(typeof repository.ui_disposition_reason === "string" && repository.ui_disposition_reason.length > 0, `UI disposition reason missing: ${repository.full_name}`);
79
+ requireCondition(Array.isArray(repository.representative_ui_paths), `representative UI paths must be an array: ${repository.full_name}`);
80
+ ids.add(repository.id);
81
+ names.add(repository.full_name);
82
+ counts.set(repository.primary_cluster, counts.get(repository.primary_cluster) + 1);
83
+ if (repository.archived) archivedCount += 1;
84
+ if (repository.fork) forkCount += 1;
85
+ if (repository.is_template) templateCount += 1;
86
+ }
87
+
88
+ for (const [cluster, expected] of expectedCounts) {
89
+ requireCondition(counts.get(cluster) === expected, `cluster count mismatch for ${cluster}: expected ${expected}, got ${counts.get(cluster)}`);
90
+ }
91
+ requireCondition(archivedCount === 59, `archived repository count mismatch: expected 59, got ${archivedCount}`);
92
+ requireCondition(forkCount === 356, `fork repository count mismatch: expected 356, got ${forkCount}`);
93
+ requireCondition(templateCount === 4, `template repository count mismatch: expected 4, got ${templateCount}`);
94
+ return { archivedCount, counts: Object.fromEntries(counts), forkCount, templateCount };
95
+ }
96
+
97
+ function validatePublication(content, label) {
98
+ requireCondition(content.includes("804"), `${label} must state the 804-row boundary`);
99
+ const sourceHashes = [...content.matchAll(/(?:pre-projection|before tracked projection)[^`\n]*SHA-256(?: was)? `([0-9a-f]{64})`/g)].map((match) => match[1]);
100
+ const trackedHashes = [...content.matchAll(/tracked JSON[^`\n]*SHA-256(?: is)? `([0-9a-f]{64})`/g)].map((match) => match[1]);
101
+ requireCondition(sourceHashes.length > 0 && sourceHashes.every((hash) => hash === sourceInventorySha256), `${label} source inventory hash drift`);
102
+ requireCondition(trackedHashes.length > 0 && trackedHashes.every((hash) => hash === trackedInventorySha256), `${label} tracked inventory hash drift`);
103
+ requireCondition(content.includes(snapshotTimestamp), `${label} snapshot timestamp drift`);
104
+ requireCondition(content.includes("59 archived") && content.includes("356 forks") && content.includes("4 templates"), `${label} lifecycle totals drift`);
105
+ }
106
+
107
+ function validateWiki(wiki) {
108
+ requireCondition(wiki.includes("**804**"), "wiki must state the 804-row boundary");
109
+ requireCondition(wiki.includes("Independent research — not affiliated with or endorsed by Unity Technologies"), "wiki must state the non-affiliation boundary");
110
+ requireCondition(wiki.includes("tasharen/ngui") && wiki.includes("outside the Unity-Technologies 804-repository inventory"), "wiki must keep NGUI external");
111
+ requireCondition(wiki.includes("data/unity-technologies-public-repositories.json"), "wiki must link the tracked inventory");
112
+ requireCondition(!wiki.includes(".omo/"), "tracked wiki must not depend on .omo research paths");
113
+ requireCondition(!/(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)/.test(wiki), "absolute home path found in wiki");
114
+ for (const [cluster, count] of expectedCounts) {
115
+ requireCondition(wiki.includes(`| \`${cluster}\` | ${count} |`), `wiki cluster table drift: ${cluster}`);
116
+ }
117
+ validatePublication(wiki, "wiki");
118
+ }
119
+
120
+ function validateMap(map) {
121
+ requireCondition(map.includes("data/unity-technologies-public-repositories.json"), "repository map must link the tracked inventory");
122
+ requireCondition(map.includes("804 classified rows"), "repository map must state the 804-row boundary");
123
+ validatePublication(map, "repository map");
124
+ }
125
+
126
+ async function main() {
127
+ try {
128
+ const options = parseArguments(process.argv.slice(2));
129
+ const [inventoryText, map, wiki] = await Promise.all([
130
+ readFile(options.inventory, "utf8"),
131
+ readFile(options.map, "utf8"),
132
+ readFile(options.wiki, "utf8"),
133
+ ]);
134
+ const stats = validateInventory(JSON.parse(inventoryText));
135
+ const inventorySha256 = createHash("sha256").update(inventoryText).digest("hex");
136
+ requireCondition(inventorySha256 === trackedInventorySha256, `tracked inventory SHA-256 mismatch: expected ${trackedInventorySha256}, got ${inventorySha256}`);
137
+ validateMap(map);
138
+ validateWiki(wiki);
139
+ console.log(JSON.stringify({ ok: true, repositories: 804, uniqueIds: 804, uniqueNames: 804, inventorySha256, clusterTotal: Object.values(stats.counts).reduce((sum, value) => sum + value, 0), ...stats }, null, 2));
140
+ } catch (error) {
141
+ console.error(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
142
+ process.exitCode = 1;
143
+ }
144
+ }
145
+
146
+ await main();