stylegallery 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/AGENTS.md +4 -1
  2. package/DOMAINS.md +2 -6
  3. package/GOVERNANCE.md +10 -9
  4. package/README.md +23 -127
  5. package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
  6. package/consumer-reference/agent-native/v2/material-registry.json +1 -0
  7. package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
  8. package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
  9. package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
  10. package/consumer-reference/contract.md +1 -1
  11. package/consumer-reference/index.md +5 -3
  12. package/layout/index.md +124 -0
  13. package/log.md +51 -0
  14. package/package.json +181 -22
  15. package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
  16. package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
  17. package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
  18. package/scripts/agent-native/v2/material-admission.mjs +295 -0
  19. package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
  20. package/scripts/agent-native/v2/material-context.mjs +251 -0
  21. package/scripts/agent-native/v2/material-identity.mjs +71 -0
  22. package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
  23. package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
  24. package/scripts/agent-native/v2/material-queries.mjs +398 -0
  25. package/scripts/agent-native/v2/material-registry.mjs +319 -0
  26. package/scripts/sg-material-mcp.mjs +98 -0
  27. package/scripts/sg-material.mjs +27 -0
  28. package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
  29. package/consumer-reference/baselines/calibration.json +0 -260
  30. package/consumer-reference/baselines/manifest.json +0 -21
  31. package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
  32. package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
  33. package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
  34. package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
  35. package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
  36. package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
  37. package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
  38. package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
  39. package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
  40. package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
  41. package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
  42. package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
  43. package/consumer-reference/fixtures/promotion/manifest.json +0 -15
  44. package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
  45. package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
  46. package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
  47. package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
  48. package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
  49. package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
  50. package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
  51. package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
  52. package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
  53. package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
  54. package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
  55. package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
  56. package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
  57. package/consumer-reference/fixtures/valid-deprecated.json +0 -21
  58. package/consumer-reference/fixtures/valid-experimental.json +0 -19
  59. package/consumer-reference/generated/manifest.json +0 -40
  60. package/consumer-reference/generated/tokens.css +0 -12
  61. package/consumer-reference/policies/shared-experimental.json +0 -37
  62. package/consumer-reference/schema/ax-evidence.schema.json +0 -30
  63. package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
  64. package/consumer-reference/schema/calibration-record.schema.json +0 -190
  65. package/consumer-reference/schema/capture-session.schema.json +0 -161
  66. package/consumer-reference/schema/component-state.schema.json +0 -150
  67. package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
  68. package/consumer-reference/schema/dom-evidence.schema.json +0 -38
  69. package/consumer-reference/schema/evidence-record.schema.json +0 -81
  70. package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
  71. package/consumer-reference/schema/item.schema.json +0 -188
  72. package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
  73. package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
  74. package/consumer-reference/schema/portable-tokens.schema.json +0 -104
  75. package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
  76. package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
  77. package/consumer-reference/schema/visual-evidence.schema.json +0 -31
  78. package/design-engineering/consumer-migration-readiness.md +0 -124
  79. package/design-engineering/interface-craft.md +0 -107
  80. package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
  81. package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
  82. package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
  83. package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
  84. package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
  85. package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
  86. package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
  87. package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
  88. package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
  89. package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
  90. package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
  91. package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
  92. package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
  93. package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
  94. package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
  95. package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
  96. package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
  97. package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
  98. package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
  99. package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
  100. package/game-ui/classification.md +0 -82
  101. package/game-ui/reference-record.md +0 -62
  102. package/game-ui/screen-hierarchy.md +0 -107
  103. package/game-ui/unity/architecture.md +0 -166
  104. package/game-ui/unity/cli-loop.md +0 -84
  105. package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
  106. package/game-ui/unity/org-wiki.md +0 -146
  107. package/game-ui/unity/repository-map.md +0 -102
  108. package/game-ui/unity/ui-systems.md +0 -85
  109. package/motion/practice-reference.md +0 -145
  110. package/motion/review-workflow.md +0 -86
  111. package/motion/vocabulary.md +0 -82
  112. package/platform-guides/apple-interaction.md +0 -89
  113. package/scripts/agent-native/events.mjs +0 -113
  114. package/scripts/agent-native/immutable-store.mjs +0 -102
  115. package/scripts/artifact-metadata.mjs +0 -20
  116. package/scripts/audit-evidence-freshness.mjs +0 -308
  117. package/scripts/baseline-contract.mjs +0 -189
  118. package/scripts/baseline-schema-parity.mjs +0 -40
  119. package/scripts/build-reference-artifacts.mjs +0 -127
  120. package/scripts/calibration-raw-contract.mjs +0 -172
  121. package/scripts/capture-session-contract.mjs +0 -187
  122. package/scripts/component-state-artifact-fixture.mjs +0 -44
  123. package/scripts/component-state-contract.mjs +0 -99
  124. package/scripts/component-state-semantics.mjs +0 -128
  125. package/scripts/component-state-workflow-contract.mjs +0 -67
  126. package/scripts/component-state-workflow-negative-cases.mjs +0 -64
  127. package/scripts/consumer-conformance-contract.mjs +0 -198
  128. package/scripts/consumer-evidence-governance-contract.mjs +0 -210
  129. package/scripts/consumer-reference-case-runner.mjs +0 -93
  130. package/scripts/consumer-reference-schema.mjs +0 -155
  131. package/scripts/create-component-state-session.mjs +0 -100
  132. package/scripts/create-page-evidence-session.mjs +0 -139
  133. package/scripts/domain-document-boundaries.mjs +0 -40
  134. package/scripts/domain-validation-checks.mjs +0 -141
  135. package/scripts/evidence-artifact-contract.mjs +0 -191
  136. package/scripts/finalize-component-state-evidence.mjs +0 -165
  137. package/scripts/finalize-page-evidence.mjs +0 -197
  138. package/scripts/generate-consumer-reference-evidence.mjs +0 -154
  139. package/scripts/generate-patterns.mjs +0 -218
  140. package/scripts/governance-matrix-contract.mjs +0 -73
  141. package/scripts/governance-matrix-negative-cases.mjs +0 -114
  142. package/scripts/governance-policy-contract.mjs +0 -49
  143. package/scripts/governance-test-fixture.mjs +0 -321
  144. package/scripts/governance-workflow-negative-cases.mjs +0 -241
  145. package/scripts/governed-profile-registry.mjs +0 -125
  146. package/scripts/json-schema-formats.mjs +0 -31
  147. package/scripts/markdown-structure.mjs +0 -45
  148. package/scripts/page-artifact-metadata.mjs +0 -124
  149. package/scripts/page-evidence-contract.mjs +0 -297
  150. package/scripts/page-evidence-fixture.mjs +0 -271
  151. package/scripts/pattern-data.mjs +0 -325
  152. package/scripts/profile-record-contract.mjs +0 -140
  153. package/scripts/promotion-attestation-contract.mjs +0 -91
  154. package/scripts/promotion-boundary-test-contract.mjs +0 -92
  155. package/scripts/promotion-fixture-inventory.mjs +0 -109
  156. package/scripts/promotion-governance-contract.mjs +0 -28
  157. package/scripts/reference-artifact-path-contract.mjs +0 -28
  158. package/scripts/reference-profile-contract.mjs +0 -114
  159. package/scripts/reference-token-contract.mjs +0 -156
  160. package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
  161. package/scripts/summarize-sentinel-calibration.mjs +0 -98
  162. package/scripts/test-agent-adapter-conformance.mjs +0 -250
  163. package/scripts/test-agent-conformance-receipt.mjs +0 -106
  164. package/scripts/test-agent-events.mjs +0 -81
  165. package/scripts/test-agent-execution-effects.mjs +0 -114
  166. package/scripts/test-agent-execution-fixtures.mjs +0 -140
  167. package/scripts/test-agent-execution-kernel.mjs +0 -39
  168. package/scripts/test-agent-execution-learning.mjs +0 -130
  169. package/scripts/test-agent-execution-lifecycle.mjs +0 -143
  170. package/scripts/test-agent-execution-safety.mjs +0 -158
  171. package/scripts/test-agent-execution-views.mjs +0 -117
  172. package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
  173. package/scripts/test-agent-knowledge-kernel.mjs +0 -183
  174. package/scripts/test-agent-learning.mjs +0 -246
  175. package/scripts/test-agent-mcp.mjs +0 -246
  176. package/scripts/test-agent-native-schemas.mjs +0 -215
  177. package/scripts/test-agent-projections.mjs +0 -249
  178. package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
  179. package/scripts/test-agent-registry-mutations.mjs +0 -110
  180. package/scripts/test-audit-evidence-freshness.mjs +0 -177
  181. package/scripts/test-component-state-sentinel.mjs +0 -35
  182. package/scripts/test-component-state-source-contract.mjs +0 -83
  183. package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
  184. package/scripts/test-consumer-reference-sentinel.mjs +0 -31
  185. package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
  186. package/scripts/test-json-cli-output.mjs +0 -151
  187. package/scripts/test-json-schema-formats.mjs +0 -40
  188. package/scripts/test-reference-adapters.mjs +0 -199
  189. package/scripts/test-renderer-purity.mjs +0 -34
  190. package/scripts/test-sg-cli.mjs +0 -129
  191. package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
  192. package/scripts/test-validate-baseline-manifest.mjs +0 -234
  193. package/scripts/test-validate-component-state-artifacts.mjs +0 -243
  194. package/scripts/test-validate-component-state.mjs +0 -106
  195. package/scripts/test-validate-consumer-conformance.mjs +0 -427
  196. package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
  197. package/scripts/test-validate-consumer-reference.mjs +0 -263
  198. package/scripts/test-validate-domains.mjs +0 -357
  199. package/scripts/test-validate-governance.mjs +0 -292
  200. package/scripts/test-validate-ia.mjs +0 -262
  201. package/scripts/test-validate-okf.mjs +0 -73
  202. package/scripts/test-validate-page-evidence.mjs +0 -302
  203. package/scripts/test-validate-patterns.mjs +0 -157
  204. package/scripts/test-validate-promotion-rfc.mjs +0 -250
  205. package/scripts/test-validate-reference-profiles.mjs +0 -170
  206. package/scripts/test-validate-unity-org-wiki.mjs +0 -57
  207. package/scripts/test-validate-unity-source-contracts.mjs +0 -67
  208. package/scripts/test-validate-vocabulary.mjs +0 -174
  209. package/scripts/test-validate-webpage-workflow.mjs +0 -293
  210. package/scripts/validate-baseline-manifest.mjs +0 -44
  211. package/scripts/validate-catalog.mjs +0 -65
  212. package/scripts/validate-component-state.mjs +0 -141
  213. package/scripts/validate-consumer-conformance.mjs +0 -477
  214. package/scripts/validate-consumer-reference.mjs +0 -288
  215. package/scripts/validate-domains.mjs +0 -237
  216. package/scripts/validate-governance.mjs +0 -248
  217. package/scripts/validate-ia.mjs +0 -136
  218. package/scripts/validate-links.mjs +0 -66
  219. package/scripts/validate-okf.mjs +0 -104
  220. package/scripts/validate-page-evidence.mjs +0 -191
  221. package/scripts/validate-patterns.mjs +0 -246
  222. package/scripts/validate-promotion-rfc.mjs +0 -224
  223. package/scripts/validate-reference-artifacts.mjs +0 -125
  224. package/scripts/validate-renderer-purity.mjs +0 -56
  225. package/scripts/validate-unity-org-wiki.mjs +0 -146
  226. package/scripts/validate-unity-source-contracts.mjs +0 -107
  227. package/scripts/validate-vocabulary.mjs +0 -115
  228. package/scripts/validate-webpage-workflow.mjs +0 -254
  229. package/scripts/visual-expectation-contract.mjs +0 -20
  230. package/scripts/workflow-action-contract.mjs +0 -163
@@ -1,140 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { parseStrictJson } from "./strict-json.mjs";
4
-
5
- const referenceKinds = Object.freeze({
6
- component: { field: "component_records", pattern: /^components\/[a-z0-9-]+\.component\.json$/, required: 1 },
7
- evidence: { field: "evidence_records", pattern: /^evidence\/[a-z0-9-]+\.evidence\.json$/, required: 1 },
8
- fixture: { field: "fixture_records", pattern: /^fixtures\/[a-z0-9-]+\.fixture\.json$/, required: 1 },
9
- generated: { field: "generated_records", pattern: /^generated\/[a-z0-9-]+\.md$/, required: 3 },
10
- states: { field: "state_records", pattern: /^states\/[a-z0-9-]+\.states\.json$/, required: 1 },
11
- });
12
-
13
- function finding(code, file, message) {
14
- return { code, message, path: file };
15
- }
16
-
17
- function isInside(base, target) {
18
- const relative = path.relative(base, target);
19
- return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
20
- }
21
-
22
- function isNormalizedRelative(reference) {
23
- return typeof reference === "string"
24
- && reference.length > 0
25
- && !path.posix.isAbsolute(reference)
26
- && !path.win32.isAbsolute(reference)
27
- && !reference.includes("\\")
28
- && !reference.includes("?")
29
- && !reference.includes("#")
30
- && !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference)
31
- && path.posix.normalize(reference) === reference
32
- && !reference.split("/").some((segment) => segment === "." || segment === "..");
33
- }
34
-
35
- function resolveReference(profileRoot, reference, kind, profileFile, failures) {
36
- const definition = referenceKinds[kind];
37
- if (!isNormalizedRelative(reference)) {
38
- failures.push(finding("profile_reference_outside", profileFile, `${definition.field} contains an unsafe reference`));
39
- return undefined;
40
- }
41
- if (!definition.pattern.test(reference)) {
42
- failures.push(finding("profile_reference_unknown", profileFile, `${definition.field} contains unknown reference ${reference}`));
43
- return undefined;
44
- }
45
- const target = path.resolve(profileRoot, reference);
46
- if (!isInside(profileRoot, target)) {
47
- failures.push(finding("profile_reference_outside", profileFile, `${reference} escapes its profile`));
48
- return undefined;
49
- }
50
- if (!fs.existsSync(target)) {
51
- failures.push(finding("profile_reference_missing", profileFile, `${reference} does not exist`));
52
- return undefined;
53
- }
54
- const metadata = fs.lstatSync(target);
55
- if (metadata.isSymbolicLink()) {
56
- failures.push(finding("profile_reference_symlink", profileFile, `${reference} must not be a symlink`));
57
- return undefined;
58
- }
59
- if (!metadata.isFile()) {
60
- failures.push(finding("profile_reference_type_invalid", profileFile, `${reference} must be a regular file`));
61
- return undefined;
62
- }
63
- const realRoot = fs.realpathSync(profileRoot);
64
- const realTarget = fs.realpathSync(target);
65
- if (!isInside(realRoot, realTarget)) {
66
- failures.push(finding("profile_reference_outside", profileFile, `${reference} resolves outside its profile`));
67
- return undefined;
68
- }
69
- if (realTarget !== path.join(realRoot, reference)) {
70
- failures.push(finding("profile_reference_redirect", profileFile, `${reference} resolves through a redirect`));
71
- return undefined;
72
- }
73
- if (kind === "generated") return { path: realTarget, reference };
74
- try {
75
- return { path: realTarget, reference, value: parseStrictJson(fs.readFileSync(realTarget, "utf8")) };
76
- } catch (error) {
77
- failures.push(finding("profile_reference_json_invalid", reference, error instanceof Error ? error.message : String(error)));
78
- return undefined;
79
- }
80
- }
81
-
82
- function resolveFixtureLinks(resolved, profileFile, failures) {
83
- const fixture = resolved.fixture[0];
84
- const component = resolved.component[0];
85
- const states = resolved.states[0];
86
- if (!fixture || !component || !states) return;
87
- for (const [field, expected] of [["component_record", component.path], ["state_record", states.path]]) {
88
- const reference = fixture.value[field];
89
- const safe = typeof reference === "string"
90
- && reference.length > 0
91
- && !path.posix.isAbsolute(reference)
92
- && !path.win32.isAbsolute(reference)
93
- && !reference.includes("\\")
94
- && !reference.includes("?")
95
- && !reference.includes("#")
96
- && !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference)
97
- && path.posix.normalize(reference) === reference;
98
- if (!safe) {
99
- failures.push(finding("fixture_reference_invalid", fixture.reference, `${field} must be normalized and profile-local`));
100
- continue;
101
- }
102
- const actual = path.resolve(path.dirname(fixture.path), reference);
103
- if (actual !== expected) failures.push(finding("fixture_reference_mismatch", fixture.reference, `${field} does not resolve to the profile-declared record`));
104
- }
105
- if (fixture.value.profile_id !== component.value.profile_id || fixture.value.profile_id !== states.value.profile_id) {
106
- failures.push(finding("profile_identity_mismatch", profileFile, "fixture links cross profile identities"));
107
- }
108
- }
109
-
110
- export function resolveProfileRecords(profileRoot, failures) {
111
- const profileFile = path.join(profileRoot, "profile.json");
112
- if (!fs.existsSync(profileFile) || fs.lstatSync(profileFile).isSymbolicLink() || !fs.lstatSync(profileFile).isFile()) {
113
- failures.push(finding("profile_file_invalid", profileFile, "profile.json must be a regular non-symlink file"));
114
- return undefined;
115
- }
116
- let profile;
117
- try {
118
- profile = parseStrictJson(fs.readFileSync(profileFile, "utf8"));
119
- } catch (error) {
120
- failures.push(finding("profile_json_invalid", profileFile, error instanceof Error ? error.message : String(error)));
121
- return undefined;
122
- }
123
- const resolved = {};
124
- for (const [kind, definition] of Object.entries(referenceKinds)) {
125
- const references = profile[definition.field];
126
- if (!Array.isArray(references)) {
127
- failures.push(finding("profile_reference_required", profileFile, `${definition.field} must contain exactly ${definition.required} references`));
128
- resolved[kind] = [];
129
- continue;
130
- }
131
- if (new Set(references).size !== references.length) failures.push(finding("profile_reference_duplicate", profileFile, `${definition.field} must not repeat references`));
132
- if (references.length !== definition.required) failures.push(finding("profile_reference_required", profileFile, `${definition.field} must contain exactly ${definition.required} references`));
133
- resolved[kind] = references.map((reference) => resolveReference(profileRoot, reference, kind, profileFile, failures)).filter(Boolean);
134
- }
135
- for (const record of [resolved.component[0], resolved.evidence[0], resolved.fixture[0], resolved.states[0]].filter(Boolean)) {
136
- if (record.value.profile_id !== profile.id) failures.push(finding("profile_identity_mismatch", record.reference, `record profile_id must equal ${profile.id}`));
137
- }
138
- resolveFixtureLinks(resolved, profileFile, failures);
139
- return { profile, profileFile, records: resolved };
140
- }
@@ -1,91 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { isPlainObject } from "./consumer-reference-schema.mjs";
4
- import { canonicalGovernedProfilePaths, isIndependentPromotionProfile } from "./governed-profile-registry.mjs";
5
-
6
- function finding(code, message, recordPath) {
7
- return { code, message, path: recordPath };
8
- }
9
-
10
- function safeProfilePath(relative, profileRoot, recordPath, failures) {
11
- if (typeof relative !== "string" || !/^(?!\/)(?!.*\/\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?![A-Za-z][A-Za-z0-9+.-]*:)[^\\?#]+\.json$/.test(relative)) {
12
- failures.push(finding("promotion_profile_path_invalid", "profile_record must be a normalized repository-relative JSON path", recordPath));
13
- return undefined;
14
- }
15
- const target = path.resolve(profileRoot, relative);
16
- if (target === profileRoot || !target.startsWith(`${profileRoot}${path.sep}`)) {
17
- failures.push(finding("promotion_profile_path_invalid", "profile_record escapes the repository", recordPath));
18
- return undefined;
19
- }
20
- let current = profileRoot;
21
- for (const segment of path.relative(profileRoot, target).split(path.sep)) {
22
- current = path.join(current, segment);
23
- if (!fs.existsSync(current)) {
24
- failures.push(finding("promotion_profile_missing", `${relative} does not exist`, recordPath));
25
- return undefined;
26
- }
27
- if (fs.lstatSync(current).isSymbolicLink()) {
28
- failures.push(finding("promotion_profile_path_symlink", `${relative} traverses a symlink`, recordPath));
29
- return undefined;
30
- }
31
- }
32
- if (!fs.lstatSync(target).isFile()) {
33
- failures.push(finding("promotion_profile_invalid", `${relative} must be a regular JSON file`, recordPath));
34
- return undefined;
35
- }
36
- const realRoot = fs.realpathSync(profileRoot);
37
- const real = fs.realpathSync(target);
38
- if (!real.startsWith(`${realRoot}${path.sep}`) || real !== path.join(realRoot, relative)) {
39
- failures.push(finding("promotion_profile_path_invalid", `${relative} resolves outside the repository`, recordPath));
40
- return undefined;
41
- }
42
- return relative;
43
- }
44
-
45
- export function validatePromotionAttestations({ inventory, profileRoot, recordPath, value }) {
46
- const attestations = Array.isArray(value.attestations) ? value.attestations : [];
47
- const consumers = new Set();
48
- const countedContexts = new Set();
49
- const countedOrganizations = new Set();
50
- const failures = [];
51
- const identities = new Set();
52
- let eligibleCount = 0;
53
- for (const attestation of attestations) {
54
- if (!isPlainObject(attestation)) continue;
55
- const identity = `${attestation.consumer_id ?? ""}:${attestation.context_id ?? ""}:${attestation.organization_id ?? ""}`;
56
- if (identities.has(identity) || consumers.has(attestation.consumer_id)) failures.push(finding("promotion_attestation_duplicate", `duplicate consumer or attestation identity ${identity}`, recordPath));
57
- identities.add(identity);
58
- consumers.add(attestation.consumer_id);
59
- const counted = attestation.count_toward_gate === true;
60
- if (attestation.profile_record === undefined) {
61
- failures.push(finding("promotion_attestation_profile_required", `${identity} has no canonical repository-local profile record`, recordPath));
62
- if (counted) failures.push(finding("promotion_independence_unproven", `${identity} cannot establish counted independence without a profile record`, recordPath));
63
- continue;
64
- }
65
- const registeredPath = safeProfilePath(attestation.profile_record, profileRoot, recordPath, failures);
66
- if (!registeredPath) continue;
67
- const registered = inventory.records.get(registeredPath);
68
- if (!canonicalGovernedProfilePaths.includes(registeredPath) || !registered || registered.failures.length > 0 || inventory.failures.length > 0) {
69
- failures.push(finding("promotion_independence_unproven", `${attestation.consumer_id} is not a fully validated registered governed profile`, recordPath));
70
- continue;
71
- }
72
- const profile = registered.profile;
73
- if (!isPlainObject(profile)) continue;
74
- const identityMatches = profile.id === attestation.consumer_id;
75
- if (!identityMatches) failures.push(finding("promotion_profile_identity_mismatch", `${attestation.profile_record} does not match ${attestation.consumer_id}`, recordPath));
76
- if (!counted) continue;
77
- if (attestation.relationship !== "independent") failures.push(finding("promotion_related_consumer_counted", `${identity} is related and cannot count toward the independent gateway`, recordPath));
78
- if (countedOrganizations.has(attestation.organization_id) || countedContexts.has(attestation.context_id)) {
79
- failures.push(finding("promotion_independence_unproven", `${identity} does not establish a distinct organization and context`, recordPath));
80
- }
81
- countedOrganizations.add(attestation.organization_id);
82
- countedContexts.add(attestation.context_id);
83
- const canonicalIndependent = profile.fixture_independence === "independent" && typeof profile.related_fixture_set_id !== "string";
84
- if (!canonicalIndependent) failures.push(finding("promotion_related_consumer_counted", `${attestation.consumer_id} is a canonical related fixture and cannot count`, recordPath));
85
- if (identityMatches && attestation.relationship === "independent" && canonicalIndependent && isIndependentPromotionProfile(profile)) eligibleCount += 1;
86
- else if (identityMatches && canonicalIndependent && !isIndependentPromotionProfile(profile)) {
87
- failures.push(finding("promotion_independence_unproven", `${attestation.consumer_id} is not lifecycle- and scope-eligible independent adoption evidence`, recordPath));
88
- }
89
- }
90
- return { eligibleCount, failures };
91
- }
@@ -1,92 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- export function copyTreeNoSymlinks(source, target) {
5
- fs.cpSync(source, target, { recursive: true, filter: (candidate) => {
6
- if (fs.lstatSync(candidate).isSymbolicLink()) throw new Error(`refusing to copy symlink ${candidate}`);
7
- return true;
8
- } });
9
- }
10
-
11
- function copyFixtureRoot(fixtureRoot, tempRoot, name) {
12
- const root = path.join(tempRoot, name);
13
- copyTreeNoSymlinks(fixtureRoot, root);
14
- return root;
15
- }
16
-
17
- function editJson(file, mutate) {
18
- const value = JSON.parse(fs.readFileSync(file, "utf8"));
19
- mutate(value);
20
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
21
- }
22
-
23
- function result(runArguments, name, args, expected) {
24
- const child = runArguments(args);
25
- const codes = child.report.failures.map((failure) => failure.code);
26
- return { actual: { codes, status: child.status }, expected, name, ok: child.status > 0 && codes.includes(expected) };
27
- }
28
-
29
- export function testPromotionBoundaries({ fixtureRoot, repositoryRoot, runArguments, tempRoot }) {
30
- const results = [];
31
- for (const [name, args] of [["file_option_value", ["--file", "--json"]], ["policy_option_value", ["--policy", "--json"]], ["profile_root_option_value", ["--profile-repository-root", "--json"]]]) {
32
- results.push(result(runArguments, name, args, "argument_value_required"));
33
- }
34
- const yamlRfc = path.join(tempRoot, "promotion.yaml");
35
- const yamlPolicy = path.join(tempRoot, "policy.yaml");
36
- fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), yamlRfc);
37
- fs.copyFileSync(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json"), yamlPolicy);
38
- results.push(result(runArguments, "rfc_yaml_extension", ["--file", yamlRfc, "--json"], "promotion_json_path_required"));
39
- results.push(result(runArguments, "policy_yaml_extension", ["--policy", yamlPolicy, "--json"], "promotion_json_path_required"));
40
-
41
- const linkedRfc = path.join(tempRoot, "linked-rfc.json");
42
- const linkedPolicy = path.join(tempRoot, "linked-policy.json");
43
- fs.symlinkSync(path.join(fixtureRoot, "valid-deferred-example.json"), linkedRfc);
44
- fs.symlinkSync(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json"), linkedPolicy);
45
- results.push(result(runArguments, "rfc_symlink", ["--file", linkedRfc, "--json"], "promotion_input_path_invalid"));
46
- results.push(result(runArguments, "policy_symlink", ["--policy", linkedPolicy, "--json"], "promotion_input_path_invalid"));
47
-
48
- for (const [name, mutate] of [
49
- ["inventory_extra_json", (root) => fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), path.join(root, "extra.json"))],
50
- ["inventory_extra_yaml", (root) => fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), path.join(root, "extra.yaml"))],
51
- ["inventory_missing", (root) => fs.rmSync(path.join(root, "invalid-count-only.json"))],
52
- ["inventory_duplicate", (root) => editJson(path.join(root, "manifest.json"), (value) => value.valid_records.push(value.valid_records[0]))],
53
- ]) {
54
- const root = copyFixtureRoot(fixtureRoot, tempRoot, name);
55
- mutate(root);
56
- results.push(result(runArguments, name, ["--promotion-fixture-root", root, "--json"], "promotion_fixture_inventory_invalid"));
57
- }
58
-
59
- const rejectedRoot = copyFixtureRoot(fixtureRoot, tempRoot, "canonical-rejected");
60
- editJson(path.join(rejectedRoot, "valid-deferred-example.json"), (value) => { value.decision = "rejected"; });
61
- results.push(result(runArguments, "canonical_rejected", ["--promotion-fixture-root", rejectedRoot, "--json"], "promotion_canonical_example_invalid"));
62
- const attestedRoot = copyFixtureRoot(fixtureRoot, tempRoot, "canonical-attested");
63
- editJson(path.join(attestedRoot, "valid-deferred-example.json"), (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "test", consumer_id: "fake", count_toward_gate: false, organization_id: "test", relationship: "related" }]; });
64
- results.push(result(runArguments, "canonical_attested", ["--promotion-fixture-root", attestedRoot, "--json"], "promotion_canonical_example_invalid"));
65
-
66
- const unbound = path.join(tempRoot, "unbound.json");
67
- const duplicateEvidence = path.join(tempRoot, "duplicate-evidence.json");
68
- fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), unbound);
69
- editJson(unbound, (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "test", consumer_id: "fake", count_toward_gate: false, organization_id: "test", relationship: "related" }]; });
70
- fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), duplicateEvidence);
71
- editJson(duplicateEvidence, (value) => { value.evidence.push(structuredClone(value.evidence[0])); });
72
- results.push(result(runArguments, "unbound_uncounted", ["--file", unbound, "--json"], "promotion_attestation_profile_required"));
73
- results.push(result(runArguments, "duplicate_evidence_id", ["--file", duplicateEvidence, "--json"], "promotion_evidence_duplicate"));
74
- results.push(result(runArguments, "missing_profile_root", ["--file", path.join(fixtureRoot, "valid-deferred-example.json"), "--profile-repository-root", path.join(tempRoot, "missing-root"), "--json"], "promotion_profile_registry_invalid"));
75
- const linkedProfileRoot = path.join(tempRoot, "linked-profile-root");
76
- fs.symlinkSync(repositoryRoot, linkedProfileRoot);
77
- results.push(result(runArguments, "symlink_profile_root", ["--file", path.join(fixtureRoot, "valid-deferred-example.json"), "--profile-repository-root", linkedProfileRoot, "--json"], "promotion_profile_registry_invalid"));
78
- const linkedManifestRoot = copyFixtureRoot(fixtureRoot, tempRoot, "linked-manifest-root");
79
- fs.rmSync(path.join(linkedManifestRoot, "manifest.json"));
80
- fs.symlinkSync(path.join(fixtureRoot, "manifest.json"), path.join(linkedManifestRoot, "manifest.json"));
81
- results.push(result(runArguments, "symlink_manifest", ["--promotion-fixture-root", linkedManifestRoot, "--json"], "promotion_fixture_inventory_invalid"));
82
-
83
- const strictRoot = path.join(tempRoot, "strict-profile-root");
84
- copyTreeNoSymlinks(path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), path.join(strictRoot, "design-engineering/reference-profiles/governed-local"));
85
- const foundations = path.join(strictRoot, "design-engineering/reference-profiles/governed-local/editorial/local-foundations.json");
86
- fs.writeFileSync(foundations, fs.readFileSync(foundations, "utf8").replace('"identity":', '"identity":"Duplicate",\n "identity":'));
87
- const counted = path.join(tempRoot, "strict-counted.json");
88
- fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), counted);
89
- editJson(counted, (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "editorial", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "test", profile_record: "design-engineering/reference-profiles/governed-local/editorial/profile.json", relationship: "independent" }]; });
90
- results.push(result(runArguments, "duplicate_foundation_identity", ["--file", counted, "--profile-repository-root", strictRoot, "--json"], "promotion_independence_unproven"));
91
- return results;
92
- }
@@ -1,109 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { parseStrictJson } from "./strict-json.mjs";
4
-
5
- const validRecords = Object.freeze(["valid-deferred-example.json", "valid-normative-bypass.json"]);
6
- const invalidRecords = Object.freeze([
7
- ["invalid-blocking-evidence.json", "promotion_blocking_evidence_failed"],
8
- ["invalid-count-only.json", "promotion_count_only_forbidden"],
9
- ["invalid-missing-owner-migration.json", "promotion_owner_migration_required"],
10
- ["invalid-normative-bypass-no-regression.json", "promotion_normative_regression_required"],
11
- ["invalid-related-counted-independent.json", "promotion_related_consumer_counted"],
12
- ["invalid-stable-ended-support.json", "stable_support_ended"],
13
- ]);
14
- const policyRecord = "consumer-reference/policies/shared-experimental.json";
15
- const schemaRecord = "consumer-reference/schema/promotion-rfc.schema.json";
16
-
17
- function finding(code, message, recordPath) {
18
- return { code, message, path: recordPath };
19
- }
20
-
21
- function isInside(base, target) {
22
- const relative = path.relative(base, target);
23
- return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
24
- }
25
-
26
- function resolveContained({ code, expectedType, repositoryRoot, target }, failures) {
27
- const root = path.resolve(repositoryRoot);
28
- const resolved = path.resolve(target);
29
- const recordPath = path.relative(root, resolved) || resolved;
30
- if (!isInside(root, resolved) || !fs.existsSync(resolved)) {
31
- failures.push(finding(code, "promotion input must exist inside the repository", recordPath));
32
- return undefined;
33
- }
34
- if (fs.realpathSync(root) !== root) {
35
- failures.push(finding(code, "repository root must not be a filesystem redirect", recordPath));
36
- return undefined;
37
- }
38
- let current = root;
39
- for (const segment of path.relative(root, resolved).split(path.sep).filter(Boolean)) {
40
- current = path.join(current, segment);
41
- if (fs.lstatSync(current).isSymbolicLink()) {
42
- failures.push(finding(code, "promotion input must not traverse a symlink", recordPath));
43
- return undefined;
44
- }
45
- }
46
- const metadata = fs.lstatSync(resolved);
47
- if ((expectedType === "file" && !metadata.isFile()) || (expectedType === "directory" && !metadata.isDirectory()) || fs.realpathSync(resolved) !== resolved) {
48
- failures.push(finding(code, `promotion input must be a contained regular ${expectedType}`, recordPath));
49
- return undefined;
50
- }
51
- return resolved;
52
- }
53
-
54
- export function resolvePromotionJsonFile({ file, repositoryRoot }, failures) {
55
- const recordPath = path.relative(repositoryRoot, path.resolve(file)) || file;
56
- if (path.extname(file) !== ".json") {
57
- failures.push(finding("promotion_json_path_required", "promotion records must use the .json extension", recordPath));
58
- return undefined;
59
- }
60
- return resolveContained({ code: "promotion_input_path_invalid", expectedType: "file", repositoryRoot, target: file }, failures);
61
- }
62
-
63
- function sameSet(actual, expected) {
64
- return Array.isArray(actual) && actual.length === expected.length && new Set(actual).size === expected.length && expected.every((entry) => actual.includes(entry));
65
- }
66
-
67
- function validManifest(value) {
68
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
69
- const keys = ["id", "invalid_records", "policy_record", "schema_record", "schema_version", "valid_records"];
70
- if (!sameSet(Object.keys(value), keys) || value.id !== "promotion-fixture-inventory" || value.schema_version !== "1.0" || value.policy_record !== policyRecord || value.schema_record !== schemaRecord || !sameSet(value.valid_records, validRecords)) return false;
71
- if (!Array.isArray(value.invalid_records) || value.invalid_records.length !== invalidRecords.length) return false;
72
- const encoded = value.invalid_records.map((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && sameSet(Object.keys(entry), ["expected_code", "file"]) ? `${entry.file}:${entry.expected_code}` : "");
73
- return sameSet(encoded, invalidRecords.map(([file, code]) => `${file}:${code}`));
74
- }
75
-
76
- export function loadPromotionFixtureInventory({ fixtureRoot, repositoryRoot }) {
77
- const failures = [];
78
- const root = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "directory", repositoryRoot, target: fixtureRoot }, failures);
79
- if (!root) return { failures, invalidRecords: [], policyFile: undefined, schema: undefined, validFiles: [] };
80
- const manifestFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(root, "manifest.json") }, failures);
81
- let manifest;
82
- try {
83
- if (manifestFile) manifest = parseStrictJson(fs.readFileSync(manifestFile, "utf8"));
84
- } catch (error) {
85
- failures.push(finding("promotion_fixture_inventory_invalid", error instanceof Error ? error.message : String(error), path.relative(repositoryRoot, manifestFile)));
86
- }
87
- if (!validManifest(manifest)) failures.push(finding("promotion_fixture_inventory_invalid", "promotion manifest must match the canonical closed inventory", path.relative(repositoryRoot, manifestFile ?? root)));
88
- const expectedFiles = ["manifest.json", ...validRecords, ...invalidRecords.map(([file]) => file)];
89
- const entries = fs.readdirSync(root, { withFileTypes: true });
90
- if (!sameSet(entries.map((entry) => entry.name), expectedFiles) || entries.some((entry) => !entry.isFile() || path.extname(entry.name) !== ".json")) {
91
- failures.push(finding("promotion_fixture_inventory_invalid", "promotion fixture directory must contain only the declared JSON closed set", path.relative(repositoryRoot, root)));
92
- }
93
- for (const file of expectedFiles) resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(root, file) }, failures);
94
- const policyFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(repositoryRoot, policyRecord) }, failures);
95
- const schemaFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(repositoryRoot, schemaRecord) }, failures);
96
- let schema;
97
- try {
98
- if (schemaFile) schema = parseStrictJson(fs.readFileSync(schemaFile, "utf8"));
99
- } catch (error) {
100
- failures.push(finding("promotion_fixture_inventory_invalid", error instanceof Error ? error.message : String(error), schemaRecord));
101
- }
102
- return {
103
- failures: [...new Map(failures.map((issue) => [`${issue.code}:${issue.path}:${issue.message}`, issue])).values()],
104
- invalidRecords: invalidRecords.map(([file, expectedCode]) => ({ expectedCode, file: path.join(root, file) })),
105
- policyFile,
106
- schema,
107
- validFiles: validRecords.map((file) => path.join(root, file)),
108
- };
109
- }
@@ -1,28 +0,0 @@
1
- const requirements = [
2
- ["GOVERNANCE.md", "The StyleGallery-local `>=2` independent-consumer gateway applies only to consumer-local → shared-experimental invariant eligibility."],
3
- ["GOVERNANCE.md", "Shared stable has no numeric adoption threshold"],
4
- ["GOVERNANCE.md", "may waive adoption count only"],
5
- ["GOVERNANCE.md", "zero durable adopter attestations and record no promotion"],
6
- ["GOVERNANCE.md", "never be silently relabeled shared-experimental"],
7
- ["DOMAINS.md", "### Consumer Reference Promotion"],
8
- ["DOMAINS.md", "Promotion records are JSON-only"],
9
- ["consumer-reference/contract.md", "They are not accepted or promoted RFCs."],
10
- ["consumer-reference/contract.md", "boolean aliases are invalid"],
11
- ["consumer-reference/index.md", "[Promotion RFC schema](schema/promotion-rfc.schema.json)"],
12
- ["consumer-reference/index.md", "[Shared-experimental promotion policy](policies/shared-experimental.json)"],
13
- ["consumer-reference/fixtures/promotion/manifest.json", '"id": "promotion-fixture-inventory"'],
14
- ["quality/evidence/executable-evidence.md", "Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract."],
15
- ["quality/evidence/executable-evidence.md", "Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision"],
16
- [".github/workflows/validate.yml", "node -c scripts/promotion-fixture-inventory.mjs"],
17
- [".github/workflows/validate.yml", "node -c scripts/promotion-boundary-test-contract.mjs"],
18
- ["consumer-reference/policies/shared-experimental.json", '"review_independence": "single_account"'],
19
- ["consumer-reference/policies/shared-experimental.json", '"promotion_occurred": false'],
20
- ["consumer-reference/policies/shared-experimental.json", '"adopter_attestations": 0'],
21
- ["consumer-reference/policies/shared-experimental.json", '"stable_numeric_threshold": null'],
22
- ];
23
-
24
- export function promotionGovernanceFailures(read) {
25
- return requirements
26
- .filter(([relative, text]) => !read(relative).includes(text))
27
- .map(([relative, text]) => `${relative}: missing ${text}`);
28
- }
@@ -1,28 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- function isContained(root, target) {
5
- const relative = path.relative(root, target);
6
- return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
7
- }
8
-
9
- export function inspectArtifactPath(root, candidate, allowMissing) {
10
- const trustedRoot = fs.realpathSync(root);
11
- const target = path.resolve(root, candidate);
12
- if (!isContained(trustedRoot, target)) return { ok: false, path: target, reason: "outside_trust_root" };
13
-
14
- const segments = path.relative(trustedRoot, target).split(path.sep);
15
- let current = trustedRoot;
16
- for (const [index, segment] of segments.entries()) {
17
- current = path.join(current, segment);
18
- if (!fs.existsSync(current)) {
19
- return allowMissing ? { ok: true, path: target } : { ok: false, path: target, reason: "missing" };
20
- }
21
- const metadata = fs.lstatSync(current);
22
- if (metadata.isSymbolicLink()) return { ok: false, path: target, reason: "symbolic_link" };
23
- const finalSegment = index === segments.length - 1;
24
- if (finalSegment && !metadata.isFile()) return { ok: false, path: target, reason: "not_regular_file" };
25
- if (!finalSegment && !metadata.isDirectory()) return { ok: false, path: target, reason: "parent_not_directory" };
26
- }
27
- return { ok: true, path: target };
28
- }
@@ -1,114 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { isPlainObject } from "./consumer-reference-schema.mjs";
5
- import { validatePortableTokens } from "./reference-token-contract.mjs";
6
- import { parseStrictJson } from "./strict-json.mjs";
7
-
8
- const layoutSourceSha = "775430bbaf4ee208a642220f440f6926d79c90a3";
9
- const requiredResetFields = ["body_margin", "box_sizing", "figure_margin"];
10
-
11
- function hash(content) {
12
- return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
13
- }
14
-
15
- function finding(code, relative, message) {
16
- return { code, message, path: relative };
17
- }
18
-
19
- function isInside(base, target) {
20
- const relative = path.relative(base, target);
21
- return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
22
- }
23
-
24
- function readJson({ failures, kind, relative, root }) {
25
- const absolute = path.resolve(root, relative);
26
- if (!fs.existsSync(absolute)) {
27
- failures.push(finding(`${kind}_unresolved`, relative, "profile artifact must exist beside profile.json"));
28
- return null;
29
- }
30
- const realRoot = fs.realpathSync(root);
31
- const realTarget = fs.realpathSync(absolute);
32
- if (!isInside(realRoot, realTarget)) {
33
- failures.push(finding(`${kind}_symlink_escape`, relative, "profile artifact resolves outside the repository root"));
34
- return null;
35
- }
36
- if (realTarget !== path.join(realRoot, relative)) {
37
- failures.push(finding(`${kind}_redirect`, relative, "profile artifact must remain at its exact sibling path"));
38
- return null;
39
- }
40
- if (!fs.statSync(realTarget).isFile()) {
41
- failures.push(finding(`${kind}_type_invalid`, relative, "profile artifact must be a regular JSON file"));
42
- return null;
43
- }
44
- const bytes = fs.readFileSync(realTarget);
45
- try {
46
- return { bytes, value: parseStrictJson(bytes.toString("utf8")) };
47
- } catch (error) {
48
- failures.push(finding(`${kind}_json_invalid`, relative, error instanceof Error ? error.message : String(error)));
49
- return null;
50
- }
51
- }
52
-
53
- export function validateReferenceProfile({ item, relative, root }) {
54
- const failures = [];
55
- const add = (code, message) => failures.push(finding(code, relative, message));
56
- if (item.profile_kind !== "governed_local") add("profile_kind_governed_local", "profile_kind must be governed_local");
57
- if (item.artifact_mode !== "governed_local") add("profile_artifact_mode_governed_local", "governed-local profiles cannot be external consumers");
58
- if (item.maturity !== "experimental") add("profile_maturity_experimental", "governed-local profiles must remain experimental");
59
- if (item.example_only !== true) add("profile_example_only_required", "governed-local profiles must be example_only");
60
- if (item.default !== false) add("profile_default_forbidden", "governed-local profiles cannot be implicit or default");
61
- if (item.layout_source_sha !== layoutSourceSha) add("profile_layout_source_sha_required", `layout_source_sha must pin ${layoutSourceSha}`);
62
- if (typeof item.related_fixture_set_id !== "string" || item.related_fixture_set_id.length === 0) add("profile_related_fixture_set_required", "related_fixture_set_id is required");
63
- if (!isPlainObject(item.selection) || item.selection.required !== true || item.selection.method !== "profile_path") add("profile_explicit_selection_required", "selection must require an explicit profile_path");
64
- if (!isPlainObject(item.environment_assumptions) || typeof item.environment_assumptions.user_agent_styles !== "string" || item.environment_assumptions.user_agent_styles.length === 0) {
65
- add("profile_ua_assumption_required", "user-agent style assumptions must be explicit");
66
- }
67
- const reset = isPlainObject(item.environment_assumptions) ? item.environment_assumptions.reset : null;
68
- if (!isPlainObject(reset) || requiredResetFields.some((field) => typeof reset[field] !== "string" || reset[field].length === 0)) {
69
- add("profile_reset_assumptions_required", "body, box-sizing, and figure reset assumptions must be explicit");
70
- }
71
- if (item.tokens !== "tokens.dtcg.json") add("profile_tokens_path_required", "tokens must resolve to the sibling tokens.dtcg.json");
72
- if (item.local_foundations !== "local-foundations.json") add("profile_foundations_path_required", "foundations must resolve to the sibling local-foundations.json");
73
-
74
- const directory = path.posix.dirname(relative);
75
- const tokenRelative = path.posix.join(directory, "tokens.dtcg.json");
76
- const foundationRelative = path.posix.join(directory, "local-foundations.json");
77
- const tokenDocument = readJson({ failures, kind: "profile_tokens", relative: tokenRelative, root });
78
- const foundations = readJson({ failures, kind: "profile_foundations", relative: foundationRelative, root });
79
- if (tokenDocument) {
80
- for (const tokenFailure of validatePortableTokens(tokenDocument.value).failures) {
81
- failures.push(finding(tokenFailure.code, tokenRelative, tokenFailure.message));
82
- }
83
- }
84
- if (foundations) {
85
- if (!isPlainObject(foundations.value)
86
- || foundations.value.profile_id !== item.id
87
- || typeof foundations.value.identity !== "string"
88
- || !isPlainObject(foundations.value.bindings)) {
89
- failures.push(finding("profile_foundations_invalid", foundationRelative, "foundations require matching profile_id, identity, and bindings"));
90
- }
91
- }
92
-
93
- const summary = tokenDocument && foundations ? {
94
- foundationHash: hash(foundations.bytes),
95
- id: item.id,
96
- identity: foundations.value.identity,
97
- layoutSourceSha: item.layout_source_sha,
98
- relatedFixtureSetId: item.related_fixture_set_id,
99
- tokenHash: hash(tokenDocument.bytes),
100
- } : null;
101
- return { failures, summary };
102
- }
103
-
104
- export function validateReferenceProfileSet(summaries) {
105
- if (summaries.length < 2) return [];
106
- const failures = [];
107
- const unique = (field) => new Set(summaries.map((summary) => summary[field])).size;
108
- if (unique("layoutSourceSha") !== 1) failures.push(finding("profile_layout_source_mismatch", "<profiles>", "profiles must share one Layout source SHA"));
109
- if (unique("relatedFixtureSetId") !== 1) failures.push(finding("profile_related_set_mismatch", "<profiles>", "profiles must share one related fixture set"));
110
- if (unique("identity") !== summaries.length) failures.push(finding("profile_identity_not_distinct", "<profiles>", "adversarial profiles must declare distinct identities"));
111
- if (unique("tokenHash") !== summaries.length) failures.push(finding("profile_tokens_not_distinct", "<profiles>", "adversarial profiles must have distinct token bytes"));
112
- if (unique("foundationHash") !== summaries.length) failures.push(finding("profile_foundations_not_distinct", "<profiles>", "adversarial profiles must have distinct foundation bytes"));
113
- return failures;
114
- }