@usefragments/core 1.10.2 → 1.11.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 (126) hide show
  1. package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
  2. package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
  3. package/dist/chunk-7ULGH74M.js +66 -0
  4. package/dist/chunk-7ULGH74M.js.map +1 -0
  5. package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
  6. package/dist/chunk-ML5S6QNU.js.map +1 -0
  7. package/dist/chunk-PWIJMOI4.js +202 -0
  8. package/dist/chunk-PWIJMOI4.js.map +1 -0
  9. package/dist/{chunk-XN3LSDPY.js → chunk-RPSEABY3.js} +622 -344
  10. package/dist/chunk-RPSEABY3.js.map +1 -0
  11. package/dist/chunk-RYFULE43.js +578 -0
  12. package/dist/chunk-RYFULE43.js.map +1 -0
  13. package/dist/codes/index.d.ts +2 -2
  14. package/dist/codes/index.js +3 -2
  15. package/dist/compiled-types/index.d.ts +327 -2
  16. package/dist/compiled-types/index.js +1 -1
  17. package/dist/generate/index.d.ts +3 -2
  18. package/dist/{governance-CLk_wkP9.d.ts → governance-BAsy1k2H.d.ts} +472 -513
  19. package/dist/governance-telemetry.d.ts +6 -0
  20. package/dist/governance-telemetry.js +1 -1
  21. package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
  22. package/dist/index.d.ts +9165 -1297
  23. package/dist/index.js +3041 -350
  24. package/dist/index.js.map +1 -1
  25. package/dist/manifest.d.ts +228 -0
  26. package/dist/manifest.js +24 -0
  27. package/dist/manifest.js.map +1 -0
  28. package/dist/preview/index.js +45 -1
  29. package/dist/preview/index.js.map +1 -1
  30. package/dist/preview-runtime.d.ts +1 -2
  31. package/dist/preview-runtime.js +150 -14
  32. package/dist/preview-runtime.js.map +1 -1
  33. package/dist/react-types.d.ts +1 -2
  34. package/dist/registry.d.ts +1412 -203
  35. package/dist/registry.js +30 -3
  36. package/dist/schemas/index.d.ts +1 -1
  37. package/dist/schemas/index.js +3 -2
  38. package/dist/storyAdapter.d.ts +1 -2
  39. package/dist/storyAdapter.js +11 -49
  40. package/dist/storyAdapter.js.map +1 -1
  41. package/dist/test-utils.d.ts +3 -2
  42. package/package.json +8 -2
  43. package/src/__tests__/contract-parser.test.ts +318 -277
  44. package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
  45. package/src/__tests__/preview-runtime.test.tsx +30 -8
  46. package/src/__tests__/schema.test.ts +191 -14
  47. package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
  48. package/src/analysis-plan/coverage.ts +181 -0
  49. package/src/analysis-plan/digest.ts +141 -0
  50. package/src/analysis-plan/index.ts +34 -0
  51. package/src/analysis-plan/types.ts +207 -0
  52. package/src/codes/__tests__/codes.test.ts +9 -0
  53. package/src/codes/codes.ts +30 -0
  54. package/src/compiled-types/index.ts +640 -39
  55. package/src/compiled-types/parse.test.ts +145 -4
  56. package/src/component-contract.ts +95 -53
  57. package/src/composition.ts +7 -13
  58. package/src/constants.ts +3 -6
  59. package/src/contract/hash.test.ts +20 -0
  60. package/src/contract/hash.ts +66 -9
  61. package/src/contract/index.ts +24 -1
  62. package/src/contract/manifest.test.ts +94 -0
  63. package/src/contract/manifest.ts +68 -0
  64. package/src/contract/preimage.test.ts +219 -1
  65. package/src/contract/preimage.ts +326 -6
  66. package/src/contract/stamp.test.ts +3 -0
  67. package/src/contract/stamp.ts +1 -1
  68. package/src/contract-parser.ts +54 -30
  69. package/src/defineFragment.test.ts +476 -91
  70. package/src/defineFragment.ts +204 -114
  71. package/src/domain-ids.test.ts +52 -0
  72. package/src/domain-ids.ts +106 -0
  73. package/src/evaluation/evaluate.test.ts +522 -0
  74. package/src/evaluation/evaluate.ts +680 -0
  75. package/src/evaluation/evaluation-v2-receipt-v1.test.ts +760 -0
  76. package/src/evaluation/index.ts +58 -0
  77. package/src/evaluation/receipt.ts +753 -0
  78. package/src/evaluation/types.ts +406 -0
  79. package/src/facts/builders.ts +2 -0
  80. package/src/facts/compile.ts +29 -6
  81. package/src/facts/fact-index.ts +13 -3
  82. package/src/facts/fact-integrity-v1.test.ts +172 -0
  83. package/src/facts/facts.test.ts +15 -0
  84. package/src/facts/ids.ts +46 -3
  85. package/src/facts/index.ts +14 -1
  86. package/src/facts/integrity.ts +134 -0
  87. package/src/facts/types.ts +36 -0
  88. package/src/feature-plan/digest.ts +217 -0
  89. package/src/feature-plan/feature-plan-v1.test.ts +529 -0
  90. package/src/feature-plan/index.ts +65 -0
  91. package/src/feature-plan/types.ts +628 -0
  92. package/src/governance-integrity.test.ts +1 -0
  93. package/src/governance-integrity.ts +5 -3
  94. package/src/governance-telemetry.ts +8 -0
  95. package/src/governance.ts +52 -8
  96. package/src/index.ts +299 -37
  97. package/src/preview/validation.test.ts +62 -0
  98. package/src/preview/validation.ts +48 -2
  99. package/src/preview-runtime.tsx +227 -20
  100. package/src/registry-install-plan.ts +200 -109
  101. package/src/registry-shards.test.ts +263 -0
  102. package/src/registry.ts +237 -0
  103. package/src/repository-binding.test.ts +50 -0
  104. package/src/repository-binding.ts +96 -0
  105. package/src/rules/families.test.ts +36 -0
  106. package/src/rules/finding.ts +7 -2
  107. package/src/rules/index.ts +17 -1
  108. package/src/rules/rule-config.test.ts +66 -0
  109. package/src/rules/rule-config.ts +73 -0
  110. package/src/rules/rules.test.ts +26 -0
  111. package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
  112. package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
  113. package/src/schema.ts +293 -113
  114. package/src/schemas/index.ts +1 -1
  115. package/src/storyAdapter.test.ts +68 -12
  116. package/src/storyAdapter.ts +44 -75
  117. package/src/types.ts +258 -37
  118. package/dist/chunk-RANPUC6C.js +0 -72
  119. package/dist/chunk-RANPUC6C.js.map +0 -1
  120. package/dist/chunk-XN3LSDPY.js.map +0 -1
  121. package/dist/chunk-YF65VYRY.js.map +0 -1
  122. package/src/fragment-types.ts +0 -214
  123. package/src/react-create-element.test.ts +0 -22
  124. package/src/react-create-element.ts +0 -12
  125. package/src/storyFilters.test.ts +0 -350
  126. package/src/storyFilters.ts +0 -253
@@ -1,13 +1,14 @@
1
1
  import type {
2
2
  FragmentDefinition,
3
3
  FragmentDefinitionV2,
4
- CompiledFragment,
4
+ FragmentDefinitionV3,
5
+ FragmentDefinitionV3Body,
5
6
  FragmentComponent,
6
7
  BlockDefinition,
7
- CompiledBlock,
8
8
  AIMetadata,
9
9
  FragmentGenerated,
10
10
  } from "./types.js";
11
+ import type { CompiledBlock, CompiledFragment } from "./compiled-types/index.js";
11
12
  import type {
12
13
  GovernedFragmentDefinition,
13
14
  ResolvedGovernedFragmentDefinition,
@@ -15,15 +16,23 @@ import type {
15
16
  import {
16
17
  fragmentDefinitionSchema,
17
18
  fragmentDefinitionV2Schema,
19
+ fragmentDefinitionV3BodySchema,
18
20
  governedFragmentDefinitionSchema,
19
21
  blockDefinitionSchema,
20
22
  } from "./schema.js";
21
23
  import { resolveComponentGovernance } from "./governance.js";
22
24
 
25
+ /**
26
+ * True only when an explicit govern function or governance record array is
27
+ * present. Optional-key sniffing (`"govern" in def`) is intentionally avoided —
28
+ * that was the optional-key type/runtime lie.
29
+ */
23
30
  function isGovernedDefinition<T>(
24
31
  def: FragmentDefinition<T> | FragmentDefinitionV2<T> | GovernedFragmentDefinition<T>
25
32
  ): def is GovernedFragmentDefinition<T> {
26
- return "govern" in def || "governance" in def;
33
+ if (!def || typeof def !== "object") return false;
34
+ const record = def as Record<string, unknown>;
35
+ return typeof record.govern === "function" || Array.isArray(record.governance);
27
36
  }
28
37
 
29
38
  /**
@@ -33,9 +42,75 @@ function isGovernedDefinition<T>(
33
42
  function isV2Definition<T>(
34
43
  def: FragmentDefinition<T> | FragmentDefinitionV2<T> | GovernedFragmentDefinition<T>
35
44
  ): def is FragmentDefinitionV2<T> {
45
+ if (!def || typeof def !== "object") return false;
36
46
  return !isGovernedDefinition(def) && ("guidance" in def || "examples" in def);
37
47
  }
38
48
 
49
+ function rejectInvalidGovernanceDiscriminants(definition: unknown): void {
50
+ if (!definition || typeof definition !== "object") return;
51
+
52
+ const record = definition as Record<string, unknown>;
53
+ const hasOwn = (key: "govern" | "governance") =>
54
+ Object.prototype.hasOwnProperty.call(record, key);
55
+ const errors: { path: string[]; message: string }[] = [];
56
+
57
+ if (hasOwn("govern") && typeof record.govern !== "function") {
58
+ errors.push({ path: ["govern"], message: "Expected a governance authoring function" });
59
+ }
60
+ if (hasOwn("governance") && !Array.isArray(record.governance)) {
61
+ errors.push({ path: ["governance"], message: "Expected an array of governance records" });
62
+ }
63
+
64
+ if (errors.length > 0) {
65
+ const name =
66
+ record.meta && typeof record.meta === "object" && "name" in record.meta
67
+ ? String((record.meta as Record<string, unknown>).name)
68
+ : "unknown";
69
+ throwInvalidFragment(name, "governed", errors);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Internal marker carried across the compiler's jiti boundary.
75
+ *
76
+ * The class deliberately stays private: callers should consume Core's public
77
+ * schemas, while the compiler needs a non-message-based way to distinguish a
78
+ * Core authoring refusal from an unrelated exception thrown by component code.
79
+ */
80
+ class InvalidFragmentDefinitionError extends Error {
81
+ readonly code = "FUI9010";
82
+ readonly source = "@usefragments/core/defineFragment";
83
+ readonly api: "v1" | "v2" | "governed" | "v3" | "unknown";
84
+ readonly issues: readonly string[];
85
+
86
+ constructor(
87
+ name: string,
88
+ api: "v1" | "v2" | "governed" | "v3" | "unknown",
89
+ errors: { path: (string | number)[]; message: string }[]
90
+ ) {
91
+ const issues = errors
92
+ .map((error) => `${error.path.join(".") || "(root)"}: ${error.message}`)
93
+ .sort();
94
+ super(
95
+ [
96
+ `${InvalidFragmentDefinitionError.name}: Invalid fragment definition for "${name}":`,
97
+ ...issues.map((issue) => ` - ${issue}`),
98
+ ].join("\n")
99
+ );
100
+ this.name = InvalidFragmentDefinitionError.name;
101
+ this.api = api;
102
+ this.issues = issues;
103
+ }
104
+ }
105
+
106
+ function throwInvalidFragment(
107
+ name: string,
108
+ api: "v1" | "v2" | "governed" | "v3" | "unknown",
109
+ errors: { path: (string | number)[]; message: string }[]
110
+ ) {
111
+ throw new InvalidFragmentDefinitionError(name, api, errors);
112
+ }
113
+
39
114
  /**
40
115
  * Normalize a v2 definition to v1 shape for downstream compatibility.
41
116
  * The build pipeline, compiler, and validators all work with v1 internally.
@@ -52,13 +127,14 @@ export function normalizeToV1<T>(def: FragmentDefinitionV2<T>): FragmentDefiniti
52
127
  };
53
128
  }
54
129
 
55
- // Map _provenance → _generated (narrow source type, drop extended fields)
130
+ // Map _provenance → _generated (drop v2-only extended fields)
56
131
  let generated: FragmentGenerated | undefined;
57
132
  if (def._provenance) {
58
133
  generated = {
59
- source: def._provenance.source === "scan" ? "ai" : def._provenance.source,
134
+ source: def._provenance.source,
60
135
  sourceFile: def._provenance.sourceFile,
61
136
  confidence: def._provenance.confidence,
137
+ verified: def._provenance.verified,
62
138
  timestamp: def._provenance.timestamp,
63
139
  };
64
140
  }
@@ -76,78 +152,53 @@ export function normalizeToV1<T>(def: FragmentDefinitionV2<T>): FragmentDefiniti
76
152
  };
77
153
  }
78
154
 
79
- /**
80
- * Define a fragment for a component.
81
- *
82
- * This is the main API for creating fragment documentation.
83
- * It provides runtime validation and type safety.
84
- *
85
- * Accepts both v1 and v2 shapes:
86
- * - v1: `{ usage, variants, ai, _generated }`
87
- * - v2: `{ guidance, examples, composition, _provenance }`
88
- *
89
- * @example v1
90
- * ```tsx
91
- * import { defineFragment } from '@usefragments/core';
92
- * import { Button } from './Button';
93
- *
94
- * export default defineFragment({
95
- * component: Button,
96
- * meta: {
97
- * name: 'Button',
98
- * description: 'Primary action trigger',
99
- * category: 'actions',
100
- * },
101
- * usage: {
102
- * when: ['User needs to trigger an action'],
103
- * whenNot: ['Navigation without side effects'],
104
- * },
105
- * props: {
106
- * variant: {
107
- * type: 'enum',
108
- * values: ['primary', 'secondary'],
109
- * default: 'primary',
110
- * description: 'Visual style',
111
- * },
112
- * },
113
- * variants: [
114
- * {
115
- * name: 'Default',
116
- * description: 'Default button',
117
- * render: () => <Button>Click me</Button>,
118
- * },
119
- * ],
120
- * });
121
- * ```
122
- *
123
- * @example v2
124
- * ```tsx
125
- * import { defineFragment } from '@usefragments/core';
126
- * import { Card } from './Card';
127
- *
128
- * export default defineFragment({
129
- * component: Card,
130
- * meta: { name: 'Card', description: 'Content container', category: 'layout' },
131
- * guidance: {
132
- * when: ['Grouping related content'],
133
- * whenNot: ['Full-page layouts'],
134
- * },
135
- * props: { ... },
136
- * examples: [{ name: 'Default', description: 'Basic card', render: () => <Card>...</Card> }],
137
- * composition: { pattern: 'compound', subComponents: ['Header', 'Body', 'Footer'] },
138
- * });
139
- * ```
140
- */
141
- export function defineFragment<TProps>(
142
- definition: GovernedFragmentDefinition<TProps>
143
- ): ResolvedGovernedFragmentDefinition<TProps>;
144
- export function defineFragment<TProps>(
145
- definition: FragmentDefinition<TProps>
146
- ): FragmentDefinition<TProps>;
147
- export function defineFragment<TProps>(
148
- definition: FragmentDefinitionV2<TProps>
149
- ): FragmentDefinitionV2<TProps>;
150
- export function defineFragment<TProps>(
155
+ function defineFragmentV3<TProps>(
156
+ component: FragmentComponent<TProps>,
157
+ definition: FragmentDefinitionV3Body<TProps>
158
+ ): FragmentDefinitionV3<TProps> {
159
+ const result = fragmentDefinitionV3BodySchema.safeParse(definition);
160
+ if (!result.success) {
161
+ throwInvalidFragment(
162
+ definition?.meta?.name || "unknown",
163
+ "v3",
164
+ result.error.errors.map((e) => ({ path: e.path, message: e.message }))
165
+ );
166
+ }
167
+
168
+ const governance =
169
+ typeof definition.govern === "function" || Array.isArray(definition.governance)
170
+ ? resolveComponentGovernance({
171
+ component,
172
+ meta: {
173
+ name: definition.meta.name,
174
+ description: definition.meta.purpose,
175
+ category: definition.meta.category,
176
+ status: definition.meta.status,
177
+ tags: definition.meta.tags,
178
+ since: definition.meta.since,
179
+ dependencies: definition.meta.dependencies,
180
+ figma: definition.meta.figma,
181
+ figmaProps: definition.meta.figmaProps,
182
+ },
183
+ guidance: {
184
+ when: definition.guidance.when,
185
+ whenNot: definition.guidance.whenNot,
186
+ guidelines: definition.guidance.guidelines,
187
+ accessibility: definition.guidance.accessibility,
188
+ },
189
+ govern: definition.govern,
190
+ governance: definition.governance,
191
+ } as GovernedFragmentDefinition<TProps>)
192
+ : [];
193
+
194
+ return {
195
+ ...definition,
196
+ component,
197
+ governance,
198
+ };
199
+ }
200
+
201
+ function defineFragmentOneArg<TProps>(
151
202
  definition:
152
203
  | FragmentDefinition<TProps>
153
204
  | FragmentDefinitionV2<TProps>
@@ -156,23 +207,30 @@ export function defineFragment<TProps>(
156
207
  | FragmentDefinition<TProps>
157
208
  | FragmentDefinitionV2<TProps>
158
209
  | ResolvedGovernedFragmentDefinition<TProps> {
159
- // Validate at runtime in development
160
- if (process.env.NODE_ENV !== "production") {
161
- const governed = isGovernedDefinition(definition);
162
- const v2 = isV2Definition(definition);
163
- const schema = governed
164
- ? governedFragmentDefinitionSchema
165
- : v2
166
- ? fragmentDefinitionV2Schema
167
- : fragmentDefinitionSchema;
168
- const result = schema.safeParse(definition);
169
- if (!result.success) {
170
- const name = definition.meta?.name || "unknown";
171
- const errors = result.error.errors
172
- .map((e) => ` - ${e.path.join(".")}: ${e.message}`)
173
- .join("\n");
174
- throw new Error(`Invalid fragment definition for "${name}":\n${errors}`);
175
- }
210
+ if (!definition || typeof definition !== "object") {
211
+ throwInvalidFragment("unknown", "unknown", [
212
+ { path: [], message: "Expected a fragment definition object" },
213
+ ]);
214
+ }
215
+
216
+ // Reserved governance keys may not fall through to the permissive legacy
217
+ // schemas merely because their values are malformed.
218
+ rejectInvalidGovernanceDiscriminants(definition);
219
+
220
+ const governed = isGovernedDefinition(definition);
221
+ const v2 = isV2Definition(definition);
222
+ const schema = governed
223
+ ? governedFragmentDefinitionSchema
224
+ : v2
225
+ ? fragmentDefinitionV2Schema
226
+ : fragmentDefinitionSchema;
227
+ const result = schema.safeParse(definition);
228
+ if (!result.success) {
229
+ throwInvalidFragment(
230
+ definition?.meta?.name || "unknown",
231
+ governed ? "governed" : v2 ? "v2" : "v1",
232
+ result.error.errors.map((e) => ({ path: e.path, message: e.message }))
233
+ );
176
234
  }
177
235
 
178
236
  if (isGovernedDefinition(definition)) {
@@ -186,6 +244,40 @@ export function defineFragment<TProps>(
186
244
  return definition;
187
245
  }
188
246
 
247
+ /**
248
+ * Define a fragment for a component.
249
+ *
250
+ * **v3 (arity discriminant):** `defineFragment(Component, definition)` — the
251
+ * only v3 entry. Optional-key sniffing is not used.
252
+ *
253
+ * **v1 / v2 / governed (one argument):** preserved. Governed definitions require
254
+ * an actual `govern` function or `governance` array (not merely an optional key).
255
+ */
256
+ export function defineFragment<C extends FragmentComponent<any>>(
257
+ component: C,
258
+ definition: FragmentDefinitionV3Body<C extends FragmentComponent<infer P> ? P : unknown>
259
+ ): FragmentDefinitionV3<C extends FragmentComponent<infer P> ? P : unknown>;
260
+ export function defineFragment<TProps>(
261
+ definition: GovernedFragmentDefinition<TProps>
262
+ ): ResolvedGovernedFragmentDefinition<TProps>;
263
+ export function defineFragment<TProps>(
264
+ definition: FragmentDefinition<TProps>
265
+ ): FragmentDefinition<TProps>;
266
+ export function defineFragment<TProps>(
267
+ definition: FragmentDefinitionV2<TProps>
268
+ ): FragmentDefinitionV2<TProps>;
269
+ export function defineFragment(componentOrDefinition: unknown, maybeDefinition?: unknown): unknown {
270
+ if (arguments.length >= 2) {
271
+ return defineFragmentV3(
272
+ componentOrDefinition as FragmentComponent,
273
+ maybeDefinition as FragmentDefinitionV3Body
274
+ );
275
+ }
276
+ return defineFragmentOneArg(
277
+ componentOrDefinition as FragmentDefinition | FragmentDefinitionV2 | GovernedFragmentDefinition
278
+ );
279
+ }
280
+
189
281
  /**
190
282
  * Compile a fragment definition to JSON-serializable format.
191
283
  * Used for generating fragments.json for AI consumption.
@@ -206,12 +298,20 @@ export function compileFragment(
206
298
  relations: definition.relations,
207
299
  variants: definition.examples ?? [],
208
300
  contract: definition.contract,
301
+ ai: definition.composition
302
+ ? {
303
+ compositionPattern: definition.composition.pattern,
304
+ subComponents: definition.composition.subComponents,
305
+ requiredChildren: definition.composition.requiredChildren,
306
+ commonPatterns: definition.composition.commonPatterns,
307
+ }
308
+ : undefined,
209
309
  _generated: definition._provenance
210
310
  ? {
211
- source:
212
- definition._provenance.source === "scan" ? "ai" : definition._provenance.source,
311
+ source: definition._provenance.source,
213
312
  sourceFile: definition._provenance.sourceFile,
214
313
  confidence: definition._provenance.confidence,
314
+ verified: definition._provenance.verified,
215
315
  timestamp: definition._provenance.timestamp,
216
316
  }
217
317
  : undefined,
@@ -249,24 +349,19 @@ export function compileFragment(
249
349
  * wire together for common use cases.
250
350
  */
251
351
  export function defineBlock(definition: BlockDefinition): BlockDefinition {
252
- if (process.env.NODE_ENV !== "production") {
253
- const result = blockDefinitionSchema.safeParse(definition);
254
- if (!result.success) {
255
- const errors = result.error.errors
256
- .map((e) => ` - ${e.path.join(".")}: ${e.message}`)
257
- .join("\n");
258
- throw new Error(`Invalid block definition for "${definition.name || "unknown"}":\n${errors}`);
259
- }
352
+ const result = blockDefinitionSchema.safeParse(definition);
353
+ if (!result.success) {
354
+ const errors = result.error.errors
355
+ .map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`)
356
+ .sort()
357
+ .map((issue) => ` - ${issue}`)
358
+ .join("\n");
359
+ throw new Error(`Invalid block definition for "${definition.name || "unknown"}":\n${errors}`);
260
360
  }
261
361
 
262
362
  return definition;
263
363
  }
264
364
 
265
- /**
266
- * @deprecated Use defineBlock instead
267
- */
268
- export const defineRecipe = defineBlock;
269
-
270
365
  /**
271
366
  * Compile a block definition to JSON-serializable format.
272
367
  */
@@ -282,11 +377,6 @@ export function compileBlock(definition: BlockDefinition, filePath: string): Com
282
377
  };
283
378
  }
284
379
 
285
- /**
286
- * @deprecated Use compileBlock instead
287
- */
288
- export const compileRecipe = compileBlock;
289
-
290
380
  /**
291
381
  * Type helper for extracting props type from a component
292
382
  */
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ analysisPlanIdFromDigest,
4
+ evaluationReceiptIdFromDigest,
5
+ mintEvaluationReceiptId,
6
+ featurePlanIdFromEntropy,
7
+ featureRevisionIdFromDigest,
8
+ parseAnalysisPlanId,
9
+ parseEvaluationReceiptId,
10
+ parseFeaturePlanId,
11
+ parseFeatureRevisionId,
12
+ } from "./domain-ids.js";
13
+
14
+ const DIGEST = "0123456789abcdef".repeat(4);
15
+
16
+ describe("V1 domain IDs", () => {
17
+ it("retains the full digest in content-derived IDs", () => {
18
+ expect(analysisPlanIdFromDigest(DIGEST)).toBe(`apl_${DIGEST}`);
19
+ expect(evaluationReceiptIdFromDigest(DIGEST)).toBe(`evr_${DIGEST}`);
20
+ expect(mintEvaluationReceiptId(DIGEST)).toBe(`evr_${DIGEST}`);
21
+ expect(featureRevisionIdFromDigest(DIGEST)).toBe(`fpr_${DIGEST}`);
22
+ });
23
+
24
+ it("keeps feature workflow identity opaque and exactly 128-bit", () => {
25
+ const entropy = "00112233445566778899aabbccddeeff";
26
+ expect(featurePlanIdFromEntropy(entropy)).toBe(`fpl_${entropy}`);
27
+ });
28
+
29
+ it.each([
30
+ [parseAnalysisPlanId, `apl_${DIGEST}`],
31
+ [parseEvaluationReceiptId, `evr_${DIGEST}`],
32
+ [parseFeatureRevisionId, `fpr_${DIGEST}`],
33
+ [parseFeaturePlanId, "fpl_00112233445566778899aabbccddeeff"],
34
+ ] as const)("round-trips a valid ID", (parse, value) => {
35
+ expect(parse(value)).toBe(value);
36
+ });
37
+
38
+ it.each([
39
+ `apl_${DIGEST.slice(0, -1)}`,
40
+ `apl_${DIGEST.toUpperCase()}`,
41
+ `evr_${DIGEST}extra`,
42
+ `fpr_${DIGEST.slice(0, 32)}`,
43
+ "fpl_00112233445566778899AABBCCDDEEFF",
44
+ ])("rejects truncated, uppercase, or extended IDs: %s", (value) => {
45
+ expect(() => {
46
+ if (value.startsWith("apl_")) parseAnalysisPlanId(value);
47
+ else if (value.startsWith("evr_")) parseEvaluationReceiptId(value);
48
+ else if (value.startsWith("fpr_")) parseFeatureRevisionId(value);
49
+ else parseFeaturePlanId(value);
50
+ }).toThrow();
51
+ });
52
+ });
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+
3
+ const FULL_DIGEST_PATTERN = /^[0-9a-f]{64}$/u;
4
+ const WORKFLOW_ENTROPY_PATTERN = /^[0-9a-f]{32}$/u;
5
+
6
+ export declare const analysisPlanIdBrand: unique symbol;
7
+ export declare const evaluationReceiptIdBrand: unique symbol;
8
+ export declare const featurePlanIdBrand: unique symbol;
9
+ export declare const featureRevisionIdBrand: unique symbol;
10
+
11
+ export type DigestHex = string;
12
+ export type AnalysisPlanId = `apl_${string}` & { readonly [analysisPlanIdBrand]: void };
13
+ export type EvaluationReceiptId = `evr_${string}` & {
14
+ readonly [evaluationReceiptIdBrand]: void;
15
+ };
16
+ export type FeaturePlanId = `fpl_${string}` & { readonly [featurePlanIdBrand]: void };
17
+ export type FeatureRevisionId = `fpr_${string}` & {
18
+ readonly [featureRevisionIdBrand]: void;
19
+ };
20
+
21
+ export const digestHexStringSchema = z
22
+ .string()
23
+ .regex(FULL_DIGEST_PATTERN, "Expected 64 lowercase hexadecimal characters");
24
+ export const digestHexSchema = digestHexStringSchema;
25
+
26
+ export const analysisPlanIdStringSchema = z
27
+ .string()
28
+ .regex(/^apl_[0-9a-f]{64}$/u, "Invalid analysis plan ID");
29
+ export const analysisPlanIdSchema = analysisPlanIdStringSchema.transform(
30
+ (value) => value as AnalysisPlanId
31
+ );
32
+
33
+ export const evaluationReceiptIdSchema = z
34
+ .string()
35
+ .regex(/^evr_[0-9a-f]{64}$/u, "Invalid evaluation receipt ID")
36
+ .transform((value) => value as EvaluationReceiptId);
37
+ export const evaluationReceiptIdStringSchema = z
38
+ .string()
39
+ .regex(/^evr_[0-9a-f]{64}$/u, "Invalid evaluation receipt ID");
40
+
41
+ export const featurePlanIdStringSchema = z
42
+ .string()
43
+ .regex(/^fpl_[0-9a-f]{32}$/u, "Invalid feature plan ID");
44
+ export const featurePlanIdSchema = featurePlanIdStringSchema.transform(
45
+ (value) => value as FeaturePlanId
46
+ );
47
+
48
+ export const featureRevisionIdStringSchema = z
49
+ .string()
50
+ .regex(/^fpr_[0-9a-f]{64}$/u, "Invalid feature revision ID");
51
+ export const featureRevisionIdSchema = featureRevisionIdStringSchema.transform(
52
+ (value) => value as FeatureRevisionId
53
+ );
54
+
55
+ export function parseDigestHex(value: string): DigestHex {
56
+ return digestHexSchema.parse(value);
57
+ }
58
+
59
+ export function analysisPlanIdFromDigest(digest: string): AnalysisPlanId {
60
+ return analysisPlanIdSchema.parse(`apl_${parseDigestHex(digest)}`);
61
+ }
62
+
63
+ export function evaluationReceiptIdFromDigest(digest: string): EvaluationReceiptId {
64
+ return evaluationReceiptIdSchema.parse(`evr_${parseDigestHex(digest)}`);
65
+ }
66
+
67
+ /**
68
+ * Mint a Cloud evaluation receipt id from a 64-hex digest (kernel
69
+ * `resultDigest` on the ingest envelope; receipt-semantic digest on the
70
+ * adoption envelope). Same `evr_` + 64 hex contract as
71
+ * `evaluationReceiptIdFromDigest`.
72
+ */
73
+ export function mintEvaluationReceiptId(resultDigest: string): EvaluationReceiptId {
74
+ return evaluationReceiptIdFromDigest(resultDigest);
75
+ }
76
+
77
+ export function featureRevisionIdFromDigest(digest: string): FeatureRevisionId {
78
+ return featureRevisionIdSchema.parse(`fpr_${parseDigestHex(digest)}`);
79
+ }
80
+
81
+ /**
82
+ * Construct a stable feature-workflow ID from 128 bits supplied by the caller.
83
+ * Core deliberately owns neither a clock nor a random-number generator.
84
+ */
85
+ export function featurePlanIdFromEntropy(entropyHex: string): FeaturePlanId {
86
+ if (!WORKFLOW_ENTROPY_PATTERN.test(entropyHex)) {
87
+ throw new TypeError("Feature plan entropy must be exactly 128-bit lowercase hexadecimal");
88
+ }
89
+ return featurePlanIdSchema.parse(`fpl_${entropyHex}`);
90
+ }
91
+
92
+ export function parseAnalysisPlanId(value: string): AnalysisPlanId {
93
+ return analysisPlanIdSchema.parse(value);
94
+ }
95
+
96
+ export function parseEvaluationReceiptId(value: string): EvaluationReceiptId {
97
+ return evaluationReceiptIdSchema.parse(value);
98
+ }
99
+
100
+ export function parseFeaturePlanId(value: string): FeaturePlanId {
101
+ return featurePlanIdSchema.parse(value);
102
+ }
103
+
104
+ export function parseFeatureRevisionId(value: string): FeatureRevisionId {
105
+ return featureRevisionIdSchema.parse(value);
106
+ }