@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.
- package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
- package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
- package/dist/chunk-7ULGH74M.js +66 -0
- package/dist/chunk-7ULGH74M.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
- package/dist/chunk-BMPYIUZE.js.map +1 -0
- package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/chunk-RYFULE43.js +578 -0
- package/dist/chunk-RYFULE43.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +3 -2
- package/dist/compiled-types/index.d.ts +327 -2
- package/dist/compiled-types/index.js +1 -1
- package/dist/generate/index.d.ts +3 -2
- package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
- package/dist/governance-telemetry.d.ts +6 -0
- package/dist/governance-telemetry.js +1 -1
- package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
- package/dist/index.d.ts +3280 -1305
- package/dist/index.js +2414 -350
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +228 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/preview/index.js +45 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -2
- package/dist/preview-runtime.js +150 -14
- package/dist/preview-runtime.js.map +1 -1
- package/dist/react-types.d.ts +1 -2
- package/dist/registry.d.ts +1412 -203
- package/dist/registry.js +30 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +3 -2
- package/dist/storyAdapter.d.ts +1 -2
- package/dist/storyAdapter.js +11 -49
- package/dist/storyAdapter.js.map +1 -1
- package/dist/test-utils.d.ts +3 -2
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +8 -2
- package/src/__tests__/contract-parser.test.ts +318 -277
- package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
- package/src/__tests__/preview-runtime.test.tsx +30 -8
- package/src/__tests__/schema.test.ts +191 -14
- package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
- package/src/analysis-plan/coverage.ts +181 -0
- package/src/analysis-plan/digest.ts +141 -0
- package/src/analysis-plan/index.ts +34 -0
- package/src/analysis-plan/types.ts +207 -0
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/codes/__tests__/codes.test.ts +13 -0
- package/src/codes/codes.ts +40 -0
- package/src/compiled-types/index.ts +640 -39
- package/src/compiled-types/parse.test.ts +145 -4
- package/src/component-contract.ts +95 -53
- package/src/composition.ts +7 -13
- package/src/constants.ts +3 -6
- package/src/contract/hash.test.ts +20 -0
- package/src/contract/hash.ts +66 -9
- package/src/contract/index.ts +24 -1
- package/src/contract/manifest.test.ts +94 -0
- package/src/contract/manifest.ts +68 -0
- package/src/contract/preimage.test.ts +219 -1
- package/src/contract/preimage.ts +326 -6
- package/src/contract/stamp.test.ts +3 -0
- package/src/contract/stamp.ts +1 -1
- package/src/contract-parser.ts +54 -30
- package/src/defineFragment.test.ts +476 -91
- package/src/defineFragment.ts +204 -114
- package/src/domain-ids.test.ts +35 -0
- package/src/domain-ids.ts +61 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +690 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
- package/src/evaluation/index.ts +58 -0
- package/src/evaluation/receipt.ts +753 -0
- package/src/evaluation/types.ts +406 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/compile.ts +29 -6
- package/src/facts/fact-index.ts +13 -3
- package/src/facts/fact-integrity-v1.test.ts +172 -0
- package/src/facts/facts.test.ts +15 -0
- package/src/facts/ids.ts +46 -3
- package/src/facts/index.ts +14 -1
- package/src/facts/integrity.ts +134 -0
- package/src/facts/types.ts +36 -0
- package/src/governance-integrity.test.ts +1 -0
- package/src/governance-integrity.ts +5 -3
- package/src/governance-telemetry.ts +8 -0
- package/src/governance.ts +70 -8
- package/src/index.ts +230 -37
- package/src/preview/validation.test.ts +62 -0
- package/src/preview/validation.ts +48 -2
- package/src/preview-runtime.tsx +227 -20
- package/src/registry-install-plan.ts +200 -109
- package/src/registry-shards.test.ts +263 -0
- package/src/registry.ts +237 -0
- package/src/repository-binding.test.ts +50 -0
- package/src/repository-binding.ts +96 -0
- package/src/rules/families.test.ts +36 -0
- package/src/rules/finding.ts +7 -2
- package/src/rules/index.ts +17 -1
- package/src/rules/rule-config.test.ts +66 -0
- package/src/rules/rule-config.ts +73 -0
- package/src/rules/rules.test.ts +26 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
- package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
- package/src/schema.ts +293 -113
- package/src/schemas/index.ts +1 -1
- package/src/storyAdapter.test.ts +68 -12
- package/src/storyAdapter.ts +44 -75
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +258 -40
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RANPUC6C.js +0 -72
- package/dist/chunk-RANPUC6C.js.map +0 -1
- package/dist/chunk-XN3LSDPY.js.map +0 -1
- package/dist/chunk-YF65VYRY.js.map +0 -1
- package/src/fragment-types.ts +0 -214
- package/src/react-create-element.test.ts +0 -22
- package/src/react-create-element.ts +0 -12
- package/src/storyFilters.test.ts +0 -350
- package/src/storyFilters.ts +0 -253
package/src/facts/facts.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/src/facts/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/facts/types.ts
CHANGED
|
@@ -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"
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
609
|
-
|
|
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 {
|