@usefragments/core 1.11.0 → 2.0.1
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-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/{chunk-RPSEABY3.js → chunk-QPOKQ5H6.js} +30 -34
- package/dist/chunk-QPOKQ5H6.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-BAsy1k2H.d.ts → governance-hOPXGbbs.d.ts} +3 -3
- package/dist/index.d.ts +145 -6026
- package/dist/index.js +298 -860
- package/dist/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/storyAdapter.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +1 -1
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/canonical-source.ts +41 -0
- package/src/codes/__tests__/codes.test.ts +4 -0
- package/src/codes/codes.ts +10 -0
- package/src/domain-ids.test.ts +9 -26
- package/src/domain-ids.ts +0 -45
- package/src/evaluation/evaluate.ts +17 -7
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +20 -8
- package/src/evaluation/index.ts +1 -1
- package/src/facts/builders.ts +4 -0
- package/src/facts/fact-index.ts +4 -0
- package/src/facts/facts.test.ts +70 -0
- package/src/facts/types.ts +4 -0
- package/src/governance.ts +18 -0
- package/src/identity/usage.test.ts +64 -0
- package/src/identity/usage.ts +7 -1
- package/src/index.ts +15 -79
- package/src/rules/components-prefer-library.ts +110 -33
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +0 -3
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RPSEABY3.js.map +0 -1
- package/src/feature-plan/digest.ts +0 -217
- package/src/feature-plan/feature-plan-v1.test.ts +0 -529
- package/src/feature-plan/index.ts +0 -65
- package/src/feature-plan/types.ts +0 -628
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { V as VariantRenderOptions, b as VariantLoader } from './governance-
|
|
3
|
+
import { V as VariantRenderOptions, b as VariantLoader } from './governance-hOPXGbbs.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
import './topology/index.js';
|
|
6
6
|
|
package/dist/react-types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentType, ReactNode, JSX } from 'react';
|
|
2
|
-
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-
|
|
2
|
+
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-hOPXGbbs.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
import './topology/index.js';
|
|
5
5
|
|
package/dist/schemas/index.js
CHANGED
package/dist/storyAdapter.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReactNode, ComponentType } from 'react';
|
|
2
2
|
import { isExportStory as isExportStory$1, storyNameFromExport as storyNameFromExport$1, toId as toId$1 } from '@storybook/csf';
|
|
3
|
-
import { F as FragmentDefinition } from './governance-
|
|
3
|
+
import { F as FragmentDefinition } from './governance-hOPXGbbs.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
import './topology/index.js';
|
|
6
6
|
|
package/dist/test-utils.d.ts
CHANGED
package/dist/topology/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* preview, cross-repo reconciliation) import it; none reimplements glob/area
|
|
7
7
|
* logic. Pure and browser-safe — no Node, no parser deps.
|
|
8
8
|
*
|
|
9
|
-
* See `
|
|
9
|
+
* See `docs/fragments-v1/ARCHITECTURE.md §1`.
|
|
10
10
|
*/
|
|
11
11
|
type AreaCriticality = "low" | "medium" | "high" | "revenue" | "regulated";
|
|
12
12
|
interface Area {
|
package/dist/topology/index.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
APPROVED_CONTRACT_TOKEN_MAX_NAMES,
|
|
4
|
+
APPROVED_CONTRACT_TOKEN_MAX_NAME_LENGTH,
|
|
5
|
+
isApprovedContractTokenNames,
|
|
6
|
+
} from "./approved-contract-tokens.js";
|
|
7
|
+
import { parseComponentGovernancePolicyJson } from "./governance.js";
|
|
8
|
+
|
|
9
|
+
describe("approved token transport", () => {
|
|
10
|
+
it("distinguishes absent authority from an explicitly empty vocabulary", () => {
|
|
11
|
+
expect(isApprovedContractTokenNames(undefined)).toBe(false);
|
|
12
|
+
expect(isApprovedContractTokenNames(null)).toBe(false);
|
|
13
|
+
expect(isApprovedContractTokenNames([])).toBe(true);
|
|
14
|
+
expect(isApprovedContractTokenNames(["--approved"])).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it("enforces the same bounded vocabulary on every consumer", () => {
|
|
17
|
+
expect(isApprovedContractTokenNames(Array(APPROVED_CONTRACT_TOKEN_MAX_NAMES).fill("--x"))).toBe(
|
|
18
|
+
true
|
|
19
|
+
);
|
|
20
|
+
expect(
|
|
21
|
+
isApprovedContractTokenNames(Array(APPROVED_CONTRACT_TOKEN_MAX_NAMES + 1).fill("--x"))
|
|
22
|
+
).toBe(false);
|
|
23
|
+
expect(
|
|
24
|
+
isApprovedContractTokenNames(["--" + "x".repeat(APPROVED_CONTRACT_TOKEN_MAX_NAME_LENGTH - 2)])
|
|
25
|
+
).toBe(true);
|
|
26
|
+
expect(
|
|
27
|
+
isApprovedContractTokenNames(["--" + "x".repeat(APPROVED_CONTRACT_TOKEN_MAX_NAME_LENGTH - 1)])
|
|
28
|
+
).toBe(false);
|
|
29
|
+
expect(isApprovedContractTokenNames(["--"])).toBe(false);
|
|
30
|
+
expect(isApprovedContractTokenNames(["local-token"])).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("normalizes supported policy envelopes without treating malformed authority as empty rules", () => {
|
|
35
|
+
for (const payload of ["[]", '{"governance":[]}', '{"rules":[]}'])
|
|
36
|
+
expect(parseComponentGovernancePolicyJson(payload)).toEqual([]);
|
|
37
|
+
for (const payload of ["null", "{}", "[{}]", "invalid", '{"governance":false}'])
|
|
38
|
+
expect(parseComponentGovernancePolicyJson(payload)).toBeNull();
|
|
39
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Portable bounds shared by contract authoring and first-party policy clients. */
|
|
2
|
+
export const APPROVED_CONTRACT_TOKEN_MAX_NAMES = 10_000;
|
|
3
|
+
export const APPROVED_CONTRACT_TOKEN_MAX_NAME_LENGTH = 512;
|
|
4
|
+
|
|
5
|
+
/** Empty is authored absence; missing is unavailable authority. */
|
|
6
|
+
export function isApprovedContractTokenNames(value: unknown): value is string[] {
|
|
7
|
+
return (
|
|
8
|
+
Array.isArray(value) &&
|
|
9
|
+
value.length <= APPROVED_CONTRACT_TOKEN_MAX_NAMES &&
|
|
10
|
+
value.every(
|
|
11
|
+
(name) =>
|
|
12
|
+
typeof name === "string" &&
|
|
13
|
+
name.startsWith("--") &&
|
|
14
|
+
name.length > 2 &&
|
|
15
|
+
name.length <= APPROVED_CONTRACT_TOKEN_MAX_NAME_LENGTH
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CanonicalSource } from "./governance.js";
|
|
2
|
+
|
|
3
|
+
/** Export filters narrow source ownership; an empty include approves nothing. */
|
|
4
|
+
export function canonicalSourceIncludesExport(source: CanonicalSource, name: string): boolean {
|
|
5
|
+
const role = name.split(".").at(-1) ?? name;
|
|
6
|
+
const matches = (names: readonly string[]) => names.includes(name) || names.includes(role);
|
|
7
|
+
return (
|
|
8
|
+
!matches(source.exclude ?? []) && (source.include === undefined || matches(source.include))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Filesystem ownership only. Package import specifiers are not local paths. */
|
|
13
|
+
export function canonicalSourceContainsFile(source: CanonicalSource, file: string): boolean {
|
|
14
|
+
const root =
|
|
15
|
+
source.kind === "directory"
|
|
16
|
+
? source.path
|
|
17
|
+
: source.kind === "registry"
|
|
18
|
+
? source.installPath
|
|
19
|
+
: source.implementationPath;
|
|
20
|
+
if (!root) return false;
|
|
21
|
+
const normalize = (value: string) =>
|
|
22
|
+
value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
23
|
+
const normalizedRoot = normalize(root);
|
|
24
|
+
const normalizedFile = normalize(file);
|
|
25
|
+
return (
|
|
26
|
+
normalizedRoot === "." ||
|
|
27
|
+
normalizedFile === normalizedRoot ||
|
|
28
|
+
normalizedFile.startsWith(`${normalizedRoot}/`)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isCanonicalSourceDefinition(
|
|
33
|
+
file: string,
|
|
34
|
+
exportName: string,
|
|
35
|
+
sources: readonly CanonicalSource[]
|
|
36
|
+
): boolean {
|
|
37
|
+
return sources.some(
|
|
38
|
+
(source) =>
|
|
39
|
+
canonicalSourceContainsFile(source, file) && canonicalSourceIncludesExport(source, exportName)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -29,6 +29,10 @@ const SUBSYSTEM_EMITTED_CODES = [
|
|
|
29
29
|
// extracted component, is refused before any rule has a contract to run on.
|
|
30
30
|
"FUI9010",
|
|
31
31
|
"FUI9011",
|
|
32
|
+
// Emitted by Cloud's machine contract write path (CLI route + MCP tool) when
|
|
33
|
+
// the workspace plan does not include contract writes from machines; the CLI
|
|
34
|
+
// relays it. Not a rule.
|
|
35
|
+
"FUI9012",
|
|
32
36
|
// Emitted by tokens/css-vars-must-be-defined when the rule is active but the
|
|
33
37
|
// resolved contract vocabulary is empty — a config-level inert diagnostic,
|
|
34
38
|
// not a second rule id in RULES.
|
package/src/codes/codes.ts
CHANGED
|
@@ -557,6 +557,16 @@ export const CODES = [
|
|
|
557
557
|
fixAvailable: false,
|
|
558
558
|
evidenceRequired: false,
|
|
559
559
|
}),
|
|
560
|
+
code({
|
|
561
|
+
code: "FUI9012",
|
|
562
|
+
ruleId: "contract/write-not-entitled",
|
|
563
|
+
category: "system",
|
|
564
|
+
defaultSeverity: "critical",
|
|
565
|
+
title: "Contract write requires a paid workspace",
|
|
566
|
+
lifecycle: "experimental",
|
|
567
|
+
fixAvailable: false,
|
|
568
|
+
evidenceRequired: false,
|
|
569
|
+
}),
|
|
560
570
|
] as const satisfies readonly FuiCode[];
|
|
561
571
|
|
|
562
572
|
export const byCode: ReadonlyMap<string, FuiCode> = new Map(
|
package/src/domain-ids.test.ts
CHANGED
|
@@ -3,12 +3,8 @@ import {
|
|
|
3
3
|
analysisPlanIdFromDigest,
|
|
4
4
|
evaluationReceiptIdFromDigest,
|
|
5
5
|
mintEvaluationReceiptId,
|
|
6
|
-
featurePlanIdFromEntropy,
|
|
7
|
-
featureRevisionIdFromDigest,
|
|
8
6
|
parseAnalysisPlanId,
|
|
9
7
|
parseEvaluationReceiptId,
|
|
10
|
-
parseFeaturePlanId,
|
|
11
|
-
parseFeatureRevisionId,
|
|
12
8
|
} from "./domain-ids.js";
|
|
13
9
|
|
|
14
10
|
const DIGEST = "0123456789abcdef".repeat(4);
|
|
@@ -18,35 +14,22 @@ describe("V1 domain IDs", () => {
|
|
|
18
14
|
expect(analysisPlanIdFromDigest(DIGEST)).toBe(`apl_${DIGEST}`);
|
|
19
15
|
expect(evaluationReceiptIdFromDigest(DIGEST)).toBe(`evr_${DIGEST}`);
|
|
20
16
|
expect(mintEvaluationReceiptId(DIGEST)).toBe(`evr_${DIGEST}`);
|
|
21
|
-
expect(featureRevisionIdFromDigest(DIGEST)).toBe(`fpr_${DIGEST}`);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("keeps feature workflow identity opaque and exactly 128-bit", () => {
|
|
25
|
-
const entropy = "00112233445566778899aabbccddeeff";
|
|
26
|
-
expect(featurePlanIdFromEntropy(entropy)).toBe(`fpl_${entropy}`);
|
|
27
17
|
});
|
|
28
18
|
|
|
29
19
|
it.each([
|
|
30
20
|
[parseAnalysisPlanId, `apl_${DIGEST}`],
|
|
31
21
|
[parseEvaluationReceiptId, `evr_${DIGEST}`],
|
|
32
|
-
[parseFeatureRevisionId, `fpr_${DIGEST}`],
|
|
33
|
-
[parseFeaturePlanId, "fpl_00112233445566778899aabbccddeeff"],
|
|
34
22
|
] as const)("round-trips a valid ID", (parse, value) => {
|
|
35
23
|
expect(parse(value)).toBe(value);
|
|
36
24
|
});
|
|
37
25
|
|
|
38
|
-
it.each([
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
else if (value.startsWith("evr_")) parseEvaluationReceiptId(value);
|
|
48
|
-
else if (value.startsWith("fpr_")) parseFeatureRevisionId(value);
|
|
49
|
-
else parseFeaturePlanId(value);
|
|
50
|
-
}).toThrow();
|
|
51
|
-
});
|
|
26
|
+
it.each([`apl_${DIGEST.slice(0, -1)}`, `apl_${DIGEST.toUpperCase()}`, `evr_${DIGEST}extra`])(
|
|
27
|
+
"rejects truncated, uppercase, or extended IDs: %s",
|
|
28
|
+
(value) => {
|
|
29
|
+
expect(() => {
|
|
30
|
+
if (value.startsWith("apl_")) parseAnalysisPlanId(value);
|
|
31
|
+
else if (value.startsWith("evr_")) parseEvaluationReceiptId(value);
|
|
32
|
+
}).toThrow();
|
|
33
|
+
}
|
|
34
|
+
);
|
|
52
35
|
});
|
package/src/domain-ids.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
const FULL_DIGEST_PATTERN = /^[0-9a-f]{64}$/u;
|
|
4
|
-
const WORKFLOW_ENTROPY_PATTERN = /^[0-9a-f]{32}$/u;
|
|
5
4
|
|
|
6
5
|
export declare const analysisPlanIdBrand: unique symbol;
|
|
7
6
|
export declare const evaluationReceiptIdBrand: unique symbol;
|
|
8
|
-
export declare const featurePlanIdBrand: unique symbol;
|
|
9
|
-
export declare const featureRevisionIdBrand: unique symbol;
|
|
10
7
|
|
|
11
8
|
export type DigestHex = string;
|
|
12
9
|
export type AnalysisPlanId = `apl_${string}` & { readonly [analysisPlanIdBrand]: void };
|
|
13
10
|
export type EvaluationReceiptId = `evr_${string}` & {
|
|
14
11
|
readonly [evaluationReceiptIdBrand]: void;
|
|
15
12
|
};
|
|
16
|
-
export type FeaturePlanId = `fpl_${string}` & { readonly [featurePlanIdBrand]: void };
|
|
17
|
-
export type FeatureRevisionId = `fpr_${string}` & {
|
|
18
|
-
readonly [featureRevisionIdBrand]: void;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
13
|
export const digestHexStringSchema = z
|
|
22
14
|
.string()
|
|
23
15
|
.regex(FULL_DIGEST_PATTERN, "Expected 64 lowercase hexadecimal characters");
|
|
@@ -38,20 +30,6 @@ export const evaluationReceiptIdStringSchema = z
|
|
|
38
30
|
.string()
|
|
39
31
|
.regex(/^evr_[0-9a-f]{64}$/u, "Invalid evaluation receipt ID");
|
|
40
32
|
|
|
41
|
-
export const featurePlanIdStringSchema = z
|
|
42
|
-
.string()
|
|
43
|
-
.regex(/^fpl_[0-9a-f]{32}$/u, "Invalid feature plan ID");
|
|
44
|
-
export const featurePlanIdSchema = featurePlanIdStringSchema.transform(
|
|
45
|
-
(value) => value as FeaturePlanId
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
export const featureRevisionIdStringSchema = z
|
|
49
|
-
.string()
|
|
50
|
-
.regex(/^fpr_[0-9a-f]{64}$/u, "Invalid feature revision ID");
|
|
51
|
-
export const featureRevisionIdSchema = featureRevisionIdStringSchema.transform(
|
|
52
|
-
(value) => value as FeatureRevisionId
|
|
53
|
-
);
|
|
54
|
-
|
|
55
33
|
export function parseDigestHex(value: string): DigestHex {
|
|
56
34
|
return digestHexSchema.parse(value);
|
|
57
35
|
}
|
|
@@ -74,21 +52,6 @@ export function mintEvaluationReceiptId(resultDigest: string): EvaluationReceipt
|
|
|
74
52
|
return evaluationReceiptIdFromDigest(resultDigest);
|
|
75
53
|
}
|
|
76
54
|
|
|
77
|
-
export function featureRevisionIdFromDigest(digest: string): FeatureRevisionId {
|
|
78
|
-
return featureRevisionIdSchema.parse(`fpr_${parseDigestHex(digest)}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Construct a stable feature-workflow ID from 128 bits supplied by the caller.
|
|
83
|
-
* Core deliberately owns neither a clock nor a random-number generator.
|
|
84
|
-
*/
|
|
85
|
-
export function featurePlanIdFromEntropy(entropyHex: string): FeaturePlanId {
|
|
86
|
-
if (!WORKFLOW_ENTROPY_PATTERN.test(entropyHex)) {
|
|
87
|
-
throw new TypeError("Feature plan entropy must be exactly 128-bit lowercase hexadecimal");
|
|
88
|
-
}
|
|
89
|
-
return featurePlanIdSchema.parse(`fpl_${entropyHex}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
55
|
export function parseAnalysisPlanId(value: string): AnalysisPlanId {
|
|
93
56
|
return analysisPlanIdSchema.parse(value);
|
|
94
57
|
}
|
|
@@ -96,11 +59,3 @@ export function parseAnalysisPlanId(value: string): AnalysisPlanId {
|
|
|
96
59
|
export function parseEvaluationReceiptId(value: string): EvaluationReceiptId {
|
|
97
60
|
return evaluationReceiptIdSchema.parse(value);
|
|
98
61
|
}
|
|
99
|
-
|
|
100
|
-
export function parseFeaturePlanId(value: string): FeaturePlanId {
|
|
101
|
-
return featurePlanIdSchema.parse(value);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function parseFeatureRevisionId(value: string): FeatureRevisionId {
|
|
105
|
-
return featureRevisionIdSchema.parse(value);
|
|
106
|
-
}
|
|
@@ -303,7 +303,7 @@ function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
|
|
|
303
303
|
: coverageSummary.requiredByState.analyzed === coverageSummary.requiredRegions
|
|
304
304
|
? "complete"
|
|
305
305
|
: "incomplete";
|
|
306
|
-
const integrity = {
|
|
306
|
+
const integrity: EvaluationResultV2["integrity"] = {
|
|
307
307
|
fcidVerified: compatibilityResult.integrity.fcidVerified === true,
|
|
308
308
|
analysisPlanVerified: true,
|
|
309
309
|
factIntegrity: input.integrity.facts.state,
|
|
@@ -324,9 +324,7 @@ function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
|
|
|
324
324
|
verdict = "indeterminate";
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
const inputDigest =
|
|
328
|
-
canonicalPreimage(normalizeInputV2ForDigest(input, coverageSummary))
|
|
329
|
-
);
|
|
327
|
+
const inputDigest = evaluationInputDigestV2(input);
|
|
330
328
|
const partial = {
|
|
331
329
|
schemaVersion: 2 as const,
|
|
332
330
|
verdict,
|
|
@@ -340,12 +338,24 @@ function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
|
|
|
340
338
|
coverageSummary,
|
|
341
339
|
integrity,
|
|
342
340
|
};
|
|
343
|
-
const resultDigest =
|
|
344
|
-
canonicalPreimage({ schema: "evaluation-result:v2", ...partial, inputDigest })
|
|
345
|
-
);
|
|
341
|
+
const resultDigest = evaluationResultDigestV2({ ...partial, inputDigest });
|
|
346
342
|
return evaluationResultV2Schema.parse({ ...partial, inputDigest, resultDigest });
|
|
347
343
|
}
|
|
348
344
|
|
|
345
|
+
/** Hash captured input identity without re-running the rule engine. */
|
|
346
|
+
export function evaluationInputDigestV2(input: EvaluationInputV2): string {
|
|
347
|
+
return sha256Hex(
|
|
348
|
+
canonicalPreimage(
|
|
349
|
+
normalizeInputV2ForDigest(input, deriveCoverageSummaryV1(input.analysisPlan, input.coverage))
|
|
350
|
+
)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Validate stored result identity using the same algorithm as evaluation. */
|
|
355
|
+
export function evaluationResultDigestV2(result: Omit<EvaluationResultV2, "resultDigest">): string {
|
|
356
|
+
return sha256Hex(canonicalPreimage({ schema: "evaluation-result:v2", ...result }));
|
|
357
|
+
}
|
|
358
|
+
|
|
349
359
|
function normalizeInputV2ForDigest(
|
|
350
360
|
input: EvaluationInputV2,
|
|
351
361
|
coverageSummary: ReturnType<typeof deriveCoverageSummaryV1>
|
|
@@ -9,6 +9,8 @@ import { repositoryBindingDigestV1 } from "../repository-binding.js";
|
|
|
9
9
|
import {
|
|
10
10
|
EVALUATOR_VERSION_V2,
|
|
11
11
|
evaluate,
|
|
12
|
+
evaluationInputDigestV2,
|
|
13
|
+
evaluationResultDigestV2,
|
|
12
14
|
evaluationInputV2Schema,
|
|
13
15
|
evaluationReceiptDigestV1,
|
|
14
16
|
evaluationReceiptMintInputV1Schema,
|
|
@@ -550,6 +552,18 @@ describe("EvaluationResultV2", () => {
|
|
|
550
552
|
});
|
|
551
553
|
|
|
552
554
|
describe("evaluate V2", () => {
|
|
555
|
+
it("validates stored semantic digests without a second evaluation", () => {
|
|
556
|
+
const input = evaluationInput();
|
|
557
|
+
const result = evaluate(input);
|
|
558
|
+
const { resultDigest, ...semantic } = result;
|
|
559
|
+
expect(evaluationInputDigestV2(input)).toBe(result.inputDigest);
|
|
560
|
+
expect(evaluationResultDigestV2(semantic)).toBe(resultDigest);
|
|
561
|
+
expect(evaluationResultDigestV2({ ...semantic, verdict: "block" })).not.toBe(resultDigest);
|
|
562
|
+
expect(evaluationInputDigestV2({ ...input, evaluatedAt: input.evaluatedAt + 1 })).not.toBe(
|
|
563
|
+
result.inputDigest
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
553
567
|
it("produces the checked-in deterministic pass result for complete healthy evidence", () => {
|
|
554
568
|
const result = evaluate(evaluationInput());
|
|
555
569
|
|
|
@@ -741,14 +755,12 @@ describe("EvaluationReceiptV1", () => {
|
|
|
741
755
|
resultDigest: A,
|
|
742
756
|
evaluatorVersion: "evaluation-kernel:v2",
|
|
743
757
|
} satisfies EvaluationReceiptMintInputV1;
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
void sourceVerifiedMintInput;
|
|
751
|
-
}
|
|
758
|
+
// @ts-expect-error Source-verified trust is derived with provider proof by the application.
|
|
759
|
+
const sourceVerifiedMintInput: EvaluationReceiptMintInputV1 = {
|
|
760
|
+
...external,
|
|
761
|
+
trust: "source_verified",
|
|
762
|
+
};
|
|
763
|
+
void sourceVerifiedMintInput;
|
|
752
764
|
expect(evaluationReceiptMintInputV1Schema.parse(external)).toEqual(external);
|
|
753
765
|
expect(() =>
|
|
754
766
|
evaluationReceiptMintInputV1Schema.parse({ ...external, authority: "enforcement_eligible" })
|
package/src/evaluation/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Adapters: `packages/cli/src/govern/evaluation.ts` and
|
|
4
4
|
* `apps/cloud/src/lib/govern/evaluation-adapter.ts`.
|
|
5
5
|
*/
|
|
6
|
-
export { evaluate } from "./evaluate.js";
|
|
6
|
+
export { evaluate, evaluationInputDigestV2, evaluationResultDigestV2 } from "./evaluate.js";
|
|
7
7
|
export { EVALUATOR_VERSION, EVALUATOR_VERSION_V2 } from "./types.js";
|
|
8
8
|
export {
|
|
9
9
|
EVALUATION_MAX_FINDINGS_V2,
|
package/src/facts/builders.ts
CHANGED
|
@@ -122,6 +122,7 @@ export function makeComponentDefinitionFact(input: {
|
|
|
122
122
|
componentKey: string;
|
|
123
123
|
renderRoot: ComponentDefinitionFact["renderRoot"];
|
|
124
124
|
propSurface: string[];
|
|
125
|
+
location?: FactLocation;
|
|
125
126
|
stylingChannel?: ComponentDefinitionFact["stylingChannel"];
|
|
126
127
|
}): ComponentDefinitionFact {
|
|
127
128
|
return {
|
|
@@ -134,6 +135,7 @@ export function makeComponentDefinitionFact(input: {
|
|
|
134
135
|
componentKey: input.componentKey,
|
|
135
136
|
renderRoot: input.renderRoot,
|
|
136
137
|
propSurface: [...input.propSurface],
|
|
138
|
+
...(input.location ? { location: { ...input.location } } : {}),
|
|
137
139
|
...(input.stylingChannel ? { stylingChannel: { ...input.stylingChannel } } : {}),
|
|
138
140
|
};
|
|
139
141
|
}
|
|
@@ -664,6 +666,7 @@ export function makeUsageNodeFact(input: {
|
|
|
664
666
|
export function makeUsageComponentFact(input: {
|
|
665
667
|
nodeId: FactId;
|
|
666
668
|
componentId: ComponentId;
|
|
669
|
+
definitionKey?: string;
|
|
667
670
|
}): UsageComponentFact {
|
|
668
671
|
return {
|
|
669
672
|
id: factId("usage_component", {
|
|
@@ -673,6 +676,7 @@ export function makeUsageComponentFact(input: {
|
|
|
673
676
|
kind: "usage_component",
|
|
674
677
|
nodeId: input.nodeId,
|
|
675
678
|
componentId: input.componentId,
|
|
679
|
+
...(input.definitionKey ? { definitionKey: input.definitionKey } : {}),
|
|
676
680
|
};
|
|
677
681
|
}
|
|
678
682
|
|
package/src/facts/fact-index.ts
CHANGED
|
@@ -87,6 +87,10 @@ function logicalFactForComparison(fact: Fact): Record<string, unknown> {
|
|
|
87
87
|
const { location: _location, sourceNames: _sourceNames, ...logicalFact } = fact;
|
|
88
88
|
return logicalFact;
|
|
89
89
|
}
|
|
90
|
+
if (fact.kind === "component_definition") {
|
|
91
|
+
const { location: _location, ...logicalFact } = fact;
|
|
92
|
+
return { ...logicalFact, componentId: logicalComponentId(fact.componentId) };
|
|
93
|
+
}
|
|
90
94
|
if (fact.kind === "jsx_import_path_preferred") {
|
|
91
95
|
return {
|
|
92
96
|
...fact,
|
package/src/facts/facts.test.ts
CHANGED
|
@@ -549,6 +549,76 @@ describe("FactIndex — query layer", () => {
|
|
|
549
549
|
expect(ix.get(first.id)).toEqual(first);
|
|
550
550
|
});
|
|
551
551
|
|
|
552
|
+
it.each(["missing-first", "missing-second", "moved"] as const)(
|
|
553
|
+
"coalesces component definitions when only their evidence region differs (%s)",
|
|
554
|
+
(mode) => {
|
|
555
|
+
const onConflict = vi.fn();
|
|
556
|
+
const onDuplicate = vi.fn();
|
|
557
|
+
const ix = new FactIndex({ onConflict, onDuplicate });
|
|
558
|
+
const definition = {
|
|
559
|
+
file: "src/Button.tsx",
|
|
560
|
+
exportName: "Button",
|
|
561
|
+
exported: true,
|
|
562
|
+
componentKey: "src/Button.tsx#Button",
|
|
563
|
+
renderRoot: { resolution: "intrinsic", tag: "button" } as const,
|
|
564
|
+
propSurface: ["disabled"],
|
|
565
|
+
};
|
|
566
|
+
const first = makeComponentDefinitionFact({
|
|
567
|
+
...definition,
|
|
568
|
+
...(mode !== "missing-first"
|
|
569
|
+
? { location: { file: definition.file, line: 2, column: 0, endLine: 4, endColumn: 1 } }
|
|
570
|
+
: {}),
|
|
571
|
+
});
|
|
572
|
+
const second = makeComponentDefinitionFact({
|
|
573
|
+
...definition,
|
|
574
|
+
...(mode !== "missing-second"
|
|
575
|
+
? { location: { file: definition.file, line: 6, column: 0, endLine: 8, endColumn: 1 } }
|
|
576
|
+
: {}),
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
ix.addMany([first, second]);
|
|
580
|
+
|
|
581
|
+
expect(onConflict).not.toHaveBeenCalled();
|
|
582
|
+
expect(onDuplicate).toHaveBeenCalledTimes(1);
|
|
583
|
+
expect(onDuplicate).toHaveBeenCalledWith({ kept: first, duplicate: second });
|
|
584
|
+
expect(ix.byKind("component_definition")).toEqual([first]);
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
it.each([
|
|
589
|
+
{ renderRoot: { resolution: "intrinsic", tag: "a" } as const },
|
|
590
|
+
{ propSurface: ["disabled", "href"] },
|
|
591
|
+
{ exported: false },
|
|
592
|
+
])("retains component-definition conflicts for material changes: %j", (change) => {
|
|
593
|
+
const onConflict = vi.fn();
|
|
594
|
+
const onDuplicate = vi.fn();
|
|
595
|
+
const ix = new FactIndex({ onConflict, onDuplicate });
|
|
596
|
+
const definition = {
|
|
597
|
+
file: "src/Button.tsx",
|
|
598
|
+
exportName: "Button",
|
|
599
|
+
exported: true,
|
|
600
|
+
componentKey: "src/Button.tsx#Button",
|
|
601
|
+
renderRoot: { resolution: "intrinsic", tag: "button" } as const,
|
|
602
|
+
propSurface: ["disabled"],
|
|
603
|
+
};
|
|
604
|
+
const first = makeComponentDefinitionFact(definition);
|
|
605
|
+
const second = makeComponentDefinitionFact({
|
|
606
|
+
...definition,
|
|
607
|
+
...change,
|
|
608
|
+
location: { file: definition.file, line: 2, column: 0, endLine: 4, endColumn: 1 },
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
ix.addMany([first, second]);
|
|
612
|
+
|
|
613
|
+
expect(onConflict).toHaveBeenCalledTimes(1);
|
|
614
|
+
expect(onConflict).toHaveBeenCalledWith(expect.stringContaining("conflicting facts"), {
|
|
615
|
+
kept: first,
|
|
616
|
+
skipped: second,
|
|
617
|
+
});
|
|
618
|
+
expect(onDuplicate).not.toHaveBeenCalled();
|
|
619
|
+
expect(ix.byKind("component_definition")).toEqual([first]);
|
|
620
|
+
});
|
|
621
|
+
|
|
552
622
|
it("includes token-definition provenance in logical conflict reports", () => {
|
|
553
623
|
const onConflict = vi.fn();
|
|
554
624
|
const ix = new FactIndex({ onConflict });
|
package/src/facts/types.ts
CHANGED
|
@@ -317,6 +317,8 @@ export interface UsageComponentFact extends BaseFact {
|
|
|
317
317
|
kind: "usage_component";
|
|
318
318
|
nodeId: FactId;
|
|
319
319
|
componentId: ComponentId;
|
|
320
|
+
/** Exact local definition resolved from source, separate from catalog/policy identity. */
|
|
321
|
+
definitionKey?: string;
|
|
320
322
|
}
|
|
321
323
|
|
|
322
324
|
export interface UsageImportFact extends BaseFact {
|
|
@@ -688,6 +690,8 @@ export interface ComponentDefinitionFact extends BaseFact {
|
|
|
688
690
|
componentKey: string;
|
|
689
691
|
renderRoot: ComponentDefinitionRenderRoot;
|
|
690
692
|
propSurface: string[];
|
|
693
|
+
/** Implementation region, used to scope exemptions; never part of fact identity. */
|
|
694
|
+
location?: FactLocation;
|
|
691
695
|
stylingChannel?: {
|
|
692
696
|
module: string;
|
|
693
697
|
ownsVisualRole: boolean;
|
package/src/governance.ts
CHANGED
|
@@ -172,6 +172,24 @@ export const componentGovernanceRecordSchema = z.discriminatedUnion("kind", [
|
|
|
172
172
|
|
|
173
173
|
export const componentGovernanceRecordsSchema = z.array(componentGovernanceRecordSchema);
|
|
174
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
|
+
|
|
175
193
|
const componentPolicyMatchSchema = z
|
|
176
194
|
.object({
|
|
177
195
|
componentKey: z.string().min(1).optional(),
|