@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.
- package/dist/{chunk-R3X7UBHU.js → chunk-57QDBEHQ.js} +101 -6
- package/dist/chunk-57QDBEHQ.js.map +1 -0
- package/dist/{chunk-243QYRUF.js → chunk-MLRDNSSA.js} +88 -47
- package/dist/chunk-MLRDNSSA.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +2 -2
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-BOa3KyiJ.d.ts → governance-DYSirwJu.d.ts} +22 -1
- package/dist/index-h_yWj15D.d.ts +6981 -0
- package/dist/index.d.ts +113 -97
- package/dist/index.js +387 -113
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +36 -36
- package/dist/schemas/index.d.ts +3 -2584
- package/dist/schemas/index.js +9 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-format.test.ts +144 -2
- package/src/agent-format.ts +34 -7
- package/src/codes/__tests__/codes.test.ts +57 -7
- package/src/codes/codes.ts +44 -42
- package/src/conform-prove.ts +4 -8
- package/src/contract/index.ts +3 -0
- package/src/contract/preimage.test.ts +50 -0
- package/src/contract/preimage.ts +56 -0
- package/src/evidence.ts +27 -0
- package/src/facts/builders.ts +4 -0
- package/src/facts/fact-index.ts +25 -1
- package/src/facts/facts.test.ts +49 -0
- package/src/facts/types.ts +3 -0
- package/src/governance-integrity.test.ts +2 -0
- package/src/governance-integrity.ts +7 -1
- package/src/index.ts +13 -0
- package/src/rules/__tests__/fix-emission-invariant.test.ts +174 -0
- package/src/rules/__tests__/tokens-require-dual-fallback.test.ts +90 -0
- package/src/rules/components-prefer-library.ts +165 -7
- package/src/rules/emit-gate.test.ts +40 -2
- package/src/rules/emit-gate.ts +21 -10
- package/src/rules/finding.ts +3 -0
- package/src/rules/fix-availability.ts +33 -0
- package/src/rules/rules.test.ts +180 -24
- package/src/rules/spacing-resolution.ts +5 -8
- package/src/rules/styles-no-raw-color.ts +32 -23
- package/src/rules/styles-no-raw-dimensions.ts +31 -12
- package/src/rules/styles-no-raw-spacing.ts +33 -11
- package/src/rules/styles-no-raw-typography.ts +37 -15
- package/src/rules/taxonomy.test.ts +19 -3
- package/src/rules/tiers.ts +1 -1
- package/src/rules/tokens-require-dual-fallback.ts +54 -17
- package/src/rules/utils.ts +51 -0
- package/src/schemas/index.ts +106 -4
- package/src/token-types.ts +6 -0
- package/src/tokens/__tests__/source-names.test.ts +42 -0
- package/src/tokens/design-token-parser.test.ts +24 -5
- package/src/tokens/design-token-parser.ts +40 -8
- package/dist/chunk-243QYRUF.js.map +0 -1
- package/dist/chunk-R3X7UBHU.js.map +0 -1
package/src/conform-prove.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
252
|
-
//
|
|
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.
|
|
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,
|
package/src/contract/index.ts
CHANGED
|
@@ -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…);
|
package/src/contract/preimage.ts
CHANGED
|
@@ -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.
|
package/src/evidence.ts
ADDED
|
@@ -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
|
+
}
|
package/src/facts/builders.ts
CHANGED
|
@@ -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
|
|
package/src/facts/fact-index.ts
CHANGED
|
@@ -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
|
|
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 [];
|
package/src/facts/facts.test.ts
CHANGED
|
@@ -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");
|
package/src/facts/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|