@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.
- 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-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-RPSEABY3.js} +622 -344
- package/dist/chunk-RPSEABY3.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-BAsy1k2H.d.ts} +472 -513
- 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 +9165 -1297
- package/dist/index.js +3041 -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/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/codes/__tests__/codes.test.ts +9 -0
- package/src/codes/codes.ts +30 -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 +52 -0
- package/src/domain-ids.ts +106 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +680 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +760 -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/feature-plan/digest.ts +217 -0
- package/src/feature-plan/feature-plan-v1.test.ts +529 -0
- package/src/feature-plan/index.ts +65 -0
- package/src/feature-plan/types.ts +628 -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 +52 -8
- package/src/index.ts +299 -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/types.ts +258 -37
- 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
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { canonicalPreimage, compareCanonicalStrings, sha256Hex } from "../contract/hash.js";
|
|
2
|
+
import { featureRevisionIdFromDigest, type DigestHex } from "../domain-ids.js";
|
|
3
|
+
import {
|
|
4
|
+
AGENT_FEATURE_CONTEXT_MAX_BYTES_V1,
|
|
5
|
+
agentFeatureContextV1Schema as structuralAgentFeatureContextV1Schema,
|
|
6
|
+
approvedFeatureManifestInputV1Schema,
|
|
7
|
+
approvedFeatureManifestV1Schema,
|
|
8
|
+
featureProposalInputV1Schema,
|
|
9
|
+
featureProposalV1Schema,
|
|
10
|
+
type AgentFeatureContextV1,
|
|
11
|
+
type ApprovedFeatureManifestInputV1,
|
|
12
|
+
type ApprovedFeatureManifestV1,
|
|
13
|
+
type FeatureProposalInputV1,
|
|
14
|
+
type FeatureProposalV1,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
const sortedUnique = (values: readonly string[]): string[] => [...new Set(values)].sort();
|
|
18
|
+
|
|
19
|
+
function normalizeEvidenceRefs<T extends { evidenceRefIds: string[] }>(row: T): T {
|
|
20
|
+
return { ...row, evidenceRefIds: sortedUnique(row.evidenceRefIds) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeCommon<
|
|
24
|
+
T extends {
|
|
25
|
+
placement: FeatureProposalInputV1["placement"];
|
|
26
|
+
componentUses: FeatureProposalInputV1["componentUses"];
|
|
27
|
+
tokenUses: FeatureProposalInputV1["tokenUses"];
|
|
28
|
+
patternUses: FeatureProposalInputV1["patternUses"];
|
|
29
|
+
layout: FeatureProposalInputV1["layout"];
|
|
30
|
+
responsive: FeatureProposalInputV1["responsive"];
|
|
31
|
+
scenarios: FeatureProposalInputV1["scenarios"];
|
|
32
|
+
gaps: FeatureProposalInputV1["gaps"];
|
|
33
|
+
acceptance: FeatureProposalInputV1["acceptance"];
|
|
34
|
+
evidenceRefs: FeatureProposalInputV1["evidenceRefs"];
|
|
35
|
+
},
|
|
36
|
+
>(value: T): T {
|
|
37
|
+
return {
|
|
38
|
+
...value,
|
|
39
|
+
placement: normalizeEvidenceRefs(value.placement),
|
|
40
|
+
componentUses: value.componentUses
|
|
41
|
+
.map(normalizeEvidenceRefs)
|
|
42
|
+
.sort((left, right) => compareCanonicalStrings(left.useId, right.useId)),
|
|
43
|
+
tokenUses: value.tokenUses
|
|
44
|
+
.map(normalizeEvidenceRefs)
|
|
45
|
+
.sort((left, right) => compareCanonicalStrings(left.useId, right.useId)),
|
|
46
|
+
patternUses: value.patternUses
|
|
47
|
+
.map(normalizeEvidenceRefs)
|
|
48
|
+
.sort((left, right) => compareCanonicalStrings(left.useId, right.useId)),
|
|
49
|
+
layout: {
|
|
50
|
+
concerns: value.layout.concerns
|
|
51
|
+
.map(normalizeEvidenceRefs)
|
|
52
|
+
.sort((left, right) => compareCanonicalStrings(left.concern, right.concern)),
|
|
53
|
+
},
|
|
54
|
+
responsive: value.responsive
|
|
55
|
+
.map(normalizeEvidenceRefs)
|
|
56
|
+
.sort((left, right) => compareCanonicalStrings(left.decisionId, right.decisionId)),
|
|
57
|
+
scenarios: value.scenarios
|
|
58
|
+
.map((scenario) => ({ ...scenario, acceptanceIds: sortedUnique(scenario.acceptanceIds) }))
|
|
59
|
+
.sort((left, right) => compareCanonicalStrings(left.scenarioId, right.scenarioId)),
|
|
60
|
+
gaps: value.gaps
|
|
61
|
+
.map(normalizeEvidenceRefs)
|
|
62
|
+
.sort((left, right) => compareCanonicalStrings(left.gapId, right.gapId)),
|
|
63
|
+
acceptance: value.acceptance
|
|
64
|
+
.map(normalizeEvidenceRefs)
|
|
65
|
+
.sort((left, right) => compareCanonicalStrings(left.acceptanceId, right.acceptanceId)),
|
|
66
|
+
evidenceRefs: [...value.evidenceRefs].sort((left, right) =>
|
|
67
|
+
compareCanonicalStrings(left.evidenceId, right.evidenceId)
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function normalizeFeatureProposalInputV1(
|
|
73
|
+
value: FeatureProposalInputV1
|
|
74
|
+
): FeatureProposalInputV1 {
|
|
75
|
+
const parsed = featureProposalInputV1Schema.parse(value);
|
|
76
|
+
return normalizeCommon({
|
|
77
|
+
...parsed,
|
|
78
|
+
questions: parsed.questions
|
|
79
|
+
.map((question) => ({ ...normalizeEvidenceRefs(question), options: [...question.options] }))
|
|
80
|
+
.sort((left, right) => compareCanonicalStrings(left.decisionId, right.decisionId)),
|
|
81
|
+
}) as FeatureProposalInputV1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function proposalProjection(proposal: FeatureProposalInputV1): Record<string, unknown> {
|
|
85
|
+
return {
|
|
86
|
+
schema: "feature-revision.v1",
|
|
87
|
+
kind: proposal.kind,
|
|
88
|
+
planId: proposal.planId,
|
|
89
|
+
parentRevisionDigest: proposal.parentRevisionDigest,
|
|
90
|
+
pins: proposal.pins,
|
|
91
|
+
placement: proposal.placement,
|
|
92
|
+
componentUses: proposal.componentUses,
|
|
93
|
+
tokenUses: proposal.tokenUses,
|
|
94
|
+
patternUses: proposal.patternUses,
|
|
95
|
+
layout: proposal.layout,
|
|
96
|
+
responsive: proposal.responsive,
|
|
97
|
+
scenarios: proposal.scenarios,
|
|
98
|
+
gaps: proposal.gaps,
|
|
99
|
+
acceptance: proposal.acceptance,
|
|
100
|
+
questions: proposal.questions,
|
|
101
|
+
evidenceRefs: proposal.evidenceRefs,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function featureProposalRevisionDigestV1(value: FeatureProposalInputV1): DigestHex {
|
|
106
|
+
const proposal = normalizeFeatureProposalInputV1(value);
|
|
107
|
+
return sha256Hex(canonicalPreimage(proposalProjection(proposal)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildFeatureProposalV1(value: FeatureProposalInputV1): FeatureProposalV1 {
|
|
111
|
+
const proposal = normalizeFeatureProposalInputV1(value);
|
|
112
|
+
const revisionDigest = sha256Hex(canonicalPreimage(proposalProjection(proposal)));
|
|
113
|
+
return featureProposalV1Schema.parse({
|
|
114
|
+
...proposal,
|
|
115
|
+
revisionId: featureRevisionIdFromDigest(revisionDigest),
|
|
116
|
+
revisionDigest,
|
|
117
|
+
}) as FeatureProposalV1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseFeatureProposalV1(value: unknown): FeatureProposalV1 {
|
|
121
|
+
const parsed = featureProposalV1Schema.parse(value);
|
|
122
|
+
const { revisionId, revisionDigest, ...input } = parsed;
|
|
123
|
+
const rebuilt = buildFeatureProposalV1(input as FeatureProposalInputV1);
|
|
124
|
+
if (revisionId !== rebuilt.revisionId || revisionDigest !== rebuilt.revisionDigest) {
|
|
125
|
+
throw new TypeError("Feature proposal derived identity does not match its semantic projection");
|
|
126
|
+
}
|
|
127
|
+
return rebuilt;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeApprovedFeatureManifestInputV1(
|
|
131
|
+
value: ApprovedFeatureManifestInputV1
|
|
132
|
+
): ApprovedFeatureManifestInputV1 {
|
|
133
|
+
const parsed = approvedFeatureManifestInputV1Schema.parse(value);
|
|
134
|
+
return normalizeCommon({
|
|
135
|
+
...parsed,
|
|
136
|
+
decisions: [...parsed.decisions].sort((left, right) =>
|
|
137
|
+
compareCanonicalStrings(left.decisionId, right.decisionId)
|
|
138
|
+
),
|
|
139
|
+
approval: { ...parsed.approval },
|
|
140
|
+
validatedGrounding: { ...parsed.validatedGrounding },
|
|
141
|
+
}) as ApprovedFeatureManifestInputV1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function manifestProjection(manifest: ApprovedFeatureManifestInputV1): Record<string, unknown> {
|
|
145
|
+
return {
|
|
146
|
+
schema: "feature-revision.v1",
|
|
147
|
+
kind: manifest.kind,
|
|
148
|
+
planId: manifest.planId,
|
|
149
|
+
parentRevisionDigest: manifest.parentRevisionDigest,
|
|
150
|
+
proposalRevisionDigest: manifest.proposalRevisionDigest,
|
|
151
|
+
pins: manifest.pins,
|
|
152
|
+
validatedGrounding: manifest.validatedGrounding,
|
|
153
|
+
placement: manifest.placement,
|
|
154
|
+
componentUses: manifest.componentUses,
|
|
155
|
+
tokenUses: manifest.tokenUses,
|
|
156
|
+
patternUses: manifest.patternUses,
|
|
157
|
+
layout: manifest.layout,
|
|
158
|
+
responsive: manifest.responsive,
|
|
159
|
+
scenarios: manifest.scenarios,
|
|
160
|
+
gaps: manifest.gaps,
|
|
161
|
+
acceptance: manifest.acceptance,
|
|
162
|
+
evidenceRefs: manifest.evidenceRefs,
|
|
163
|
+
decisions: manifest.decisions,
|
|
164
|
+
approval: {
|
|
165
|
+
approverUserId: manifest.approval.approverUserId,
|
|
166
|
+
decisionDigest: manifest.approval.decisionDigest,
|
|
167
|
+
expiresAt: manifest.approval.expiresAt,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function approvedFeatureManifestRevisionDigestV1(
|
|
173
|
+
value: ApprovedFeatureManifestInputV1
|
|
174
|
+
): DigestHex {
|
|
175
|
+
const manifest = normalizeApprovedFeatureManifestInputV1(value);
|
|
176
|
+
return sha256Hex(canonicalPreimage(manifestProjection(manifest)));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildApprovedFeatureManifestV1(
|
|
180
|
+
value: ApprovedFeatureManifestInputV1
|
|
181
|
+
): ApprovedFeatureManifestV1 {
|
|
182
|
+
const manifest = normalizeApprovedFeatureManifestInputV1(value);
|
|
183
|
+
const revisionDigest = sha256Hex(canonicalPreimage(manifestProjection(manifest)));
|
|
184
|
+
return approvedFeatureManifestV1Schema.parse({
|
|
185
|
+
...manifest,
|
|
186
|
+
revisionId: featureRevisionIdFromDigest(revisionDigest),
|
|
187
|
+
revisionDigest,
|
|
188
|
+
}) as ApprovedFeatureManifestV1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function parseApprovedFeatureManifestV1(value: unknown): ApprovedFeatureManifestV1 {
|
|
192
|
+
const parsed = approvedFeatureManifestV1Schema.parse(value);
|
|
193
|
+
const { revisionId, revisionDigest, ...input } = parsed;
|
|
194
|
+
const rebuilt = buildApprovedFeatureManifestV1(input as ApprovedFeatureManifestInputV1);
|
|
195
|
+
if (revisionId !== rebuilt.revisionId || revisionDigest !== rebuilt.revisionDigest) {
|
|
196
|
+
throw new TypeError(
|
|
197
|
+
"Approved manifest derived identity does not match its semantic projection"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return rebuilt;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const agentFeatureContextV1Schema = structuralAgentFeatureContextV1Schema.superRefine(
|
|
204
|
+
(value, context) => {
|
|
205
|
+
const bytes = new TextEncoder().encode(canonicalPreimage(value)).byteLength;
|
|
206
|
+
if (bytes > AGENT_FEATURE_CONTEXT_MAX_BYTES_V1) {
|
|
207
|
+
context.addIssue({
|
|
208
|
+
code: "custom",
|
|
209
|
+
message: `Agent feature context exceeds ${AGENT_FEATURE_CONTEXT_MAX_BYTES_V1} UTF-8 bytes`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
export function parseAgentFeatureContextV1(value: unknown): AgentFeatureContextV1 {
|
|
216
|
+
return agentFeatureContextV1Schema.parse(value) as AgentFeatureContextV1;
|
|
217
|
+
}
|