@usefragments/core 1.0.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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,245 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ REGISTRY_ARTIFACT_SCHEMA_VERSION,
5
+ REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION,
6
+ REGISTRY_MANIFEST_SCHEMA_VERSION,
7
+ buildRegistryArtifact,
8
+ buildRegistryFile,
9
+ computeRegistryHash,
10
+ finalizeRegistryManifest,
11
+ registryArtifactDigest,
12
+ registryArtifactSchema,
13
+ registryInstallReceiptSchema,
14
+ registryManifestSchema,
15
+ type RegistryManifestDraft,
16
+ } from "./registry.js";
17
+
18
+ function manifestDraft(): RegistryManifestDraft {
19
+ const buttonSource = buildRegistryFile({
20
+ path: "components/Button/index.tsx",
21
+ role: "source",
22
+ content: "export function Button() { return <button />; }\n",
23
+ contentType: "text/tsx",
24
+ });
25
+ const buttonStyle = buildRegistryFile({
26
+ path: "components/Button/Button.module.scss",
27
+ role: "style",
28
+ content: ".button { color: var(--brand-accent); }\n",
29
+ contentType: "text/scss",
30
+ });
31
+
32
+ return {
33
+ schemaVersion: REGISTRY_MANIFEST_SCHEMA_VERSION,
34
+ registryId: "acme-ui",
35
+ name: "Acme UI",
36
+ channel: "stable",
37
+ version: "2026.06.14",
38
+ fcid: "a".repeat(64),
39
+ generatedAt: "2026-06-14T12:00:00.000Z",
40
+ source: {
41
+ kind: "github",
42
+ repoFullName: "acme/ui",
43
+ commitSha: "abc123",
44
+ path: "packages/ui",
45
+ },
46
+ components: {
47
+ button: {
48
+ componentId: "button",
49
+ publicRef: "acme-ui:button",
50
+ name: "Button",
51
+ status: "stable",
52
+ tier: "core",
53
+ category: "actions",
54
+ files: [buttonSource.metadata, buttonStyle.metadata],
55
+ styleFiles: [buttonStyle.metadata.path],
56
+ tokenDependencies: ["--brand-accent"],
57
+ runtimeDependencies: [
58
+ {
59
+ name: "@base-ui/react",
60
+ versionRange: "^1.0.0",
61
+ type: "runtime",
62
+ },
63
+ ],
64
+ peerDependencies: [
65
+ {
66
+ name: "react",
67
+ versionRange: ">=18",
68
+ type: "peer",
69
+ },
70
+ ],
71
+ internalDependencies: [],
72
+ exports: [{ name: "Button", path: buttonSource.metadata.path }],
73
+ contract: { a11y: ["requires-name"] },
74
+ examples: [{ name: "Default", code: "<Button />" }],
75
+ provenance: { scanner: "fixture" },
76
+ },
77
+ },
78
+ dependencies: [],
79
+ installProfiles: [
80
+ {
81
+ name: "react-app",
82
+ targetPath: "src/fragments/ui",
83
+ importPath: "@/fragments/ui",
84
+ stylePath: "src/fragments/ui/styles.scss",
85
+ includeTests: false,
86
+ },
87
+ ],
88
+ };
89
+ }
90
+
91
+ describe("registry schemas", () => {
92
+ it("finalizes and validates a registry manifest", () => {
93
+ const manifest = finalizeRegistryManifest(manifestDraft());
94
+
95
+ expect(registryManifestSchema.parse(manifest).registryHash).toMatch(/^[0-9a-f]{64}$/);
96
+ expect(manifest.components.button.name).toBe("Button");
97
+ });
98
+
99
+ it("keeps the registry hash stable across generatedAt changes", () => {
100
+ const first = manifestDraft();
101
+ const second = {
102
+ ...first,
103
+ generatedAt: "2026-06-14T13:00:00.000Z",
104
+ };
105
+
106
+ expect(computeRegistryHash(first)).toBe(computeRegistryHash(second));
107
+ });
108
+
109
+ it("changes the registry hash when file content changes", () => {
110
+ const first = manifestDraft();
111
+ const changedFile = buildRegistryFile({
112
+ path: "components/Button/index.tsx",
113
+ role: "source",
114
+ content: "export function Button() { return <button data-x />; }\n",
115
+ contentType: "text/tsx",
116
+ });
117
+ const second = {
118
+ ...first,
119
+ components: {
120
+ button: {
121
+ ...first.components.button,
122
+ files: [changedFile.metadata, first.components.button.files[1]],
123
+ },
124
+ },
125
+ };
126
+
127
+ expect(computeRegistryHash(first)).not.toBe(computeRegistryHash(second));
128
+ });
129
+
130
+ it("rejects unsafe registry file paths", () => {
131
+ expect(() =>
132
+ buildRegistryFile({
133
+ path: "../Button.tsx",
134
+ role: "source",
135
+ content: "",
136
+ contentType: "text/tsx",
137
+ })
138
+ ).toThrow(/must not contain \.\./i);
139
+
140
+ expect(() =>
141
+ buildRegistryFile({
142
+ path: "/tmp/Button.tsx",
143
+ role: "source",
144
+ content: "",
145
+ contentType: "text/tsx",
146
+ })
147
+ ).toThrow(/not absolute/i);
148
+ });
149
+
150
+ it("builds local registry artifacts and detects missing content", () => {
151
+ const source = buildRegistryFile({
152
+ path: "components/Button/index.tsx",
153
+ role: "source",
154
+ content: "export const Button = () => null;\n",
155
+ contentType: "text/tsx",
156
+ });
157
+ const draft = manifestDraft();
158
+ const artifact = buildRegistryArtifact({
159
+ manifest: {
160
+ ...draft,
161
+ components: {
162
+ button: {
163
+ ...draft.components.button,
164
+ files: [source.metadata],
165
+ },
166
+ },
167
+ },
168
+ files: [source.content],
169
+ });
170
+
171
+ expect(registryArtifactDigest(artifact)).toMatch(/^[0-9a-f]{64}$/);
172
+ expect(() =>
173
+ buildRegistryArtifact({
174
+ manifest: artifact.manifest,
175
+ files: [source.content, source.content],
176
+ })
177
+ ).toThrow(/Duplicate registry file content/);
178
+
179
+ const manifest = finalizeRegistryManifest({
180
+ ...draft,
181
+ components: {
182
+ button: {
183
+ ...draft.components.button,
184
+ files: [source.metadata],
185
+ },
186
+ },
187
+ });
188
+
189
+ expect(() =>
190
+ registryArtifactSchema.parse({
191
+ schemaVersion: REGISTRY_ARTIFACT_SCHEMA_VERSION,
192
+ manifest,
193
+ files: {},
194
+ })
195
+ ).toThrow(/Missing content/);
196
+
197
+ expect(() =>
198
+ registryArtifactSchema.parse({
199
+ schemaVersion: REGISTRY_ARTIFACT_SCHEMA_VERSION,
200
+ manifest,
201
+ files: {
202
+ [source.metadata.path]: {
203
+ ...source.content,
204
+ path: "components/Button/other.tsx",
205
+ },
206
+ },
207
+ })
208
+ ).toThrow(/does not match content path/);
209
+ });
210
+
211
+ it("validates install receipts for committed local proof", () => {
212
+ const manifest = finalizeRegistryManifest(manifestDraft());
213
+
214
+ expect(() =>
215
+ registryInstallReceiptSchema.parse({
216
+ schemaVersion: REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION,
217
+ registryId: manifest.registryId,
218
+ registryVersion: manifest.version,
219
+ registryHash: manifest.registryHash,
220
+ fcid: manifest.fcid,
221
+ channel: manifest.channel,
222
+ targetPath: "src/fragments/ui",
223
+ importPath: "@/fragments/ui",
224
+ installedComponents: [
225
+ {
226
+ componentId: "button",
227
+ publicRef: "acme-ui:button",
228
+ name: "Button",
229
+ files: ["src/fragments/ui/components/Button/index.tsx"],
230
+ },
231
+ ],
232
+ files: [
233
+ {
234
+ path: "src/fragments/ui/components/Button/index.tsx",
235
+ sourcePath: "components/Button/index.tsx",
236
+ sha256: manifest.components.button.files[0].sha256,
237
+ componentId: "button",
238
+ },
239
+ ],
240
+ dependencies: manifest.components.button.peerDependencies,
241
+ installedAt: "2026-06-14T12:30:00.000Z",
242
+ })
243
+ ).not.toThrow();
244
+ });
245
+ });
@@ -0,0 +1,369 @@
1
+ import { z } from "zod";
2
+
3
+ import { portableRepoPathError } from "./config-paths.js";
4
+ import { canonicalPreimage, contractHash, sha256Hex } from "./contract/hash.js";
5
+
6
+ export const REGISTRY_MANIFEST_SCHEMA_VERSION = "fragments.registry.v1";
7
+ export const REGISTRY_ARTIFACT_SCHEMA_VERSION = "fragments.registry-artifact.v1";
8
+ export const REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION = "fragments.registry-lock.v1";
9
+
10
+ const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/);
11
+
12
+ const registryPathSchema = z
13
+ .string()
14
+ .min(1)
15
+ .superRefine((value, ctx) => {
16
+ const error = portableRepoPathError(value);
17
+ if (error) {
18
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: error });
19
+ }
20
+ });
21
+
22
+ const registryFilePathSchema = registryPathSchema.superRefine((value, ctx) => {
23
+ if (value === ".") {
24
+ ctx.addIssue({
25
+ code: z.ZodIssueCode.custom,
26
+ message: "File path must point to a file, not the package root",
27
+ });
28
+ }
29
+ });
30
+
31
+ export const registryDependencyTypeSchema = z.enum(["runtime", "peer", "dev", "optional"]);
32
+
33
+ export const registryDependencySchema = z.object({
34
+ name: z.string().min(1),
35
+ versionRange: z.string().min(1),
36
+ type: registryDependencyTypeSchema,
37
+ optional: z.boolean().optional(),
38
+ });
39
+
40
+ export const registryFileRoleSchema = z.enum([
41
+ "source",
42
+ "style",
43
+ "token",
44
+ "contract",
45
+ "test",
46
+ "story",
47
+ "example",
48
+ "asset",
49
+ "barrel",
50
+ "metadata",
51
+ ]);
52
+
53
+ export const registryFileSchema = z.object({
54
+ path: registryFilePathSchema,
55
+ role: registryFileRoleSchema,
56
+ sha256: sha256Schema,
57
+ size: z.number().int().nonnegative(),
58
+ contentType: z.string().min(1),
59
+ contentRef: z.string().min(1),
60
+ });
61
+
62
+ export const registryFileContentSchema = z.object({
63
+ path: registryFilePathSchema,
64
+ content: z.string(),
65
+ sha256: sha256Schema,
66
+ size: z.number().int().nonnegative(),
67
+ contentType: z.string().min(1),
68
+ });
69
+
70
+ export const registrySourceSchema = z.discriminatedUnion("kind", [
71
+ z.object({
72
+ kind: z.literal("package"),
73
+ packageName: z.string().min(1),
74
+ version: z.string().optional(),
75
+ rootPath: registryPathSchema.optional(),
76
+ commitSha: z.string().optional(),
77
+ }),
78
+ z.object({
79
+ kind: z.literal("github"),
80
+ repoFullName: z.string().min(1),
81
+ commitSha: z.string().min(1),
82
+ path: registryPathSchema.optional(),
83
+ }),
84
+ z.object({
85
+ kind: z.literal("local"),
86
+ rootPath: registryPathSchema.optional(),
87
+ commitSha: z.string().optional(),
88
+ }),
89
+ z.object({
90
+ kind: z.literal("manual"),
91
+ label: z.string().min(1),
92
+ }),
93
+ ]);
94
+
95
+ export const registryComponentExportSchema = z.object({
96
+ name: z.string().min(1),
97
+ kind: z.enum(["component", "hook", "utility", "type", "style"]).default("component"),
98
+ path: registryFilePathSchema.optional(),
99
+ });
100
+
101
+ export const registryComponentSchema = z.object({
102
+ componentId: z.string().min(1),
103
+ publicRef: z.string().min(1),
104
+ name: z.string().min(1),
105
+ status: z.string().min(1).default("stable"),
106
+ tier: z.enum(["core", "composition", "utility", "custom"]).default("core"),
107
+ category: z.string().optional(),
108
+ files: z.array(registryFileSchema).min(1),
109
+ styleFiles: z.array(registryFilePathSchema).default([]),
110
+ tokenDependencies: z.array(z.string().min(1)).default([]),
111
+ runtimeDependencies: z.array(registryDependencySchema).default([]),
112
+ peerDependencies: z.array(registryDependencySchema).default([]),
113
+ internalDependencies: z.array(z.string().min(1)).default([]),
114
+ exports: z.array(registryComponentExportSchema).default([]),
115
+ contract: z.unknown().optional(),
116
+ examples: z.array(z.unknown()).default([]),
117
+ provenance: z.record(z.unknown()).default({}),
118
+ });
119
+
120
+ export const registryEntrypointSchema = z.object({
121
+ specifier: z.string().min(1),
122
+ sourcePath: registryFilePathSchema,
123
+ });
124
+
125
+ export const registryInstallProfileSchema = z.object({
126
+ name: z.string().min(1),
127
+ targetPath: registryPathSchema,
128
+ importPath: z.string().min(1),
129
+ stylePath: registryPathSchema.optional(),
130
+ includeTests: z.boolean().default(false),
131
+ });
132
+
133
+ const registryManifestBaseSchema = z.object({
134
+ schemaVersion: z.literal(REGISTRY_MANIFEST_SCHEMA_VERSION),
135
+ registryId: z.string().min(1),
136
+ name: z.string().min(1),
137
+ channel: z.string().min(1),
138
+ version: z.string().min(1),
139
+ contractVersionId: z.string().optional(),
140
+ fcid: sha256Schema.optional(),
141
+ generatedAt: z.string().datetime(),
142
+ source: registrySourceSchema,
143
+ packageFiles: z.array(registryFileSchema).default([]),
144
+ entrypoints: z.array(registryEntrypointSchema).default([]),
145
+ components: z.record(z.string(), registryComponentSchema),
146
+ dependencies: z.array(registryDependencySchema).default([]),
147
+ installProfiles: z.array(registryInstallProfileSchema).default([]),
148
+ });
149
+
150
+ export const registryManifestDraftSchema = registryManifestBaseSchema;
151
+
152
+ export const registryManifestSchema = registryManifestBaseSchema.extend({
153
+ registryHash: sha256Schema,
154
+ });
155
+
156
+ export const registryArtifactSchema = z
157
+ .object({
158
+ schemaVersion: z.literal(REGISTRY_ARTIFACT_SCHEMA_VERSION),
159
+ manifest: registryManifestSchema,
160
+ files: z.record(registryFilePathSchema, registryFileContentSchema),
161
+ })
162
+ .superRefine((artifact, ctx) => {
163
+ const filesByPath = artifact.files;
164
+ for (const [path, content] of Object.entries(filesByPath)) {
165
+ if (content.path !== path) {
166
+ ctx.addIssue({
167
+ code: z.ZodIssueCode.custom,
168
+ path: ["files", path, "path"],
169
+ message: `Registry file map key ${path} does not match content path ${content.path}`,
170
+ });
171
+ }
172
+ }
173
+
174
+ const manifestFiles = [
175
+ ...artifact.manifest.packageFiles,
176
+ ...artifact.manifest.entrypoints.map((entrypoint) => ({
177
+ path: entrypoint.sourcePath,
178
+ role: "barrel" as const,
179
+ sha256: filesByPath[entrypoint.sourcePath]?.sha256 ?? "",
180
+ size: filesByPath[entrypoint.sourcePath]?.size ?? 0,
181
+ contentType: filesByPath[entrypoint.sourcePath]?.contentType ?? "text/plain",
182
+ contentRef: `sha256:${filesByPath[entrypoint.sourcePath]?.sha256 ?? ""}`,
183
+ })),
184
+ ...Object.values(artifact.manifest.components).flatMap((component) => component.files),
185
+ ];
186
+
187
+ for (const file of manifestFiles) {
188
+ const content = filesByPath[file.path];
189
+ if (!content) {
190
+ ctx.addIssue({
191
+ code: z.ZodIssueCode.custom,
192
+ path: ["files", file.path],
193
+ message: `Missing content for registry file ${file.path}`,
194
+ });
195
+ continue;
196
+ }
197
+ if (content.sha256 !== file.sha256) {
198
+ ctx.addIssue({
199
+ code: z.ZodIssueCode.custom,
200
+ path: ["files", file.path, "sha256"],
201
+ message: `Content hash for ${file.path} does not match manifest`,
202
+ });
203
+ }
204
+ if (content.size !== file.size) {
205
+ ctx.addIssue({
206
+ code: z.ZodIssueCode.custom,
207
+ path: ["files", file.path, "size"],
208
+ message: `Content size for ${file.path} does not match manifest`,
209
+ });
210
+ }
211
+ }
212
+ });
213
+
214
+ export const registryInstallReceiptFileSchema = z.object({
215
+ path: registryFilePathSchema,
216
+ sha256: sha256Schema,
217
+ sourcePath: registryFilePathSchema,
218
+ componentId: z.string().min(1).optional(),
219
+ });
220
+
221
+ export const registryInstallReceiptComponentSchema = z.object({
222
+ componentId: z.string().min(1),
223
+ publicRef: z.string().min(1),
224
+ name: z.string().min(1),
225
+ files: z.array(registryFilePathSchema),
226
+ });
227
+
228
+ export const registryInstallReceiptSchema = z.object({
229
+ schemaVersion: z.literal(REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION),
230
+ registryId: z.string().min(1),
231
+ registryVersion: z.string().min(1),
232
+ registryHash: sha256Schema,
233
+ fcid: sha256Schema.optional(),
234
+ channel: z.string().min(1).optional(),
235
+ targetPath: registryPathSchema,
236
+ importPath: z.string().min(1),
237
+ installedComponents: z.array(registryInstallReceiptComponentSchema),
238
+ files: z.array(registryInstallReceiptFileSchema),
239
+ dependencies: z.array(registryDependencySchema).default([]),
240
+ installedAt: z.string().datetime(),
241
+ });
242
+
243
+ export type RegistryDependencyType = z.infer<typeof registryDependencyTypeSchema>;
244
+ export type RegistryDependency = z.infer<typeof registryDependencySchema>;
245
+ export type RegistryFileRole = z.infer<typeof registryFileRoleSchema>;
246
+ export type RegistryFile = z.infer<typeof registryFileSchema>;
247
+ export type RegistryFileContent = z.infer<typeof registryFileContentSchema>;
248
+ export type RegistrySource = z.infer<typeof registrySourceSchema>;
249
+ export type RegistryComponentExport = z.infer<typeof registryComponentExportSchema>;
250
+ export type RegistryComponent = z.infer<typeof registryComponentSchema>;
251
+ export type RegistryEntrypoint = z.infer<typeof registryEntrypointSchema>;
252
+ export type RegistryInstallProfile = z.infer<typeof registryInstallProfileSchema>;
253
+ export type RegistryManifestDraft = z.input<typeof registryManifestDraftSchema>;
254
+ export type RegistryManifest = z.infer<typeof registryManifestSchema>;
255
+ export type RegistryArtifact = z.infer<typeof registryArtifactSchema>;
256
+ export type RegistryInstallReceiptFile = z.infer<typeof registryInstallReceiptFileSchema>;
257
+ export type RegistryInstallReceiptComponent = z.infer<typeof registryInstallReceiptComponentSchema>;
258
+ export type RegistryInstallReceipt = z.infer<typeof registryInstallReceiptSchema>;
259
+
260
+ export interface RegistryFileInput {
261
+ path: string;
262
+ role: RegistryFileRole;
263
+ content: string;
264
+ contentType: string;
265
+ contentRef?: string;
266
+ }
267
+
268
+ export interface RegistryArtifactInput {
269
+ manifest: RegistryManifestDraft;
270
+ files: readonly RegistryFileContent[];
271
+ }
272
+
273
+ function registryHashPayload(manifest: RegistryManifestDraft | RegistryManifest): unknown {
274
+ const { generatedAt: _generatedAt, ...withoutGeneratedAt } = manifest;
275
+ if ("registryHash" in withoutGeneratedAt) {
276
+ const { registryHash: _registryHash, ...withoutHash } = withoutGeneratedAt;
277
+ return withoutHash;
278
+ }
279
+ return withoutGeneratedAt;
280
+ }
281
+
282
+ export function computeRegistryHash(manifest: RegistryManifestDraft | RegistryManifest): string {
283
+ return contractHash(registryHashPayload(manifest));
284
+ }
285
+
286
+ export function finalizeRegistryManifest(input: RegistryManifestDraft): RegistryManifest {
287
+ const draft = registryManifestDraftSchema.parse(input);
288
+ return registryManifestSchema.parse({
289
+ ...draft,
290
+ registryHash: computeRegistryHash(draft),
291
+ });
292
+ }
293
+
294
+ export function hashRegistryFileContent(content: string): string {
295
+ return sha256Hex(content);
296
+ }
297
+
298
+ export function registryFileSize(content: string): number {
299
+ return new TextEncoder().encode(content).length;
300
+ }
301
+
302
+ export function buildRegistryFile(input: RegistryFileInput): {
303
+ metadata: RegistryFile;
304
+ content: RegistryFileContent;
305
+ } {
306
+ const sha256 = hashRegistryFileContent(input.content);
307
+ const size = registryFileSize(input.content);
308
+ const path = registryFilePathSchema.parse(input.path);
309
+ const contentType = z.string().min(1).parse(input.contentType);
310
+ return {
311
+ metadata: registryFileSchema.parse({
312
+ path,
313
+ role: input.role,
314
+ sha256,
315
+ size,
316
+ contentType,
317
+ contentRef: input.contentRef ?? `sha256:${sha256}`,
318
+ }),
319
+ content: registryFileContentSchema.parse({
320
+ path,
321
+ content: input.content,
322
+ sha256,
323
+ size,
324
+ contentType,
325
+ }),
326
+ };
327
+ }
328
+
329
+ export function buildRegistryArtifact(input: RegistryArtifactInput): RegistryArtifact {
330
+ const manifest = finalizeRegistryManifest(input.manifest);
331
+ const files = new Map<string, RegistryFileContent>();
332
+ for (const file of input.files) {
333
+ const content = registryFileContentSchema.parse(file);
334
+ if (files.has(content.path)) {
335
+ throw new Error(`Duplicate registry file content for ${content.path}`);
336
+ }
337
+ files.set(content.path, content);
338
+ }
339
+
340
+ return registryArtifactSchema.parse({
341
+ schemaVersion: REGISTRY_ARTIFACT_SCHEMA_VERSION,
342
+ manifest,
343
+ files: Object.fromEntries(files),
344
+ });
345
+ }
346
+
347
+ export function assertValidRegistryArtifact(artifact: unknown): RegistryArtifact {
348
+ return registryArtifactSchema.parse(artifact);
349
+ }
350
+
351
+ export function registryArtifactDigest(artifact: RegistryArtifact): string {
352
+ const files = Object.fromEntries(
353
+ Object.entries(artifact.files)
354
+ .map(([path, file]) => [path, { sha256: file.sha256, size: file.size }] as const)
355
+ .sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))
356
+ );
357
+
358
+ return contractHash({
359
+ schemaVersion: artifact.schemaVersion,
360
+ manifest: registryHashPayload(artifact.manifest),
361
+ files,
362
+ });
363
+ }
364
+
365
+ export function registryManifestCanonicalPreimage(
366
+ manifest: RegistryManifestDraft | RegistryManifest
367
+ ): string {
368
+ return canonicalPreimage(registryHashPayload(manifest));
369
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `a11y/required-accessible-name` — fires for components with an
3
+ * `a11y.requireName` policy whose JSX usage has no discoverable accessible
4
+ * name.
5
+ *
6
+ * Accepted name sources (any one is enough):
7
+ * - non-empty `usage_text_child` content
8
+ * - `aria-label="..."` (static or dynamic both pass; we cannot prove a
9
+ * dynamic value is empty at scan time)
10
+ * - `aria-labelledby={...}`
11
+ * - `title="..."`
12
+ *
13
+ * Components that explicitly opt out via `aria-hidden="true"` are skipped —
14
+ * they are not part of the accessibility tree.
15
+ */
16
+
17
+ import type { FactIndex, UsagePropResolvedFact } from "../facts/index.js";
18
+
19
+ import { makeFinding } from "./finding.js";
20
+ import type { Finding } from "./types.js";
21
+ import { indexPropsByNodeId, indexTextChildrenByNodeId, readUsageNode } from "./utils.js";
22
+
23
+ export const RULE_ID = "a11y/required-accessible-name";
24
+ export const RULE_VERSION = "1";
25
+
26
+ const NAME_SOURCE_PROPS = new Set(["aria-label", "aria-labelledby", "title"]);
27
+
28
+ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
29
+ const findings: Finding[] = [];
30
+ const propsByNode = indexPropsByNodeId(ix);
31
+ const textByNode = indexTextChildrenByNodeId(ix);
32
+
33
+ for (const usageComponent of ix.byKind("usage_component")) {
34
+ const policy = ix.policy.a11yNameRequired(usageComponent.componentId);
35
+ if (!policy) continue;
36
+
37
+ const node = readUsageNode(ix, usageComponent.nodeId);
38
+ if (!node) continue;
39
+
40
+ const props = propsByNode.get(node.id) ?? [];
41
+ if (props.some(isAriaHidden)) continue;
42
+
43
+ if (hasAccessibleName(props, textByNode.get(node.id) ?? [])) continue;
44
+
45
+ findings.push(
46
+ makeFinding({
47
+ ruleId: RULE_ID,
48
+ ruleVersion: RULE_VERSION,
49
+ severity: policy.severity,
50
+ message: `<${node.element}> has no accessible name. ${policy.because}`.trim(),
51
+ location: node.location,
52
+ evidence: ix.evidence([node.id, usageComponent.id, policy.id]),
53
+ fingerprintIdentity: {
54
+ file: node.file,
55
+ componentId: usageComponent.componentId,
56
+ element: node.element,
57
+ nodePath: node.nodePath,
58
+ },
59
+ attributes: {
60
+ componentId: usageComponent.componentId,
61
+ element: node.element,
62
+ },
63
+ })
64
+ );
65
+ }
66
+
67
+ return findings;
68
+ }
69
+
70
+ function hasAccessibleName(
71
+ props: UsagePropResolvedFact[],
72
+ textChildren: ReadonlyArray<{ text: string }>
73
+ ): boolean {
74
+ for (const child of textChildren) {
75
+ if (typeof child.text === "string" && child.text.trim().length > 0) return true;
76
+ }
77
+ for (const prop of props) {
78
+ if (!NAME_SOURCE_PROPS.has(prop.prop)) continue;
79
+ if (prop.resolution === "spread") continue;
80
+ if (prop.resolution === "dynamic") return true;
81
+ if (prop.resolution === "static") {
82
+ if (typeof prop.value === "string" && prop.value.trim().length > 0) return true;
83
+ if (prop.value !== undefined && prop.value !== "" && prop.value !== null) return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ function isAriaHidden(prop: UsagePropResolvedFact): boolean {
90
+ if (prop.prop !== "aria-hidden") return false;
91
+ if (prop.resolution !== "static") return false;
92
+ return prop.value === true || prop.value === "true";
93
+ }