@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
@@ -0,0 +1,529 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { featurePlanIdFromEntropy } from "../domain-ids.js";
3
+ import {
4
+ agentFeatureContextV1Schema,
5
+ approvedFeatureManifestV1Schema,
6
+ buildApprovedFeatureManifestV1,
7
+ buildFeatureProposalV1,
8
+ featureProposalV1Schema,
9
+ parseApprovedFeatureManifestV1,
10
+ parseFeatureProposalV1,
11
+ type ApprovedFeatureManifestInputV1,
12
+ type FeatureProposalInputV1,
13
+ } from "./index.js";
14
+
15
+ const A = "a".repeat(64);
16
+ const B = "b".repeat(64);
17
+ const C = "c".repeat(64);
18
+ const D = "d".repeat(64);
19
+ const PLAN_ID = featurePlanIdFromEntropy("00112233445566778899aabbccddeeff");
20
+
21
+ function proposalInput(overrides: Partial<FeatureProposalInputV1> = {}): FeatureProposalInputV1 {
22
+ return {
23
+ schemaVersion: 1,
24
+ kind: "proposal",
25
+ planId: PLAN_ID,
26
+ pins: {
27
+ bindingId: "binding_1",
28
+ grounding: {
29
+ sourceCommitId: "head-sha",
30
+ analysisPlanDigest: A,
31
+ contextSliceDigest: B,
32
+ },
33
+ authority: {
34
+ fcid: C,
35
+ contractArtifactDigest: D,
36
+ analysisObligationsDigest: A,
37
+ },
38
+ evidenceDigest: B,
39
+ },
40
+ placement: {
41
+ targetKind: "page",
42
+ targetRef: "settings/notifications",
43
+ routeRef: "/settings/notifications",
44
+ parentSlotRef: "WorkspacePage.content",
45
+ evidenceRefIds: ["source-shell"],
46
+ },
47
+ componentUses: [
48
+ {
49
+ useId: "switch",
50
+ componentId: "Switch",
51
+ importPath: "@usefragments/ui",
52
+ purpose: "Toggle notification preference",
53
+ props: { size: "md", disabled: false },
54
+ evidenceRefIds: ["contract-switch", "source-form"],
55
+ },
56
+ ],
57
+ tokenUses: [
58
+ {
59
+ useId: "section-gap",
60
+ tokenId: "--fui-space-3",
61
+ role: "gap",
62
+ evidenceRefIds: ["contract-space"],
63
+ },
64
+ ],
65
+ patternUses: [
66
+ {
67
+ useId: "settings-row",
68
+ patternId: "cloud.settings-row",
69
+ patternRevisionDigest: A,
70
+ status: "approved_guidance",
71
+ disposition: "reuse",
72
+ evidenceRefIds: ["source-form"],
73
+ },
74
+ ],
75
+ layout: {
76
+ concerns: [
77
+ {
78
+ concern: "outer_inline_spacing",
79
+ ownerLayer: "application_shell",
80
+ ownerRef: "WorkspacePage",
81
+ childAction: "consume",
82
+ evidenceRefIds: ["source-shell"],
83
+ },
84
+ {
85
+ concern: "child_gap",
86
+ ownerLayer: "section",
87
+ ownerRef: "NotificationPreferencesSection",
88
+ childAction: "own",
89
+ tokenRef: "--fui-space-3",
90
+ evidenceRefIds: ["contract-space"],
91
+ },
92
+ ],
93
+ },
94
+ responsive: [
95
+ {
96
+ decisionId: "responsive-row",
97
+ source: "observed",
98
+ breakpointRef: "$fui-breakpoint-md",
99
+ behavior: "Stack label and control below md",
100
+ evidenceRefIds: ["source-form"],
101
+ },
102
+ ],
103
+ scenarios: [
104
+ {
105
+ scenarioId: "save-error",
106
+ viewportRef: "mobile",
107
+ dataState: "error",
108
+ interactionState: "saving",
109
+ acceptanceIds: ["A2"],
110
+ },
111
+ ],
112
+ gaps: [
113
+ {
114
+ gapId: "inline-field",
115
+ capability: "Inline labelled boolean field",
116
+ required: false,
117
+ disposition: "local_composition",
118
+ reason: "Reuse the existing settings-row composition",
119
+ evidenceRefIds: ["source-form"],
120
+ },
121
+ ],
122
+ acceptance: [
123
+ {
124
+ acceptanceId: "A1",
125
+ statement: "Owner saves a preference and reload sees it",
126
+ proof: "integration",
127
+ required: true,
128
+ evidenceRefIds: ["user-intent"],
129
+ },
130
+ {
131
+ acceptanceId: "A2",
132
+ statement: "A failed save restores the prior value",
133
+ proof: "browser",
134
+ required: true,
135
+ evidenceRefIds: ["user-intent"],
136
+ },
137
+ ],
138
+ questions: [
139
+ {
140
+ decisionId: "save-model",
141
+ kind: "reuse",
142
+ question: "Save immediately or as a section?",
143
+ options: [
144
+ { optionId: "immediate", label: "Immediately", consequence: "Matches current settings" },
145
+ { optionId: "section", label: "Save section", consequence: "Adds dirty state" },
146
+ ],
147
+ recommendedOptionId: "immediate",
148
+ required: true,
149
+ evidenceRefIds: ["source-form"],
150
+ },
151
+ ],
152
+ evidenceRefs: [
153
+ {
154
+ evidenceId: "user-intent",
155
+ kind: "user_intent",
156
+ authority: "user_asserted",
157
+ ref: "intent:settings-notifications",
158
+ digest: A,
159
+ },
160
+ {
161
+ evidenceId: "source-shell",
162
+ kind: "source",
163
+ authority: "source_observed",
164
+ ref: "apps/cloud/src/WorkspacePage.tsx:1",
165
+ digest: B,
166
+ },
167
+ {
168
+ evidenceId: "source-form",
169
+ kind: "source",
170
+ authority: "source_observed",
171
+ ref: "apps/cloud/src/SettingsRow.tsx:1",
172
+ digest: C,
173
+ },
174
+ {
175
+ evidenceId: "contract-switch",
176
+ kind: "contract",
177
+ authority: "contract_approved",
178
+ ref: "component:Switch",
179
+ digest: D,
180
+ },
181
+ {
182
+ evidenceId: "contract-space",
183
+ kind: "contract",
184
+ authority: "contract_approved",
185
+ ref: "token:--fui-space-3",
186
+ digest: A,
187
+ },
188
+ ],
189
+ ...overrides,
190
+ };
191
+ }
192
+
193
+ function manifestInput(
194
+ overrides: Partial<ApprovedFeatureManifestInputV1> = {}
195
+ ): ApprovedFeatureManifestInputV1 {
196
+ const proposal = buildFeatureProposalV1(proposalInput());
197
+ return {
198
+ schemaVersion: 1,
199
+ kind: "approved_manifest",
200
+ planId: proposal.planId,
201
+ parentRevisionDigest: proposal.revisionDigest,
202
+ proposalRevisionDigest: proposal.revisionDigest,
203
+ pins: proposal.pins,
204
+ validatedGrounding: { ...proposal.pins.grounding },
205
+ placement: proposal.placement,
206
+ componentUses: proposal.componentUses,
207
+ tokenUses: proposal.tokenUses,
208
+ patternUses: proposal.patternUses,
209
+ layout: proposal.layout,
210
+ responsive: proposal.responsive,
211
+ scenarios: proposal.scenarios,
212
+ gaps: proposal.gaps,
213
+ acceptance: proposal.acceptance,
214
+ evidenceRefs: proposal.evidenceRefs,
215
+ decisions: [{ decisionId: "save-model", selectedOptionId: "immediate" }],
216
+ approval: {
217
+ approverUserId: "user_1",
218
+ decisionDigest: D,
219
+ approvedAt: "2026-09-01T12:00:00.000Z",
220
+ },
221
+ ...overrides,
222
+ };
223
+ }
224
+
225
+ describe("FeatureProposalV1", () => {
226
+ it("has a checked-in stable revision digest", () => {
227
+ const proposal = buildFeatureProposalV1(proposalInput());
228
+ expect(proposal.revisionDigest).toBe(
229
+ "a0e17ecec3c2a88ed421bd1102c1a806be34887f8e053b7236cd5e9551fd0cf7"
230
+ );
231
+ expect(proposal.revisionId).toBe(`fpr_${proposal.revisionDigest}`);
232
+ expect(parseFeatureProposalV1(proposal)).toEqual(proposal);
233
+ });
234
+
235
+ it("normalizes set-like order but preserves decision-option order", () => {
236
+ const first = buildFeatureProposalV1(proposalInput());
237
+ const reordered = buildFeatureProposalV1(
238
+ proposalInput({
239
+ evidenceRefs: [...proposalInput().evidenceRefs].reverse(),
240
+ componentUses: proposalInput().componentUses.map((use) => ({
241
+ ...use,
242
+ evidenceRefIds: [...use.evidenceRefIds].reverse(),
243
+ props: { disabled: false, size: "md" },
244
+ })),
245
+ acceptance: [...proposalInput().acceptance].reverse(),
246
+ layout: { concerns: [...proposalInput().layout.concerns].reverse() },
247
+ })
248
+ );
249
+ expect(reordered.revisionDigest).toBe(first.revisionDigest);
250
+
251
+ const optionsReordered = buildFeatureProposalV1(
252
+ proposalInput({
253
+ questions: proposalInput().questions.map((question) => ({
254
+ ...question,
255
+ options: [...question.options].reverse(),
256
+ })),
257
+ })
258
+ );
259
+ expect(optionsReordered.revisionDigest).not.toBe(first.revisionDigest);
260
+ });
261
+
262
+ it("uses locale-independent Unicode ordering for canonical feature records", () => {
263
+ const unicodeEvidence = [
264
+ {
265
+ evidenceId: "unicode-ä",
266
+ kind: "source" as const,
267
+ authority: "source_observed" as const,
268
+ ref: "src/ä.tsx:1",
269
+ digest: A,
270
+ },
271
+ {
272
+ evidenceId: "unicode-z",
273
+ kind: "source" as const,
274
+ authority: "source_observed" as const,
275
+ ref: "src/z.tsx:1",
276
+ digest: B,
277
+ },
278
+ ];
279
+ const first = buildFeatureProposalV1(
280
+ proposalInput({ evidenceRefs: [...proposalInput().evidenceRefs, ...unicodeEvidence] })
281
+ );
282
+ const reordered = buildFeatureProposalV1(
283
+ proposalInput({
284
+ evidenceRefs: [...proposalInput().evidenceRefs, ...unicodeEvidence].reverse(),
285
+ })
286
+ );
287
+ expect(
288
+ first.evidenceRefs
289
+ .map((evidence) => evidence.evidenceId)
290
+ .filter((evidenceId) => evidenceId.startsWith("unicode-"))
291
+ ).toEqual(["unicode-z", "unicode-ä"]);
292
+ expect(reordered.revisionDigest).toBe(first.revisionDigest);
293
+ });
294
+
295
+ it("changes identity for authority, layout, and workflow changes", () => {
296
+ const first = buildFeatureProposalV1(proposalInput());
297
+ expect(
298
+ buildFeatureProposalV1(
299
+ proposalInput({ pins: { ...proposalInput().pins, evidenceDigest: C } })
300
+ ).revisionDigest
301
+ ).not.toBe(first.revisionDigest);
302
+ expect(
303
+ buildFeatureProposalV1(
304
+ proposalInput({
305
+ layout: {
306
+ concerns: proposalInput().layout.concerns.map((row) =>
307
+ row.concern === "child_gap"
308
+ ? { ...row, ownerLayer: "pattern" as const, ownerRef: "cloud.settings-row" }
309
+ : row
310
+ ),
311
+ },
312
+ })
313
+ ).revisionDigest
314
+ ).not.toBe(first.revisionDigest);
315
+ expect(
316
+ buildFeatureProposalV1(
317
+ proposalInput({ planId: featurePlanIdFromEntropy("ffeeddccbbaa99887766554433221100") })
318
+ ).revisionDigest
319
+ ).not.toBe(first.revisionDigest);
320
+ });
321
+
322
+ it("rejects unknown fields, dangling evidence, invalid gaps, and duplicate layout owners", () => {
323
+ expect(() =>
324
+ featureProposalV1Schema.parse({ ...buildFeatureProposalV1(proposalInput()), extra: true })
325
+ ).toThrow();
326
+ expect(() =>
327
+ buildFeatureProposalV1(
328
+ proposalInput({ placement: { ...proposalInput().placement, evidenceRefIds: ["missing"] } })
329
+ )
330
+ ).toThrow();
331
+ expect(() =>
332
+ buildFeatureProposalV1(
333
+ proposalInput({
334
+ componentUses: [
335
+ {
336
+ ...proposalInput().componentUses[0],
337
+ props: { label: "x".repeat(33 * 1024) },
338
+ },
339
+ ],
340
+ })
341
+ )
342
+ ).toThrow();
343
+ expect(() =>
344
+ buildFeatureProposalV1(
345
+ proposalInput({
346
+ gaps: [{ ...proposalInput().gaps[0], disposition: "invent" as never }],
347
+ })
348
+ )
349
+ ).toThrow();
350
+ expect(() =>
351
+ buildFeatureProposalV1(
352
+ proposalInput({
353
+ layout: {
354
+ concerns: [
355
+ ...proposalInput().layout.concerns,
356
+ { ...proposalInput().layout.concerns[0], ownerRef: "FeaturePage" },
357
+ ],
358
+ },
359
+ })
360
+ )
361
+ ).toThrow();
362
+ });
363
+ });
364
+
365
+ describe("ApprovedFeatureManifestV1", () => {
366
+ it("pins approved choices and excludes approvedAt from semantic identity", () => {
367
+ const manifest = buildApprovedFeatureManifestV1(manifestInput());
368
+ expect(manifest.revisionDigest).toBe(
369
+ "2b9eb1d40d853cd47412d811526eaaba6aaaa7cb855778d08fa1d4acd930bcaa"
370
+ );
371
+ expect(parseApprovedFeatureManifestV1(manifest)).toEqual(manifest);
372
+ const laterTimestamp = buildApprovedFeatureManifestV1(
373
+ manifestInput({
374
+ approval: { ...manifestInput().approval, approvedAt: "2026-09-02T12:00:00.000Z" },
375
+ })
376
+ );
377
+ expect(laterTimestamp.revisionDigest).toBe(manifest.revisionDigest);
378
+ const differentChoice = buildApprovedFeatureManifestV1(
379
+ manifestInput({ decisions: [{ decisionId: "save-model", selectedOptionId: "section" }] })
380
+ );
381
+ expect(differentChoice.revisionDigest).not.toBe(manifest.revisionDigest);
382
+ });
383
+
384
+ it("blocks required unresolved/proposed gaps and dangling override decisions", () => {
385
+ expect(() =>
386
+ buildApprovedFeatureManifestV1(
387
+ manifestInput({
388
+ gaps: [
389
+ {
390
+ ...manifestInput().gaps[0],
391
+ required: true,
392
+ disposition: "unresolved",
393
+ },
394
+ ],
395
+ })
396
+ )
397
+ ).toThrow();
398
+ expect(() =>
399
+ buildApprovedFeatureManifestV1(
400
+ manifestInput({
401
+ gaps: [
402
+ {
403
+ ...manifestInput().gaps[0],
404
+ required: true,
405
+ disposition: "proposed_contract_addition",
406
+ proposedContractCandidateId: "candidate-field-row",
407
+ },
408
+ ],
409
+ })
410
+ )
411
+ ).toThrow();
412
+ expect(() =>
413
+ buildApprovedFeatureManifestV1(
414
+ manifestInput({
415
+ layout: {
416
+ concerns: manifestInput().layout.concerns.map((row) =>
417
+ row.concern === "child_gap"
418
+ ? { ...row, childAction: "override" as const, overrideDecisionId: "missing" }
419
+ : row
420
+ ),
421
+ },
422
+ })
423
+ )
424
+ ).toThrow();
425
+ });
426
+
427
+ it("allows compatible re-grounding to a newer source and exact analysis plan", () => {
428
+ const advanced = buildApprovedFeatureManifestV1(
429
+ manifestInput({
430
+ validatedGrounding: {
431
+ sourceCommitId: "next-head-sha",
432
+ analysisPlanDigest: C,
433
+ contextSliceDigest: manifestInput().pins.grounding.contextSliceDigest,
434
+ },
435
+ })
436
+ );
437
+ expect(advanced.validatedGrounding).toMatchObject({
438
+ sourceCommitId: "next-head-sha",
439
+ analysisPlanDigest: C,
440
+ });
441
+ expect(() =>
442
+ buildApprovedFeatureManifestV1(
443
+ manifestInput({
444
+ validatedGrounding: {
445
+ ...manifestInput().validatedGrounding,
446
+ contextSliceDigest: D,
447
+ },
448
+ })
449
+ )
450
+ ).toThrow(/material context/u);
451
+ });
452
+
453
+ it.each(["observed", "candidate", "deprecated"] as const)(
454
+ "prevents a %s pattern from becoming an approved reusable claim",
455
+ (status) => {
456
+ expect(() =>
457
+ buildApprovedFeatureManifestV1(
458
+ manifestInput({
459
+ patternUses: [
460
+ {
461
+ ...manifestInput().patternUses[0],
462
+ status,
463
+ disposition: "reuse",
464
+ },
465
+ ],
466
+ })
467
+ )
468
+ ).toThrow(/may be reused/u);
469
+ }
470
+ );
471
+
472
+ it.each(["approved_guidance", "contract_canonical"] as const)(
473
+ "allows a %s pattern to be reused",
474
+ (status) => {
475
+ expect(
476
+ buildApprovedFeatureManifestV1(
477
+ manifestInput({
478
+ patternUses: [{ ...manifestInput().patternUses[0], status, disposition: "reuse" }],
479
+ })
480
+ ).patternUses[0]
481
+ ).toMatchObject({ status, disposition: "reuse" });
482
+ }
483
+ );
484
+
485
+ it("validates the bounded agent projection", () => {
486
+ const manifest = buildApprovedFeatureManifestV1(manifestInput());
487
+ const context = {
488
+ schemaVersion: 1 as const,
489
+ planId: manifest.planId,
490
+ manifestDigest: manifest.revisionDigest,
491
+ pins: manifest.pins,
492
+ validatedGrounding: manifest.validatedGrounding,
493
+ target: manifest.placement,
494
+ componentUses: manifest.componentUses,
495
+ tokenUses: manifest.tokenUses,
496
+ patternUses: manifest.patternUses,
497
+ layout: manifest.layout,
498
+ responsive: manifest.responsive,
499
+ scenarios: manifest.scenarios,
500
+ acceptedGaps: manifest.gaps,
501
+ acceptance: manifest.acceptance,
502
+ decisions: manifest.decisions,
503
+ relevantFiles: ["apps/cloud/src/settings.tsx"],
504
+ commands: ["pnpm test"],
505
+ };
506
+ expect(agentFeatureContextV1Schema.parse(context)).toEqual(context);
507
+ expect(() =>
508
+ agentFeatureContextV1Schema.parse({
509
+ ...context,
510
+ commands: ["x".repeat(70 * 1024)],
511
+ })
512
+ ).toThrow();
513
+ expect(() =>
514
+ agentFeatureContextV1Schema.parse({
515
+ ...context,
516
+ relevantFiles: ["/tmp/customer-source.tsx"],
517
+ })
518
+ ).toThrow();
519
+ });
520
+
521
+ it("keeps the stored manifest validator strict", () => {
522
+ expect(() =>
523
+ approvedFeatureManifestV1Schema.parse({
524
+ ...buildApprovedFeatureManifestV1(manifestInput()),
525
+ actorEmail: "not-in-authority-record@example.com",
526
+ })
527
+ ).toThrow();
528
+ });
529
+ });
@@ -0,0 +1,65 @@
1
+ export {
2
+ AGENT_FEATURE_CONTEXT_MAX_BYTES_V1,
3
+ FEATURE_PLAN_MAX_ACCEPTANCE_ITEMS_V1,
4
+ FEATURE_PLAN_MAX_DECISIONS_V1,
5
+ FEATURE_PLAN_MAX_EVIDENCE_REFS_V1,
6
+ FEATURE_PLAN_MAX_GAPS_V1,
7
+ FEATURE_PLAN_MAX_SCENARIOS_V1,
8
+ FEATURE_PLAN_MAX_TEXT_BYTES_V1,
9
+ FEATURE_PLAN_MAX_USES_PER_KIND_V1,
10
+ approvedFeatureManifestInputV1Schema,
11
+ approvedFeatureManifestV1Schema,
12
+ capabilityGapV1Schema,
13
+ componentUseDecisionV1Schema,
14
+ decisionRequestV1Schema,
15
+ decisionResolutionV1Schema,
16
+ evidenceRefV1Schema,
17
+ featureAcceptanceItemV1Schema,
18
+ featurePinsV1Schema,
19
+ featureProposalInputV1Schema,
20
+ featureProposalV1Schema,
21
+ gapDispositionV1Schema,
22
+ layoutConcernV1Schema,
23
+ layoutOwnershipV1Schema,
24
+ patternStatusV1Schema,
25
+ patternUseDecisionV1Schema,
26
+ placementDecisionV1Schema,
27
+ responsiveDecisionV1Schema,
28
+ scenarioRequirementV1Schema,
29
+ tokenUseDecisionV1Schema,
30
+ } from "./types.js";
31
+ export type {
32
+ AgentFeatureContextV1,
33
+ ApprovedFeatureManifestInputV1,
34
+ ApprovedFeatureManifestV1,
35
+ CapabilityGapV1,
36
+ ComponentUseDecisionV1,
37
+ DecisionRequestV1,
38
+ DecisionResolutionV1,
39
+ EvidenceRefV1,
40
+ FeatureAcceptanceItemV1,
41
+ FeaturePinsV1,
42
+ FeatureProposalInputV1,
43
+ FeatureProposalV1,
44
+ GapDispositionV1,
45
+ LayoutConcernV1,
46
+ LayoutOwnershipV1,
47
+ PatternStatusV1,
48
+ PatternUseDecisionV1,
49
+ PlacementDecisionV1,
50
+ ResponsiveDecisionV1,
51
+ ScenarioRequirementV1,
52
+ TokenUseDecisionV1,
53
+ } from "./types.js";
54
+ export {
55
+ agentFeatureContextV1Schema,
56
+ approvedFeatureManifestRevisionDigestV1,
57
+ buildApprovedFeatureManifestV1,
58
+ buildFeatureProposalV1,
59
+ featureProposalRevisionDigestV1,
60
+ normalizeApprovedFeatureManifestInputV1,
61
+ normalizeFeatureProposalInputV1,
62
+ parseAgentFeatureContextV1,
63
+ parseApprovedFeatureManifestV1,
64
+ parseFeatureProposalV1,
65
+ } from "./digest.js";