@usefragments/core 1.10.2 → 2.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 (130) 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-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
  6. package/dist/chunk-BMPYIUZE.js.map +1 -0
  7. package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
  8. package/dist/chunk-ML5S6QNU.js.map +1 -0
  9. package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
  10. package/dist/chunk-MZ2FS7U4.js.map +1 -0
  11. package/dist/chunk-PWIJMOI4.js +202 -0
  12. package/dist/chunk-PWIJMOI4.js.map +1 -0
  13. package/dist/chunk-RYFULE43.js +578 -0
  14. package/dist/chunk-RYFULE43.js.map +1 -0
  15. package/dist/codes/index.d.ts +2 -2
  16. package/dist/codes/index.js +3 -2
  17. package/dist/compiled-types/index.d.ts +327 -2
  18. package/dist/compiled-types/index.js +1 -1
  19. package/dist/generate/index.d.ts +3 -2
  20. package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
  21. package/dist/governance-telemetry.d.ts +6 -0
  22. package/dist/governance-telemetry.js +1 -1
  23. package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
  24. package/dist/index.d.ts +3280 -1305
  25. package/dist/index.js +2414 -350
  26. package/dist/index.js.map +1 -1
  27. package/dist/manifest.d.ts +228 -0
  28. package/dist/manifest.js +24 -0
  29. package/dist/manifest.js.map +1 -0
  30. package/dist/preview/index.js +45 -1
  31. package/dist/preview/index.js.map +1 -1
  32. package/dist/preview-runtime.d.ts +1 -2
  33. package/dist/preview-runtime.js +150 -14
  34. package/dist/preview-runtime.js.map +1 -1
  35. package/dist/react-types.d.ts +1 -2
  36. package/dist/registry.d.ts +1412 -203
  37. package/dist/registry.js +30 -3
  38. package/dist/schemas/index.d.ts +1 -1
  39. package/dist/schemas/index.js +3 -2
  40. package/dist/storyAdapter.d.ts +1 -2
  41. package/dist/storyAdapter.js +11 -49
  42. package/dist/storyAdapter.js.map +1 -1
  43. package/dist/test-utils.d.ts +3 -2
  44. package/dist/topology/index.d.ts +1 -1
  45. package/dist/topology/index.js +1 -1
  46. package/package.json +8 -2
  47. package/src/__tests__/contract-parser.test.ts +318 -277
  48. package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
  49. package/src/__tests__/preview-runtime.test.tsx +30 -8
  50. package/src/__tests__/schema.test.ts +191 -14
  51. package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
  52. package/src/analysis-plan/coverage.ts +181 -0
  53. package/src/analysis-plan/digest.ts +141 -0
  54. package/src/analysis-plan/index.ts +34 -0
  55. package/src/analysis-plan/types.ts +207 -0
  56. package/src/approved-contract-tokens.test.ts +39 -0
  57. package/src/approved-contract-tokens.ts +18 -0
  58. package/src/codes/__tests__/codes.test.ts +13 -0
  59. package/src/codes/codes.ts +40 -0
  60. package/src/compiled-types/index.ts +640 -39
  61. package/src/compiled-types/parse.test.ts +145 -4
  62. package/src/component-contract.ts +95 -53
  63. package/src/composition.ts +7 -13
  64. package/src/constants.ts +3 -6
  65. package/src/contract/hash.test.ts +20 -0
  66. package/src/contract/hash.ts +66 -9
  67. package/src/contract/index.ts +24 -1
  68. package/src/contract/manifest.test.ts +94 -0
  69. package/src/contract/manifest.ts +68 -0
  70. package/src/contract/preimage.test.ts +219 -1
  71. package/src/contract/preimage.ts +326 -6
  72. package/src/contract/stamp.test.ts +3 -0
  73. package/src/contract/stamp.ts +1 -1
  74. package/src/contract-parser.ts +54 -30
  75. package/src/defineFragment.test.ts +476 -91
  76. package/src/defineFragment.ts +204 -114
  77. package/src/domain-ids.test.ts +35 -0
  78. package/src/domain-ids.ts +61 -0
  79. package/src/evaluation/evaluate.test.ts +522 -0
  80. package/src/evaluation/evaluate.ts +690 -0
  81. package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
  82. package/src/evaluation/index.ts +58 -0
  83. package/src/evaluation/receipt.ts +753 -0
  84. package/src/evaluation/types.ts +406 -0
  85. package/src/facts/builders.ts +2 -0
  86. package/src/facts/compile.ts +29 -6
  87. package/src/facts/fact-index.ts +13 -3
  88. package/src/facts/fact-integrity-v1.test.ts +172 -0
  89. package/src/facts/facts.test.ts +15 -0
  90. package/src/facts/ids.ts +46 -3
  91. package/src/facts/index.ts +14 -1
  92. package/src/facts/integrity.ts +134 -0
  93. package/src/facts/types.ts +36 -0
  94. package/src/governance-integrity.test.ts +1 -0
  95. package/src/governance-integrity.ts +5 -3
  96. package/src/governance-telemetry.ts +8 -0
  97. package/src/governance.ts +70 -8
  98. package/src/index.ts +230 -37
  99. package/src/preview/validation.test.ts +62 -0
  100. package/src/preview/validation.ts +48 -2
  101. package/src/preview-runtime.tsx +227 -20
  102. package/src/registry-install-plan.ts +200 -109
  103. package/src/registry-shards.test.ts +263 -0
  104. package/src/registry.ts +237 -0
  105. package/src/repository-binding.test.ts +50 -0
  106. package/src/repository-binding.ts +96 -0
  107. package/src/rules/families.test.ts +36 -0
  108. package/src/rules/finding.ts +7 -2
  109. package/src/rules/index.ts +17 -1
  110. package/src/rules/rule-config.test.ts +66 -0
  111. package/src/rules/rule-config.ts +73 -0
  112. package/src/rules/rules.test.ts +26 -0
  113. package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
  114. package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
  115. package/src/schema.ts +293 -113
  116. package/src/schemas/index.ts +1 -1
  117. package/src/storyAdapter.test.ts +68 -12
  118. package/src/storyAdapter.ts +44 -75
  119. package/src/topology/resolve-area.ts +1 -1
  120. package/src/types.ts +258 -40
  121. package/dist/chunk-3LLRNCPX.js.map +0 -1
  122. package/dist/chunk-RANPUC6C.js +0 -72
  123. package/dist/chunk-RANPUC6C.js.map +0 -1
  124. package/dist/chunk-XN3LSDPY.js.map +0 -1
  125. package/dist/chunk-YF65VYRY.js.map +0 -1
  126. package/src/fragment-types.ts +0 -214
  127. package/src/react-create-element.test.ts +0 -22
  128. package/src/react-create-element.ts +0 -12
  129. package/src/storyFilters.test.ts +0 -350
  130. package/src/storyFilters.ts +0 -253
@@ -153,6 +153,21 @@ describe("fact IR — content-addressed IDs", () => {
153
153
  expect(a).toBe(b);
154
154
  });
155
155
 
156
+ it("rejects circular and runtime-only fact identity values deterministically", () => {
157
+ const circular: Record<string, unknown> = {};
158
+ circular.self = circular;
159
+
160
+ expect(() => factId("component", circular)).toThrow(
161
+ "Circular fact identity value at (root).i.self"
162
+ );
163
+ expect(() => factId("component", { componentId: () => "Button" })).toThrow(
164
+ "Unsupported fact identity value at (root).i.componentId: function"
165
+ );
166
+ expect(() => factId("component", { confidence: Number.NaN })).toThrow(
167
+ "Unsupported fact identity value at (root).i.confidence: non-finite number"
168
+ );
169
+ });
170
+
156
171
  it("keys definition and identity facts by the portable componentKey", () => {
157
172
  const first = makeComponentDefinitionFact({
158
173
  file: "src/a/Button.tsx",
package/src/facts/ids.ts CHANGED
@@ -22,15 +22,58 @@ import type { ComponentId, FactId } from "./types.js";
22
22
  // ---------------------------------------------------------------------------
23
23
 
24
24
  export function canonicalJson(value: unknown): string {
25
+ return canonicalJsonValue(value, new Set<object>(), "(root)");
26
+ }
27
+
28
+ function canonicalJsonValue(value: unknown, ancestors: Set<object>, path: string): string {
25
29
  if (value === undefined) return "null";
26
- if (value === null || typeof value !== "object") return JSON.stringify(value);
30
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
31
+ return JSON.stringify(value);
32
+ }
33
+ if (typeof value === "number") {
34
+ if (!Number.isFinite(value)) {
35
+ throw new TypeError(`Unsupported fact identity value at ${path}: non-finite number`);
36
+ }
37
+ const serialized = JSON.stringify(value);
38
+ if (serialized === undefined) {
39
+ throw new TypeError(`Unsupported fact identity value at ${path}: number`);
40
+ }
41
+ return serialized;
42
+ }
43
+ if (typeof value !== "object") {
44
+ throw new TypeError(`Unsupported fact identity value at ${path}: ${typeof value}`);
45
+ }
27
46
  if (Array.isArray(value)) {
28
- return `[${value.map(canonicalJson).join(",")}]`;
47
+ if (ancestors.has(value)) throw new TypeError(`Circular fact identity value at ${path}`);
48
+ ancestors.add(value);
49
+ try {
50
+ return `[${value
51
+ .map((entry, index) => canonicalJsonValue(entry, ancestors, `${path}[${index}]`))
52
+ .join(",")}]`;
53
+ } finally {
54
+ ancestors.delete(value);
55
+ }
29
56
  }
57
+ const prototype = Object.getPrototypeOf(value);
58
+ if (prototype !== Object.prototype && prototype !== null) {
59
+ const objectType = prototype?.constructor?.name ?? "unknown";
60
+ throw new TypeError(`Unsupported fact identity object at ${path}: ${objectType}`);
61
+ }
62
+ if (ancestors.has(value)) throw new TypeError(`Circular fact identity value at ${path}`);
63
+ ancestors.add(value);
30
64
  const entries = Object.entries(value as Record<string, unknown>)
31
65
  .filter(([, v]) => v !== undefined)
32
66
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
33
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
67
+ try {
68
+ return `{${entries
69
+ .map(
70
+ ([key, entry]) =>
71
+ `${JSON.stringify(key)}:${canonicalJsonValue(entry, ancestors, `${path}.${key}`)}`
72
+ )
73
+ .join(",")}}`;
74
+ } finally {
75
+ ancestors.delete(value);
76
+ }
34
77
  }
35
78
 
36
79
  // ---------------------------------------------------------------------------
@@ -72,8 +72,21 @@ export type {
72
72
  TailwindTokenResolvedFact,
73
73
  TailwindValue,
74
74
  ThemeDeclarationFact,
75
+ CanonicalFactConflictV1,
76
+ CanonicalFactConflictValueV1,
77
+ CanonicalFactIntegrityV1,
75
78
  } from "./types.js";
76
79
 
80
+ export {
81
+ CANONICAL_FACT_MAX_CONFLICTS_V1,
82
+ CANONICAL_FACT_MAX_PROVENANCE_PER_VALUE_V1,
83
+ CANONICAL_FACT_MAX_VALUES_PER_CONFLICT_V1,
84
+ canonicalFactConflictV1Schema,
85
+ canonicalFactConflictValueV1Schema,
86
+ canonicalFactIntegrityV1Schema,
87
+ normalizeCanonicalFactIntegrityV1,
88
+ } from "./integrity.js";
89
+
77
90
  export { componentId, asComponentId, factId, hash64Hex, canonicalJson } from "./ids.js";
78
91
 
79
92
  export {
@@ -122,7 +135,7 @@ export {
122
135
  } from "./builders.js";
123
136
 
124
137
  export { FactIndex, matchesGlob } from "./fact-index.js";
125
- export type { FactConflict, FactEvidence } from "./fact-index.js";
138
+ export type { FactConflict, FactDuplicate, FactEvidence } from "./fact-index.js";
126
139
 
127
140
  export { compileGlobalGovernanceFacts, compileComponentFacts } from "./compile.js";
128
141
  export { projectSupersededImportPathPreferences } from "./compile.js";
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ import { compareCanonicalStrings } from "../contract/hash.js";
3
+ import { digestHexStringSchema } from "../domain-ids.js";
4
+ import type { CanonicalFactIntegrityV1 } from "./types.js";
5
+
6
+ export const CANONICAL_FACT_MAX_CONFLICTS_V1 = 1_000;
7
+ export const CANONICAL_FACT_MAX_VALUES_PER_CONFLICT_V1 = 64;
8
+ export const CANONICAL_FACT_MAX_PROVENANCE_PER_VALUE_V1 = 1_000;
9
+
10
+ const boundedString = z.string().min(1).max(4_096);
11
+ const unique = (values: readonly string[]): boolean => new Set(values).size === values.length;
12
+ const provenance = z
13
+ .array(boundedString)
14
+ .min(1)
15
+ .max(CANONICAL_FACT_MAX_PROVENANCE_PER_VALUE_V1)
16
+ .refine(unique, "provenance values must be unique");
17
+
18
+ export const canonicalFactConflictValueV1Schema = z
19
+ .object({
20
+ valueDigest: digestHexStringSchema,
21
+ factIds: provenance,
22
+ totalFactIdCount: z.number().int().positive().optional(),
23
+ analyzerIds: provenance,
24
+ totalAnalyzerIdCount: z.number().int().positive().optional(),
25
+ evidenceRefs: provenance,
26
+ totalEvidenceRefCount: z.number().int().positive().optional(),
27
+ })
28
+ .strict()
29
+ .superRefine((value, context) => {
30
+ for (const [path, total, retained] of [
31
+ ["totalFactIdCount", value.totalFactIdCount, value.factIds.length],
32
+ ["totalAnalyzerIdCount", value.totalAnalyzerIdCount, value.analyzerIds.length],
33
+ ["totalEvidenceRefCount", value.totalEvidenceRefCount, value.evidenceRefs.length],
34
+ ] as const) {
35
+ if (total !== undefined && total < retained) {
36
+ context.addIssue({
37
+ code: z.ZodIssueCode.custom,
38
+ path: [path],
39
+ message: `${path} cannot be smaller than retained provenance`,
40
+ });
41
+ }
42
+ }
43
+ });
44
+
45
+ export const canonicalFactConflictV1Schema = z
46
+ .object({
47
+ canonicalKey: boundedString,
48
+ factKind: boundedString,
49
+ state: z.literal("conflict"),
50
+ values: z
51
+ .array(canonicalFactConflictValueV1Schema)
52
+ .min(2)
53
+ .max(CANONICAL_FACT_MAX_VALUES_PER_CONFLICT_V1)
54
+ .refine(
55
+ (values) => new Set(values.map((value) => value.valueDigest)).size === values.length,
56
+ "conflict values must have distinct material digests"
57
+ ),
58
+ totalValueCount: z.number().int().min(2).optional(),
59
+ })
60
+ .strict()
61
+ .superRefine((value, context) => {
62
+ if (value.totalValueCount !== undefined && value.totalValueCount < value.values.length) {
63
+ context.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ path: ["totalValueCount"],
66
+ message: "totalValueCount cannot be smaller than retained values",
67
+ });
68
+ }
69
+ });
70
+
71
+ export const canonicalFactIntegrityV1Schema = z.union([
72
+ z
73
+ .object({
74
+ state: z.literal("healthy"),
75
+ coalescedDuplicateCount: z.number().int().nonnegative(),
76
+ })
77
+ .strict(),
78
+ z
79
+ .object({
80
+ state: z.literal("conflict"),
81
+ coalescedDuplicateCount: z.number().int().nonnegative(),
82
+ conflicts: z
83
+ .array(canonicalFactConflictV1Schema)
84
+ .min(1)
85
+ .max(CANONICAL_FACT_MAX_CONFLICTS_V1)
86
+ .refine(
87
+ (conflicts) =>
88
+ new Set(conflicts.map((conflict) => conflict.canonicalKey)).size === conflicts.length,
89
+ "canonical conflict keys must be unique"
90
+ ),
91
+ totalConflictCount: z.number().int().positive().optional(),
92
+ })
93
+ .strict()
94
+ .superRefine((value, context) => {
95
+ if (
96
+ value.totalConflictCount !== undefined &&
97
+ value.totalConflictCount < value.conflicts.length
98
+ ) {
99
+ context.addIssue({
100
+ code: z.ZodIssueCode.custom,
101
+ path: ["totalConflictCount"],
102
+ message: "totalConflictCount cannot be smaller than retained conflicts",
103
+ });
104
+ }
105
+ }),
106
+ ]);
107
+
108
+ const sorted = (values: readonly string[]): string[] => [...values].sort();
109
+
110
+ export function normalizeCanonicalFactIntegrityV1(value: unknown): CanonicalFactIntegrityV1 {
111
+ const parsed = canonicalFactIntegrityV1Schema.parse(value);
112
+ if (parsed.state === "healthy") return parsed;
113
+ return {
114
+ ...parsed,
115
+ totalConflictCount: parsed.totalConflictCount ?? parsed.conflicts.length,
116
+ conflicts: parsed.conflicts
117
+ .map((conflict) => ({
118
+ ...conflict,
119
+ totalValueCount: conflict.totalValueCount ?? conflict.values.length,
120
+ values: conflict.values
121
+ .map((entry) => ({
122
+ ...entry,
123
+ factIds: sorted(entry.factIds),
124
+ totalFactIdCount: entry.totalFactIdCount ?? entry.factIds.length,
125
+ analyzerIds: sorted(entry.analyzerIds),
126
+ totalAnalyzerIdCount: entry.totalAnalyzerIdCount ?? entry.analyzerIds.length,
127
+ evidenceRefs: sorted(entry.evidenceRefs),
128
+ totalEvidenceRefCount: entry.totalEvidenceRefCount ?? entry.evidenceRefs.length,
129
+ }))
130
+ .sort((left, right) => compareCanonicalStrings(left.valueDigest, right.valueDigest)),
131
+ }))
132
+ .sort((left, right) => compareCanonicalStrings(left.canonicalKey, right.canonicalKey)),
133
+ };
134
+ }
@@ -160,6 +160,11 @@ export interface StyleFontSizeScaleFact extends BaseFact, PolicyFactExcludes {
160
160
  export interface StyleCssVarsMustBeDefinedFact extends BaseFact, PolicyFactExcludes {
161
161
  kind: "style_css_vars_must_be_defined";
162
162
  severity: GovernanceSeverity;
163
+ /**
164
+ * Repo-relative path of the resolved fragments config, when the scan knows
165
+ * it. Omitted from fact identity — same policy, different config filename.
166
+ */
167
+ configPath?: string;
163
168
  }
164
169
 
165
170
  /**
@@ -758,3 +763,34 @@ export type FactKind = Fact["kind"];
758
763
 
759
764
  /** Narrows a Fact union by kind. */
760
765
  export type FactOfKind<K extends FactKind> = Extract<Fact, { kind: K }>;
766
+
767
+ // ---------------------------------------------------------------------------
768
+ // V1 canonical integrity vocabulary (additive; current FactIndex is unchanged)
769
+ // ---------------------------------------------------------------------------
770
+
771
+ export interface CanonicalFactConflictValueV1 {
772
+ valueDigest: string;
773
+ factIds: string[];
774
+ totalFactIdCount?: number;
775
+ analyzerIds: string[];
776
+ totalAnalyzerIdCount?: number;
777
+ evidenceRefs: string[];
778
+ totalEvidenceRefCount?: number;
779
+ }
780
+
781
+ export interface CanonicalFactConflictV1 {
782
+ canonicalKey: string;
783
+ factKind: string;
784
+ state: "conflict";
785
+ values: CanonicalFactConflictValueV1[];
786
+ totalValueCount?: number;
787
+ }
788
+
789
+ export type CanonicalFactIntegrityV1 =
790
+ | { state: "healthy"; coalescedDuplicateCount: number }
791
+ | {
792
+ state: "conflict";
793
+ coalescedDuplicateCount: number;
794
+ conflicts: CanonicalFactConflictV1[];
795
+ totalConflictCount?: number;
796
+ };
@@ -614,6 +614,7 @@ describe("evaluateGovernanceIntegrity", () => {
614
614
  const tokens = verdict.families.find((f) => f.id === "tokens");
615
615
  expect(tokens?.armed).toBe(false);
616
616
  expect(tokens?.reason).toBe("no token vocabulary");
617
+ expect(verdict.roster).toMatchObject({ configured: 1, active: 0, inert: 1 });
617
618
  });
618
619
 
619
620
  it("7c. error-level token drift with a vocabulary is blocking-capable", () => {
@@ -227,7 +227,7 @@ const PASSTHROUGH_KEYS = [
227
227
  },
228
228
  {
229
229
  path: ["registry"],
230
- keys: ["requireStory", "publicOnly", "categoryDepth", "includeProps", "embedFragments"],
230
+ keys: ["requireStory", "publicOnly", "categoryDepth", "includeProps"],
231
231
  },
232
232
  {
233
233
  path: ["govern", "tailwind"],
@@ -953,10 +953,12 @@ export function evaluateGovernanceIntegrity(
953
953
  const activeRuleCount = [...configs.entries()].filter(
954
954
  ([ruleId, config]) => CONSUMED_RULE_IDS.has(ruleId) && config.enabled
955
955
  ).length;
956
+ const tokensVocabularyInert =
957
+ !tokensArmed && tokensFamily.reason === "no token vocabulary" ? 1 : 0;
956
958
  const roster = {
957
959
  configured: configuredRuleCount + configDiagnostics.length,
958
- active: activeRuleCount,
959
- inert: configDiagnostics.length,
960
+ active: Math.max(0, activeRuleCount - tokensVocabularyInert),
961
+ inert: configDiagnostics.length + tokensVocabularyInert,
960
962
  };
961
963
 
962
964
  return {
@@ -327,6 +327,14 @@ export const GOVERNANCE_TELEMETRY_FIELDS = [
327
327
  carriage: "derived",
328
328
  topLevelField: "adoption",
329
329
  },
330
+ {
331
+ path: "adoption.adopted[].{componentKey,displayName,canonicalTarget,usageCount}",
332
+ type: "bounded adopted component-identity summaries",
333
+ purpose:
334
+ "Explains which canonical components account for current adopted call sites without uploading source.",
335
+ carriage: "derived",
336
+ topLevelField: "adoption",
337
+ },
330
338
  {
331
339
  path: "adoption.shadows[].{componentKey,displayName,canonicalTarget,usageCount}",
332
340
  type: "bounded component-identity summaries",
package/src/governance.ts CHANGED
@@ -133,10 +133,15 @@ const componentCapabilityRecordSchema = z.object({
133
133
  capability: z.string().min(1),
134
134
  });
135
135
 
136
+ const requiredComponentGovernanceValueSchema = z.custom<unknown>(
137
+ (value) => value !== undefined,
138
+ "Expected an explicitly present governance value"
139
+ );
140
+
136
141
  const propValueAvoidRecordSchema = z.object({
137
142
  kind: z.literal("prop.value.avoid"),
138
143
  prop: z.string().min(1),
139
- value: z.unknown(),
144
+ value: requiredComponentGovernanceValueSchema,
140
145
  because: z.string(),
141
146
  suggest: z.string().optional(),
142
147
  severity: governanceSeveritySchema,
@@ -145,10 +150,10 @@ const propValueAvoidRecordSchema = z.object({
145
150
  const propValueForbidRecordSchema = z.object({
146
151
  kind: z.literal("prop.value.forbid"),
147
152
  prop: z.string().min(1),
148
- value: z.unknown(),
153
+ value: requiredComponentGovernanceValueSchema,
149
154
  when: z.object({ path: z.string().min(1).optional() }).optional(),
150
155
  because: z.string(),
151
- fix: z.object({ replaceWith: z.unknown() }).optional(),
156
+ fix: z.object({ replaceWith: requiredComponentGovernanceValueSchema }).optional(),
152
157
  severity: governanceSeveritySchema,
153
158
  });
154
159
 
@@ -167,6 +172,24 @@ export const componentGovernanceRecordSchema = z.discriminatedUnion("kind", [
167
172
 
168
173
  export const componentGovernanceRecordsSchema = z.array(componentGovernanceRecordSchema);
169
174
 
175
+ /** Parse portable component policy records; malformed authority is unavailable, never empty. */
176
+ export function parseComponentGovernancePolicyJson(
177
+ policyJson: string
178
+ ): ComponentGovernanceRecord[] | null {
179
+ try {
180
+ const parsed: unknown = JSON.parse(policyJson);
181
+ const record =
182
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
183
+ ? (parsed as Record<string, unknown>)
184
+ : null;
185
+ const candidate = Array.isArray(parsed) ? parsed : (record?.governance ?? record?.rules);
186
+ const result = componentGovernanceRecordsSchema.safeParse(candidate);
187
+ return result.success ? result.data : null;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
170
193
  const componentPolicyMatchSchema = z
171
194
  .object({
172
195
  componentKey: z.string().min(1).optional(),
@@ -587,10 +610,15 @@ export interface ComponentGovernanceBuilder<TProps> {
587
610
  accessibility(): AccessibilityGovernanceBuilder;
588
611
  }
589
612
 
590
- export interface GovernedFragmentDefinition<
613
+ /**
614
+ * One-argument governed definition. Requires an explicit `govern` function
615
+ * and/or `governance` records — both optional keys previously let plain v2
616
+ * definitions satisfy this type (the optional-key type lie).
617
+ */
618
+ type GovernedFragmentDefinitionBase<
591
619
  TProps = unknown,
592
620
  TComponent extends FragmentComponent<TProps> = FragmentComponent<TProps>,
593
- > {
621
+ > = {
594
622
  component: TComponent;
595
623
  meta: FragmentMeta;
596
624
  guidance: FragmentGuidance;
@@ -605,9 +633,22 @@ export interface GovernedFragmentDefinition<
605
633
  };
606
634
  contract?: FragmentContract;
607
635
  _provenance?: FragmentProvenance;
608
- govern?: (g: ComponentGovernanceBuilder<TProps>) => ComponentGovernanceRecord[];
609
- governance?: ComponentGovernanceRecord[];
610
- }
636
+ };
637
+
638
+ export type GovernedFragmentDefinition<
639
+ TProps = unknown,
640
+ TComponent extends FragmentComponent<TProps> = FragmentComponent<TProps>,
641
+ > = GovernedFragmentDefinitionBase<TProps, TComponent> &
642
+ (
643
+ | {
644
+ govern: (g: ComponentGovernanceBuilder<TProps>) => ComponentGovernanceRecord[];
645
+ governance?: ComponentGovernanceRecord[];
646
+ }
647
+ | {
648
+ governance: ComponentGovernanceRecord[];
649
+ govern?: (g: ComponentGovernanceBuilder<TProps>) => ComponentGovernanceRecord[];
650
+ }
651
+ );
611
652
 
612
653
  export type ResolvedGovernedFragmentDefinition<
613
654
  TProps = unknown,
@@ -668,6 +709,27 @@ export function resolveComponentGovernance<TProps>(
668
709
  return componentGovernanceRecordsSchema.parse(records);
669
710
  }
670
711
 
712
+ /**
713
+ * Resolve the component governance records that belong in the FCID preimage.
714
+ *
715
+ * Prefer an already-resolved `governance` array (Cloud mint / compiled payloads).
716
+ * Fall back to evaluating `govern` when present. Returns `undefined` when the
717
+ * fragment carries no governance surface — callers omit the field from identity.
718
+ */
719
+ export function resolveGovernanceRecordsForIdentity(
720
+ definition: Pick<GovernedFragmentDefinition, "govern" | "governance"> | null | undefined
721
+ ): ComponentGovernanceRecord[] | undefined {
722
+ if (!definition) return undefined;
723
+ if (Array.isArray(definition.governance)) {
724
+ return componentGovernanceRecordsSchema.parse(definition.governance);
725
+ }
726
+ if (typeof definition.govern === "function") {
727
+ const builder = createComponentGovernanceBuilder();
728
+ return componentGovernanceRecordsSchema.parse(definition.govern(builder));
729
+ }
730
+ return undefined;
731
+ }
732
+
671
733
  export function normalizeGovernanceConfig<TConfig extends { govern?: GovernanceConfig }>(
672
734
  config: TConfig
673
735
  ): TConfig {