@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.
Files changed (47) hide show
  1. package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
  2. package/dist/chunk-MZ2FS7U4.js.map +1 -0
  3. package/dist/{chunk-RPSEABY3.js → chunk-QPOKQ5H6.js} +30 -34
  4. package/dist/chunk-QPOKQ5H6.js.map +1 -0
  5. package/dist/codes/index.d.ts +1 -1
  6. package/dist/codes/index.js +1 -1
  7. package/dist/compiled-types/index.d.ts +1 -1
  8. package/dist/generate/index.d.ts +1 -1
  9. package/dist/{governance-BAsy1k2H.d.ts → governance-hOPXGbbs.d.ts} +3 -3
  10. package/dist/index.d.ts +145 -6026
  11. package/dist/index.js +298 -860
  12. package/dist/index.js.map +1 -1
  13. package/dist/preview-runtime.d.ts +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/schemas/index.js +1 -1
  16. package/dist/storyAdapter.d.ts +1 -1
  17. package/dist/test-utils.d.ts +1 -1
  18. package/dist/topology/index.d.ts +1 -1
  19. package/dist/topology/index.js +1 -1
  20. package/package.json +1 -1
  21. package/src/approved-contract-tokens.test.ts +39 -0
  22. package/src/approved-contract-tokens.ts +18 -0
  23. package/src/canonical-source.ts +41 -0
  24. package/src/codes/__tests__/codes.test.ts +4 -0
  25. package/src/codes/codes.ts +10 -0
  26. package/src/domain-ids.test.ts +9 -26
  27. package/src/domain-ids.ts +0 -45
  28. package/src/evaluation/evaluate.ts +17 -7
  29. package/src/evaluation/evaluation-v2-receipt-v1.test.ts +20 -8
  30. package/src/evaluation/index.ts +1 -1
  31. package/src/facts/builders.ts +4 -0
  32. package/src/facts/fact-index.ts +4 -0
  33. package/src/facts/facts.test.ts +70 -0
  34. package/src/facts/types.ts +4 -0
  35. package/src/governance.ts +18 -0
  36. package/src/identity/usage.test.ts +64 -0
  37. package/src/identity/usage.ts +7 -1
  38. package/src/index.ts +15 -79
  39. package/src/rules/components-prefer-library.ts +110 -33
  40. package/src/topology/resolve-area.ts +1 -1
  41. package/src/types.ts +0 -3
  42. package/dist/chunk-3LLRNCPX.js.map +0 -1
  43. package/dist/chunk-RPSEABY3.js.map +0 -1
  44. package/src/feature-plan/digest.ts +0 -217
  45. package/src/feature-plan/feature-plan-v1.test.ts +0 -529
  46. package/src/feature-plan/index.ts +0 -65
  47. 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-BAsy1k2H.js';
3
+ import { V as VariantRenderOptions, b as VariantLoader } from './governance-hOPXGbbs.js';
4
4
  import 'zod';
5
5
  import './topology/index.js';
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { ComponentType, ReactNode, JSX } from 'react';
2
- export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-BAsy1k2H.js';
2
+ export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-hOPXGbbs.js';
3
3
  import 'zod';
4
4
  import './topology/index.js';
5
5
 
@@ -24,7 +24,7 @@ import {
24
24
  suppressionDirectiveSchema,
25
25
  validatorResultSchema,
26
26
  violationSchema
27
- } from "../chunk-RPSEABY3.js";
27
+ } from "../chunk-QPOKQ5H6.js";
28
28
  import "../chunk-EIYNNS77.js";
29
29
  import "../chunk-PWIJMOI4.js";
30
30
  import "../chunk-JNBFJ34I.js";
@@ -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-BAsy1k2H.js';
3
+ import { F as FragmentDefinition } from './governance-hOPXGbbs.js';
4
4
  import 'zod';
5
5
  import './topology/index.js';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { CompiledBlock, CompiledFragment } from './compiled-types/index.js';
2
2
  import './types-xJ2xyp_G.js';
3
- import './governance-BAsy1k2H.js';
3
+ import './governance-hOPXGbbs.js';
4
4
  import 'zod';
5
5
  import 'react';
6
6
  import './topology/index.js';
@@ -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 `apps/cloud/docs/topology/01-architecture.md §1`.
9
+ * See `docs/fragments-v1/ARCHITECTURE.md §1`.
10
10
  */
11
11
  type AreaCriticality = "low" | "medium" | "high" | "revenue" | "regulated";
12
12
  interface Area {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveArea
3
- } from "../chunk-3LLRNCPX.js";
3
+ } from "../chunk-MZ2FS7U4.js";
4
4
  export {
5
5
  resolveArea
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefragments/core",
3
- "version": "1.11.0",
3
+ "version": "2.0.1",
4
4
  "license": "MIT",
5
5
  "description": "Core types, schemas, and runtime API for Fragments component definitions",
6
6
  "author": "Conan McNicholl",
@@ -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.
@@ -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(
@@ -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
- `apl_${DIGEST.slice(0, -1)}`,
40
- `apl_${DIGEST.toUpperCase()}`,
41
- `evr_${DIGEST}extra`,
42
- `fpr_${DIGEST.slice(0, 32)}`,
43
- "fpl_00112233445566778899AABBCCDDEEFF",
44
- ])("rejects truncated, uppercase, or extended IDs: %s", (value) => {
45
- expect(() => {
46
- if (value.startsWith("apl_")) parseAnalysisPlanId(value);
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 = sha256Hex(
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 = sha256Hex(
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
- if (false) {
745
- // @ts-expect-error Source-verified trust is derived with provider proof by the application.
746
- const sourceVerifiedMintInput: EvaluationReceiptMintInputV1 = {
747
- ...external,
748
- trust: "source_verified",
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" })
@@ -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,
@@ -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
 
@@ -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,
@@ -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 });
@@ -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(),