@usefragments/core 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/{chunk-R3X7UBHU.js → chunk-57QDBEHQ.js} +101 -6
  2. package/dist/chunk-57QDBEHQ.js.map +1 -0
  3. package/dist/{chunk-243QYRUF.js → chunk-MLRDNSSA.js} +88 -47
  4. package/dist/chunk-MLRDNSSA.js.map +1 -0
  5. package/dist/codes/index.d.ts +2 -2
  6. package/dist/codes/index.js +2 -2
  7. package/dist/compiled-types/index.d.ts +1 -1
  8. package/dist/generate/index.d.ts +1 -1
  9. package/dist/{governance-BOa3KyiJ.d.ts → governance-DYSirwJu.d.ts} +22 -1
  10. package/dist/index-h_yWj15D.d.ts +6981 -0
  11. package/dist/index.d.ts +113 -97
  12. package/dist/index.js +387 -113
  13. package/dist/index.js.map +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/registry.d.ts +36 -36
  16. package/dist/schemas/index.d.ts +3 -2584
  17. package/dist/schemas/index.js +9 -1
  18. package/dist/test-utils.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/agent-format.test.ts +144 -2
  21. package/src/agent-format.ts +34 -7
  22. package/src/codes/__tests__/codes.test.ts +57 -7
  23. package/src/codes/codes.ts +44 -42
  24. package/src/conform-prove.ts +4 -8
  25. package/src/contract/index.ts +3 -0
  26. package/src/contract/preimage.test.ts +50 -0
  27. package/src/contract/preimage.ts +56 -0
  28. package/src/evidence.ts +27 -0
  29. package/src/facts/builders.ts +4 -0
  30. package/src/facts/fact-index.ts +25 -1
  31. package/src/facts/facts.test.ts +49 -0
  32. package/src/facts/types.ts +3 -0
  33. package/src/governance-integrity.test.ts +2 -0
  34. package/src/governance-integrity.ts +7 -1
  35. package/src/index.ts +13 -0
  36. package/src/rules/__tests__/fix-emission-invariant.test.ts +174 -0
  37. package/src/rules/__tests__/tokens-require-dual-fallback.test.ts +90 -0
  38. package/src/rules/components-prefer-library.ts +165 -7
  39. package/src/rules/emit-gate.test.ts +40 -2
  40. package/src/rules/emit-gate.ts +21 -10
  41. package/src/rules/finding.ts +3 -0
  42. package/src/rules/fix-availability.ts +33 -0
  43. package/src/rules/rules.test.ts +180 -24
  44. package/src/rules/spacing-resolution.ts +5 -8
  45. package/src/rules/styles-no-raw-color.ts +32 -23
  46. package/src/rules/styles-no-raw-dimensions.ts +31 -12
  47. package/src/rules/styles-no-raw-spacing.ts +33 -11
  48. package/src/rules/styles-no-raw-typography.ts +37 -15
  49. package/src/rules/taxonomy.test.ts +19 -3
  50. package/src/rules/tiers.ts +1 -1
  51. package/src/rules/tokens-require-dual-fallback.ts +54 -17
  52. package/src/rules/utils.ts +51 -0
  53. package/src/schemas/index.ts +106 -4
  54. package/src/token-types.ts +6 -0
  55. package/src/tokens/__tests__/source-names.test.ts +42 -0
  56. package/src/tokens/design-token-parser.test.ts +24 -5
  57. package/src/tokens/design-token-parser.ts +40 -8
  58. package/dist/chunk-243QYRUF.js.map +0 -1
  59. package/dist/chunk-R3X7UBHU.js.map +0 -1
@@ -15,8 +15,7 @@
15
15
  *
16
16
  * Determinism (architecture §6.3): the sampler receives `(seq, codeHashAtRequest)`
17
17
  * so a durable-replay layer can key recorded sampler output on the exact code it
18
- * was asked to repair, and hard-invalidate on a hash mismatch — instead of the
19
- * pass-index-only key the standalone MCP tool uses.
18
+ * was asked to repair and hard-invalidate on a hash mismatch.
20
19
  *
21
20
  * Browser-safe: depends only on the conform contract types + the pure SHA-256
22
21
  * in this package. No Node APIs.
@@ -248,14 +247,11 @@ export async function proveCompliant(
248
247
  // and we are under the attempt cap (kept strictly below maxPasses so a
249
248
  // revalidation pass always remains in budget).
250
249
  //
251
- // Sampling semantics (intentional, and DIFFERENT from the standalone MCP
252
- // tool): the loop re-samples only while the sampler keeps producing *new*
253
- // code (each fresh snippet is re-validated on the next pass); a sample that
250
+ // The loop re-samples only while the sampler keeps producing *new* code
251
+ // (each fresh snippet is re-validated on the next pass). A sample that
254
252
  // returns nothing new — null, no `code`, or byte-identical code — ends the
255
253
  // loop. Re-asking the model with the identical residual rarely helps and
256
- // burns tokens. The MCP tool instead retries up to its cap on each stall
257
- // (its suspend/resume transport makes per-stall retry the natural shape);
258
- // both are bounded by maxPasses. See docs/contract-mode/01-architecture.md.
254
+ // burns tokens.
259
255
  const residual: ProveResidual = {
260
256
  suggestions: result.suggestions,
261
257
  unresolved: result.unresolved,
@@ -8,11 +8,13 @@
8
8
  export { canonicalPreimage, contractHash, sha256Hex } from "./hash.js";
9
9
  export {
10
10
  CONTRACT_DOMAINS,
11
+ CONTRACT_PREIMAGE_CAPABILITY_HEADER,
11
12
  CONTRACT_PREIMAGE_SCHEMA,
12
13
  contractComponentsFromFragments,
13
14
  contractPolicyFromGovernanceConfig,
14
15
  diffContractDomains,
15
16
  projectContractPreimage,
17
+ verifiedContractPreimageFromPin,
16
18
  } from "./preimage.js";
17
19
  export type {
18
20
  CanonicalMappingStatus,
@@ -20,6 +22,7 @@ export type {
20
22
  ContractCatalogInput,
21
23
  ContractComponentInput,
22
24
  ContractDomain,
25
+ ContractIdentityPin,
23
26
  ContractPolicyInput,
24
27
  ContractPreimage,
25
28
  ContractPropMappingInput,
@@ -18,6 +18,7 @@ import {
18
18
  contractComponentsFromFragments,
19
19
  diffContractDomains,
20
20
  projectContractPreimage,
21
+ verifiedContractPreimageFromPin,
21
22
  } from "./preimage.js";
22
23
  import { formatStampPin } from "./stamp.js";
23
24
 
@@ -61,6 +62,55 @@ describe("§1 byte parity — one fixture, one hash, every runtime", () => {
61
62
  });
62
63
  });
63
64
 
65
+ describe("published identity pin verification", () => {
66
+ it("reconstructs the exact canonical preimage behind a matching FCID", () => {
67
+ const preimage = projectContractPreimage(catalogFixtureA);
68
+
69
+ expect(
70
+ verifiedContractPreimageFromPin({
71
+ contractHash: contractHash(preimage),
72
+ preimageSchema: preimage.schema,
73
+ domainHashes: preimage.domains,
74
+ })
75
+ ).toEqual(preimage);
76
+ });
77
+
78
+ it("rejects tampered, incomplete, pre-identity, and future-domain pins", () => {
79
+ const preimage = projectContractPreimage(catalogFixtureA);
80
+ const valid = {
81
+ contractHash: contractHash(preimage),
82
+ preimageSchema: preimage.schema,
83
+ domainHashes: preimage.domains,
84
+ };
85
+
86
+ expect(
87
+ verifiedContractPreimageFromPin({
88
+ ...valid,
89
+ contractHash: "f".repeat(64),
90
+ })
91
+ ).toBeNull();
92
+ expect(
93
+ verifiedContractPreimageFromPin({
94
+ ...valid,
95
+ domainHashes: { components: preimage.domains.components },
96
+ })
97
+ ).toBeNull();
98
+ expect(
99
+ verifiedContractPreimageFromPin({
100
+ ...valid,
101
+ preimageSchema: undefined,
102
+ domainHashes: null,
103
+ })
104
+ ).toBeNull();
105
+ expect(
106
+ verifiedContractPreimageFromPin({
107
+ ...valid,
108
+ domainHashes: { ...preimage.domains, future: "a".repeat(64) },
109
+ })
110
+ ).toBeNull();
111
+ });
112
+ });
113
+
64
114
  describe("§2 enforced-only — prose doesn't move the hash, enforcement does", () => {
65
115
  it("(a) display-data edits leave the identity unchanged", () => {
66
116
  // Adapters may pass wider objects (enrichment descriptions, docs URLs…);
@@ -139,6 +139,8 @@ export interface ContractCatalogInput {
139
139
  // ---------------------------------------------------------------------------
140
140
 
141
141
  export const CONTRACT_PREIMAGE_SCHEMA = "fcid-preimage:v1" as const;
142
+ /** Request header used by clients that can verify and consume this preimage. */
143
+ export const CONTRACT_PREIMAGE_CAPABILITY_HEADER = "X-Fragments-Contract-Preimage" as const;
142
144
 
143
145
  /**
144
146
  * The hashed identity record: four domain sub-hashes plus the projection schema
@@ -150,6 +152,60 @@ export interface ContractPreimage {
150
152
  domains: Record<ContractDomain, string>;
151
153
  }
152
154
 
155
+ /**
156
+ * Wire-compatible identity pin carried by Cloud policy responses. Older
157
+ * contract rows may omit the schema/domains, so every field except the FCID is
158
+ * intentionally nullable at this boundary.
159
+ */
160
+ export interface ContractIdentityPin {
161
+ contractHash: string;
162
+ preimageSchema?: string | null;
163
+ domainHashes?: Readonly<Record<string, string>> | null;
164
+ }
165
+
166
+ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/u;
167
+
168
+ /**
169
+ * Reconstruct and verify the canonical preimage behind a published FCID.
170
+ *
171
+ * This is the only supported bridge from the additive ledger/wire shape
172
+ * (`preimageSchema` + `domainHashes`) back to the hash input. Consumers must
173
+ * not trust `enforcedIdentityComparable` on its own: a malformed, partial, or
174
+ * internally inconsistent pin returns `null` and therefore fails closed.
175
+ */
176
+ export function verifiedContractPreimageFromPin(pin: ContractIdentityPin): ContractPreimage | null {
177
+ if (
178
+ pin.preimageSchema !== CONTRACT_PREIMAGE_SCHEMA ||
179
+ !SHA256_HEX_PATTERN.test(pin.contractHash) ||
180
+ !pin.domainHashes ||
181
+ typeof pin.domainHashes !== "object"
182
+ ) {
183
+ return null;
184
+ }
185
+
186
+ const keys = Object.keys(pin.domainHashes).sort();
187
+ const expectedKeys = [...CONTRACT_DOMAINS].sort();
188
+ if (
189
+ keys.length !== expectedKeys.length ||
190
+ keys.some((key, index) => key !== expectedKeys[index])
191
+ ) {
192
+ return null;
193
+ }
194
+
195
+ const domains = Object.fromEntries(
196
+ CONTRACT_DOMAINS.map((domain) => [domain, pin.domainHashes?.[domain]])
197
+ ) as Record<ContractDomain, string | undefined>;
198
+ if (CONTRACT_DOMAINS.some((domain) => !SHA256_HEX_PATTERN.test(domains[domain] ?? ""))) {
199
+ return null;
200
+ }
201
+
202
+ const preimage: ContractPreimage = {
203
+ schema: CONTRACT_PREIMAGE_SCHEMA,
204
+ domains: domains as Record<ContractDomain, string>,
205
+ };
206
+ return contractHash(preimage) === pin.contractHash ? preimage : null;
207
+ }
208
+
153
209
  /**
154
210
  * Sort projected elements by their canonical serialization: total, deterministic,
155
211
  * and independent of adapter iteration order, without per-type comparators.
@@ -0,0 +1,27 @@
1
+ /** Stable evidence grades shared by scanners, renderers, Inspect, and Cloud. */
2
+ export type EvidenceGrade =
3
+ | "none"
4
+ | "runtime_advisory"
5
+ | "provenance_bound"
6
+ | "source_backed"
7
+ | "receipt_backed";
8
+
9
+ export const EVIDENCE_ORDER = [
10
+ "none",
11
+ "runtime_advisory",
12
+ "provenance_bound",
13
+ "source_backed",
14
+ "receipt_backed",
15
+ ] as const satisfies readonly EvidenceGrade[];
16
+
17
+ /** Only source-backed or receipt-backed claims may block a write or CI gate. */
18
+ export function canBlock(grade: EvidenceGrade): boolean {
19
+ return EVIDENCE_ORDER.indexOf(grade) >= EVIDENCE_ORDER.indexOf("source_backed");
20
+ }
21
+
22
+ export type InspectClaimTier = "advisory" | "local_only" | "team_ci" | "receipt";
23
+
24
+ export interface InspectClaim {
25
+ tier: InspectClaimTier;
26
+ text: string;
27
+ }
@@ -461,16 +461,20 @@ export function makeJsxComponentPreferredFact(input: {
461
461
  export function makeTokenDefinitionFact(input: {
462
462
  name: string;
463
463
  value: string;
464
+ location?: TokenDefinitionFact["location"];
464
465
  category?: TokenDefinitionFact["category"];
465
466
  referenceFormat?: TokenDefinitionFact["referenceFormat"];
467
+ sourceNames?: TokenDefinitionFact["sourceNames"];
466
468
  }): TokenDefinitionFact {
467
469
  return {
468
470
  id: factId("token_definition", { name: input.name }),
469
471
  kind: "token_definition",
470
472
  name: input.name,
471
473
  value: input.value,
474
+ ...(input.location ? { location: input.location } : {}),
472
475
  category: input.category,
473
476
  referenceFormat: input.referenceFormat,
477
+ ...(input.sourceNames?.length ? { sourceNames: [...input.sourceNames] } : {}),
474
478
  };
475
479
  }
476
480
 
@@ -58,6 +58,10 @@ function logicalComponentId(componentId: ComponentId): ComponentId {
58
58
  }
59
59
 
60
60
  function logicalFactForComparison(fact: Fact): Record<string, unknown> {
61
+ if (fact.kind === "token_definition") {
62
+ const { location: _location, sourceNames: _sourceNames, ...logicalFact } = fact;
63
+ return logicalFact;
64
+ }
61
65
  if (fact.kind === "jsx_import_path_preferred") {
62
66
  return {
63
67
  ...fact,
@@ -109,8 +113,12 @@ export class FactIndex {
109
113
  private readonly facts = new Map<FactId, Fact>();
110
114
  private readonly idsByKind = new Map<FactKind, Set<FactId>>();
111
115
  private readonly idsByComponent = new Map<ComponentId, Set<FactId>>();
116
+ private readonly tokenBySymbol = new Map<string, TokenDefinitionFact>();
112
117
 
113
118
  add(fact: Fact): void {
119
+ if (fact.kind === "token_definition") {
120
+ this.indexTokenSymbols(fact);
121
+ }
114
122
  const existing = this.facts.get(fact.id);
115
123
  if (existing) {
116
124
  if (
@@ -118,7 +126,7 @@ export class FactIndex {
118
126
  canonicalJson(logicalFactForComparison(fact))
119
127
  ) {
120
128
  console.warn(
121
- `FactIndex: conflicting facts for id ${fact.id} — keeping first fact, skipping ${describeFactForConflict(fact)}`
129
+ `FactIndex: conflicting facts for id ${fact.id} — keeping ${describeFactForConflict(existing)}, skipping ${describeFactForConflict(fact)}`
122
130
  );
123
131
  }
124
132
  return;
@@ -342,12 +350,28 @@ export class FactIndex {
342
350
  list: (): TokenDefinitionFact[] => this.byKind("token_definition"),
343
351
  byCategory: (category: TokenDefinitionFact["category"]): TokenDefinitionFact[] =>
344
352
  this.byKind("token_definition").filter((t) => t.category === category),
353
+ symbolExists: (name: string): boolean => this.tokenBySymbol.has(name),
354
+ definitionForSymbol: (name: string): TokenDefinitionFact | undefined =>
355
+ this.tokenBySymbol.get(name),
345
356
  };
346
357
 
347
358
  // ---------------------------------------------------------------------
348
359
  // Internals
349
360
  // ---------------------------------------------------------------------
350
361
 
362
+ private indexTokenSymbols(fact: TokenDefinitionFact): void {
363
+ const legacyScssAlias =
364
+ fact.referenceFormat === "scss-var" && fact.name.startsWith("--")
365
+ ? `$${fact.name.slice(2)}`
366
+ : fact.referenceFormat === "scss-var" && fact.name.startsWith("$")
367
+ ? `--${fact.name.slice(1)}`
368
+ : undefined;
369
+ for (const symbol of [fact.name, ...(fact.sourceNames ?? []), legacyScssAlias]) {
370
+ if (!symbol) continue;
371
+ if (!this.tokenBySymbol.has(symbol)) this.tokenBySymbol.set(symbol, fact);
372
+ }
373
+ }
374
+
351
375
  private byComponentOfKind<F extends Fact>(componentId: ComponentId, kind: FactKind): F[] {
352
376
  const ids = this.idsByComponent.get(logicalComponentId(componentId));
353
377
  if (!ids) return [];
@@ -9,6 +9,7 @@ import {
9
9
  compileComponentFacts,
10
10
  compileGlobalGovernanceFacts,
11
11
  g,
12
+ makeTokenDefinitionFact,
12
13
  makeUsageNodeFact,
13
14
  } from "../index.js";
14
15
  import type { ComponentId, FactId, PropValueForbiddenFact } from "../index.js";
@@ -443,6 +444,54 @@ describe("FactIndex — query layer", () => {
443
444
  warn.mockRestore();
444
445
  });
445
446
 
447
+ it("does not treat token-definition provenance as a logical conflict", () => {
448
+ const ix = new FactIndex();
449
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
450
+ const first = makeTokenDefinitionFact({
451
+ name: "--fui-color-accent",
452
+ value: "#2563eb",
453
+ location: { file: "tokens/base.css", line: 2, column: 1 },
454
+ });
455
+ const second = makeTokenDefinitionFact({
456
+ name: "--fui-color-accent",
457
+ value: "#2563eb",
458
+ location: { file: "tokens/theme.css", line: 4, column: 1 },
459
+ });
460
+
461
+ ix.add(first);
462
+ ix.add(second);
463
+
464
+ expect(warn).not.toHaveBeenCalled();
465
+ expect(ix.get(first.id)).toEqual(first);
466
+ warn.mockRestore();
467
+ });
468
+
469
+ it("includes token-definition provenance in logical conflict warnings", () => {
470
+ const ix = new FactIndex();
471
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
472
+ const first = makeTokenDefinitionFact({
473
+ name: "--fui-color-accent",
474
+ value: "#2563eb",
475
+ location: { file: "tokens/base.css", line: 2, column: 1 },
476
+ });
477
+ const second = makeTokenDefinitionFact({
478
+ name: "--fui-color-accent",
479
+ value: "#1d4ed8",
480
+ location: { file: "tokens/theme.css", line: 4, column: 1 },
481
+ });
482
+
483
+ ix.add(first);
484
+ ix.add(second);
485
+
486
+ expect(warn).toHaveBeenCalledWith(
487
+ expect.stringMatching(
488
+ /conflicting facts.*keeping kind=token_definition location=tokens\/base\.css:2:1, skipping kind=token_definition location=tokens\/theme\.css:4:1/
489
+ )
490
+ );
491
+ expect(ix.get(first.id)).toEqual(first);
492
+ warn.mockRestore();
493
+ });
494
+
446
495
  it("treats old and current owned component spellings as the same indexed facts", () => {
447
496
  const legacyId = asComponentId("@fragments-sdk/ui#Button");
448
497
  const currentId = asComponentId("@usefragments/ui#Button");
@@ -200,6 +200,7 @@ export interface JsxComponentPreferredFact extends BaseFact {
200
200
  */
201
201
  export interface TokenDefinitionFact extends BaseFact {
202
202
  kind: "token_definition";
203
+ location?: FactLocation;
203
204
  /** Token identifier, e.g., "fui-color-accent" or "--fui-color-accent". */
204
205
  name: string;
205
206
  /** Resolved value, e.g., "#2563eb". */
@@ -216,6 +217,8 @@ export interface TokenDefinitionFact extends BaseFact {
216
217
  * Absent on legacy/hand-built facts, which are treated as safe.
217
218
  */
218
219
  referenceFormat?: "css-var" | "scss-var" | "scss-map" | "dtcg";
220
+ /** Original authored spellings retained before parser normalization. */
221
+ sourceNames?: string[];
219
222
  }
220
223
 
221
224
  export interface TailwindPaletteAllowFact extends BaseFact {
@@ -177,8 +177,10 @@ describe("evaluateGovernanceIntegrity", () => {
177
177
  };
178
178
  const verdict = evaluate({ policy, policySource: "config", declared: true });
179
179
  const hygiene = verdict.families.find((f) => f.id === "hygiene");
180
+ const components = verdict.families.find((f) => f.id === "components");
180
181
  expect(hygiene?.armed).toBe(true);
181
182
  expect(hygiene?.rules).toContain("styles/no-raw-color");
183
+ expect(components?.remediation).toContain("govern.canonicalSources");
182
184
  expect(verdict.status).toBe("degraded");
183
185
  // degraded is enforceable, so not CI-fatal
184
186
  expect(verdict.fatalForCi).toBe(false);
@@ -25,7 +25,11 @@ import { BLOCKING_RULE_ALLOWLIST } from "./rules/emit-gate.js";
25
25
  export type GovernanceIntegrityStatus = "healthy" | "degraded" | "inert";
26
26
 
27
27
  export type GovernanceIntegrityFamilyId =
28
- "policy" | "components" | "tokens" | "hygiene" | "blocking-deny";
28
+ | "policy"
29
+ | "components"
30
+ | "tokens"
31
+ | "hygiene"
32
+ | "blocking-deny";
29
33
 
30
34
  export interface GovernanceIntegrityFamily {
31
35
  id: GovernanceIntegrityFamilyId;
@@ -177,6 +181,8 @@ export function evaluateGovernanceIntegrity(
177
181
  "add govern.canonicalSources (a directory source, or npm/registry with a non-empty include) or designSystem.path/packageName";
178
182
  } else {
179
183
  componentsFamily.reason = "components/prefer-library not enabled";
184
+ componentsFamily.remediation =
185
+ "add govern.canonicalSources (a directory source, or npm/registry with a non-empty include) or designSystem.path/packageName";
180
186
  }
181
187
  }
182
188
 
package/src/index.ts CHANGED
@@ -7,6 +7,10 @@
7
7
  export { BRAND, DEFAULTS } from "./constants.js";
8
8
  export type { Brand, Defaults } from "./constants.js";
9
9
 
10
+ // Evidence vocabulary (wire-stable, browser-safe, shared with Inspect).
11
+ export { EVIDENCE_ORDER, canBlock } from "./evidence.js";
12
+ export type { EvidenceGrade, InspectClaim, InspectClaimTier } from "./evidence.js";
13
+
10
14
  // Conform engine contract (shared across extract engine, mcp tool, surfaces)
11
15
  export type {
12
16
  ConformChange,
@@ -62,7 +66,11 @@ export type { Severity, SeverityLevel } from "./severity.js";
62
66
 
63
67
  // Canonical TypeStyle schemas
64
68
  export {
69
+ AGENT_FORMAT_SCHEMA_VERSION,
70
+ agentErrorEnvelopeSchema,
65
71
  agentFormatSchema,
72
+ agentIntegritySchema,
73
+ agentOutputSchema,
66
74
  factEvidenceSchema,
67
75
  factLocationSchema,
68
76
  findingFixSchema,
@@ -83,7 +91,9 @@ export {
83
91
  violationSchema,
84
92
  } from "./schemas/index.js";
85
93
  export type {
94
+ AgentErrorEnvelope,
86
95
  AgentFormat,
96
+ AgentOutput,
87
97
  Fix,
88
98
  Finding as CanonicalFinding,
89
99
  GovernanceVerdict as CanonicalGovernanceVerdict,
@@ -763,6 +773,7 @@ export {
763
773
  catalogFixtureAContractPayload,
764
774
  catalogFixtureATokensDtcg,
765
775
  CONTRACT_DOMAINS,
776
+ CONTRACT_PREIMAGE_CAPABILITY_HEADER,
766
777
  CONTRACT_PREIMAGE_SCHEMA,
767
778
  contractComponentsFromFragments,
768
779
  contractHash,
@@ -775,6 +786,7 @@ export {
775
786
  projectContractPreimage,
776
787
  serializeContractStamp,
777
788
  sha256Hex,
789
+ verifiedContractPreimageFromPin,
778
790
  } from "./contract/index.js";
779
791
  export {
780
792
  OWNED_PACKAGE_IDENTITY_EPOCH,
@@ -794,6 +806,7 @@ export type {
794
806
  ContractCatalogInput,
795
807
  ContractComponentInput,
796
808
  ContractDomain,
809
+ ContractIdentityPin,
797
810
  ContractPolicyInput,
798
811
  ContractPreimage,
799
812
  ContractPropMappingInput,
@@ -0,0 +1,174 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { FactIndex } from "../../facts/fact-index.js";
4
+ import {
5
+ makeGovernanceRuleConfigFact,
6
+ makeScaleFact,
7
+ makeScaleValueFact,
8
+ makeStyleDeclarationFact,
9
+ makeStyleFontSizeScaleFact,
10
+ makeStylePropertyScaleFact,
11
+ makeStyleRawColorForbiddenFact,
12
+ makeStyleRawDimensionForbiddenFact,
13
+ makeTokenDefinitionFact,
14
+ } from "../../facts/builders.js";
15
+ import type { Finding, FindingFix } from "../types.js";
16
+ import { RULES } from "../index.js";
17
+ import { tokenSymbolsInText } from "../utils.js";
18
+
19
+ describe("proof-carrying fix invariant", () => {
20
+ it("runs every rule and downgrades every token fix when its registry is poisoned", () => {
21
+ const ix = buildFixtureCorpus();
22
+ const fullRegistryFindings = runEveryRule(ix);
23
+ const fullRegistryFixes = tokenFixes(fullRegistryFindings);
24
+
25
+ expect(fullRegistryFindings.every((finding) => finding.evidenceGrade === "source_backed")).toBe(
26
+ true
27
+ );
28
+ expect(fullRegistryFixes).toHaveLength(5);
29
+ expect(fullRegistryFixes.every(({ fix }) => fix.deterministic === true)).toBe(true);
30
+
31
+ ix.tokens.symbolExists = () => false;
32
+ const poisonedFindings = runEveryRule(ix);
33
+ const poisonedFixes = tokenFixes(poisonedFindings);
34
+
35
+ expect(poisonedFixes).toHaveLength(fullRegistryFixes.length);
36
+ expect(
37
+ poisonedFixes.map(({ finding, fix }) => ({
38
+ ruleId: finding.ruleId,
39
+ fix: withoutDeterminism(fix),
40
+ }))
41
+ ).toEqual(
42
+ fullRegistryFixes.map(({ finding, fix }) => ({
43
+ ruleId: finding.ruleId,
44
+ fix: withoutDeterminism(fix),
45
+ }))
46
+ );
47
+ expect(poisonedFixes.filter(({ fix }) => fix.deterministic === true)).toEqual([]);
48
+ expect(
49
+ poisonedFixes.every(
50
+ ({ finding }) =>
51
+ typeof finding.attributes?.downgradeReason === "string" &&
52
+ finding.attributes.downgradeReason.includes("absent token symbol")
53
+ )
54
+ ).toBe(true);
55
+ });
56
+
57
+ it("downgrades an exact dimension match whose token has no directly applicable form", () => {
58
+ const ix = buildFixtureCorpus("dtcg");
59
+ const dimension = RULES.find((rule) => rule.id === "styles/no-raw-dimensions")
60
+ ?.run(ix)
61
+ .find((finding) => finding.fix);
62
+
63
+ expect(dimension?.fix?.deterministic).toBe(false);
64
+ expect(dimension?.attributes?.downgradeReason).toContain(
65
+ "token reference is not directly applicable"
66
+ );
67
+ });
68
+ });
69
+
70
+ function runEveryRule(ix: FactIndex): Finding[] {
71
+ const visited: string[] = [];
72
+ const findings = RULES.flatMap((rule) => {
73
+ visited.push(rule.id);
74
+ return rule.run(ix);
75
+ });
76
+ expect(visited).toEqual(RULES.map((rule) => rule.id));
77
+ return findings;
78
+ }
79
+
80
+ function tokenFixes(findings: readonly Finding[]) {
81
+ return findings.flatMap((finding) => {
82
+ const fix = finding.fix;
83
+ if (!fix) return [];
84
+ const symbols = tokenSymbolsInText(replacementText(fix));
85
+ return symbols.length > 0 ? [{ finding, fix, symbols }] : [];
86
+ });
87
+ }
88
+
89
+ function replacementText(fix: FindingFix): string {
90
+ if (fix.kind === "replaceStyleValue") return fix.value;
91
+ if (fix.kind === "replaceImport" || fix.kind === "replaceComponent") return fix.to;
92
+ if (fix.kind === "replaceClassToken") return fix.to ?? "";
93
+ return typeof fix.value === "string" ? fix.value : "";
94
+ }
95
+
96
+ function withoutDeterminism(fix: FindingFix): Omit<FindingFix, "deterministic"> {
97
+ const { deterministic: _deterministic, ...rest } = fix;
98
+ return rest;
99
+ }
100
+
101
+ function buildFixtureCorpus(radiusReferenceFormat: "css-var" | "dtcg" = "css-var"): FactIndex {
102
+ const ix = new FactIndex();
103
+ ix.addMany([
104
+ makeTokenDefinitionFact({
105
+ name: "--proof-color",
106
+ value: "#123456",
107
+ category: "color",
108
+ referenceFormat: "css-var",
109
+ sourceNames: ["--proof-color"],
110
+ }),
111
+ makeTokenDefinitionFact({
112
+ name: "--proof-space",
113
+ value: "8px",
114
+ category: "spacing",
115
+ referenceFormat: "css-var",
116
+ sourceNames: ["--proof-space"],
117
+ }),
118
+ makeTokenDefinitionFact({
119
+ name: "--proof-radius",
120
+ value: "4px",
121
+ category: "radius",
122
+ referenceFormat: radiusReferenceFormat,
123
+ sourceNames: ["--proof-radius"],
124
+ }),
125
+ makeTokenDefinitionFact({
126
+ name: "--proof-font",
127
+ value: "16px",
128
+ category: "typography",
129
+ referenceFormat: "css-var",
130
+ sourceNames: ["--proof-font"],
131
+ }),
132
+ makeTokenDefinitionFact({
133
+ name: "--proof-dual",
134
+ value: "8px",
135
+ category: "spacing",
136
+ referenceFormat: "scss-var",
137
+ sourceNames: ["--proof-dual", "$proof-dual"],
138
+ }),
139
+ makeStyleRawColorForbiddenFact({ except: [], prefer: "token", severity: "error" }),
140
+ makeStyleRawDimensionForbiddenFact({
141
+ appliesTo: ["border-radius"],
142
+ prefer: "token",
143
+ severity: "error",
144
+ }),
145
+ makeScaleFact({ name: "space", unit: "px" }),
146
+ makeScaleValueFact({ scale: "space", value: 8 }),
147
+ makeStylePropertyScaleFact({ property: "margin", scale: "space", severity: "error" }),
148
+ makeScaleFact({ name: "type", unit: "px" }),
149
+ makeScaleValueFact({ scale: "type", value: 16 }),
150
+ makeStyleFontSizeScaleFact({ scale: "type", severity: "error" }),
151
+ makeGovernanceRuleConfigFact({
152
+ ruleId: "tokens/require-dual-fallback",
153
+ enabled: true,
154
+ severity: "error",
155
+ }),
156
+ declaration("color", "#123456", 1),
157
+ declaration("border-radius", "4px", 2),
158
+ declaration("margin", "8px", 3),
159
+ declaration("font-size", "16px", 4),
160
+ declaration("gap", "var(--proof-dual)", 5),
161
+ ]);
162
+ return ix;
163
+ }
164
+
165
+ function declaration(property: string, value: string, line: number) {
166
+ return makeStyleDeclarationFact({
167
+ file: "fixture.scss",
168
+ selector: ".fixture",
169
+ declarationPath: `.fixture/${property}`,
170
+ property,
171
+ value,
172
+ location: { file: "fixture.scss", line, column: 1 },
173
+ });
174
+ }