@usefragments/core 1.0.0 → 1.2.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-BQQTMGLA.js} +24 -2
- package/dist/chunk-BQQTMGLA.js.map +1 -0
- package/dist/{chunk-243QYRUF.js → chunk-DBVTSUMT.js} +71 -44
- package/dist/chunk-DBVTSUMT.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- 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.d.ts +8 -6
- package/dist/index.js +156 -19
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/schemas/index.d.ts +797 -4
- 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 +72 -2
- package/src/agent-format.ts +10 -5
- 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/facts/builders.ts +2 -0
- package/src/facts/fact-index.ts +4 -0
- package/src/facts/facts.test.ts +49 -0
- package/src/facts/types.ts +1 -0
- package/src/governance-integrity.test.ts +2 -0
- package/src/governance-integrity.ts +7 -1
- package/src/index.ts +9 -0
- package/src/rules/components-prefer-library.ts +165 -7
- package/src/rules/emit-gate.test.ts +24 -2
- package/src/rules/emit-gate.ts +15 -7
- package/src/rules/fix-availability.ts +33 -0
- package/src/rules/rules.test.ts +178 -23
- package/src/rules/taxonomy.test.ts +6 -3
- package/src/rules/tiers.ts +1 -1
- package/src/schemas/index.ts +25 -1
- package/dist/chunk-243QYRUF.js.map +0 -1
- package/dist/chunk-R3X7UBHU.js.map +0 -1
package/dist/schemas/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AGENT_FORMAT_SCHEMA_VERSION,
|
|
3
|
+
agentErrorEnvelopeSchema,
|
|
2
4
|
agentFindingSchema,
|
|
3
5
|
agentFormatSchema,
|
|
6
|
+
agentIntegritySchema,
|
|
7
|
+
agentOutputSchema,
|
|
4
8
|
agentPlanSchema,
|
|
5
9
|
factEvidenceSchema,
|
|
6
10
|
factLocationSchema,
|
|
@@ -20,15 +24,19 @@ import {
|
|
|
20
24
|
suppressionDirectiveSchema,
|
|
21
25
|
validatorResultSchema,
|
|
22
26
|
violationSchema
|
|
23
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-BQQTMGLA.js";
|
|
24
28
|
import {
|
|
25
29
|
legacySeverityLevelSchema,
|
|
26
30
|
severityLevelSchema,
|
|
27
31
|
severitySchema
|
|
28
32
|
} from "../chunk-V4VQB57N.js";
|
|
29
33
|
export {
|
|
34
|
+
AGENT_FORMAT_SCHEMA_VERSION,
|
|
35
|
+
agentErrorEnvelopeSchema,
|
|
30
36
|
agentFindingSchema,
|
|
31
37
|
agentFormatSchema,
|
|
38
|
+
agentIntegritySchema,
|
|
39
|
+
agentOutputSchema,
|
|
32
40
|
agentPlanSchema,
|
|
33
41
|
factEvidenceSchema,
|
|
34
42
|
factLocationSchema,
|
package/dist/test-utils.d.ts
CHANGED
package/package.json
CHANGED
package/src/agent-format.test.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { buildAgentFormat } from "./agent-format.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AGENT_FORMAT_SCHEMA_VERSION,
|
|
6
|
+
agentFormatSchema,
|
|
7
|
+
agentOutputSchema,
|
|
8
|
+
type AgentFormat,
|
|
9
|
+
type Finding,
|
|
10
|
+
} from "./schemas/index.js";
|
|
5
11
|
|
|
6
12
|
const DEAD_TOOL_NAMES = ["apply_fix", "swap_to_canonical", "tokens.suggest"];
|
|
13
|
+
const HEALTHY_INTEGRITY = {
|
|
14
|
+
rulesLoaded: 1,
|
|
15
|
+
status: "healthy",
|
|
16
|
+
healthy: true,
|
|
17
|
+
inert: false,
|
|
18
|
+
reasons: [],
|
|
19
|
+
} satisfies AgentFormat["integrity"];
|
|
7
20
|
|
|
8
21
|
function finding(overrides: Partial<Finding> = {}): Finding {
|
|
9
22
|
return {
|
|
@@ -32,6 +45,7 @@ describe("buildAgentFormat", () => {
|
|
|
32
45
|
it("names only hosted MCP tools in fix plans", () => {
|
|
33
46
|
const agentFormat = buildAgentFormat({
|
|
34
47
|
filesScanned: 1,
|
|
48
|
+
integrity: HEALTHY_INTEGRITY,
|
|
35
49
|
findings: [
|
|
36
50
|
finding({
|
|
37
51
|
fix: {
|
|
@@ -69,6 +83,7 @@ describe("buildAgentFormat", () => {
|
|
|
69
83
|
it("keeps warn-only scans passed when the report summary passed", () => {
|
|
70
84
|
const agentFormat = buildAgentFormat({
|
|
71
85
|
filesScanned: 1,
|
|
86
|
+
integrity: HEALTHY_INTEGRITY,
|
|
72
87
|
passed: true,
|
|
73
88
|
findings: [
|
|
74
89
|
finding({
|
|
@@ -84,6 +99,7 @@ describe("buildAgentFormat", () => {
|
|
|
84
99
|
it("omits full findings and evidence by default", () => {
|
|
85
100
|
const agentFormat = buildAgentFormat({
|
|
86
101
|
filesScanned: 1,
|
|
102
|
+
integrity: HEALTHY_INTEGRITY,
|
|
87
103
|
findings: [finding()],
|
|
88
104
|
});
|
|
89
105
|
|
|
@@ -100,6 +116,60 @@ describe("buildAgentFormat", () => {
|
|
|
100
116
|
})
|
|
101
117
|
);
|
|
102
118
|
|
|
103
|
-
expect(
|
|
119
|
+
expect(
|
|
120
|
+
buildAgentFormat({ filesScanned: 3, findings, integrity: HEALTHY_INTEGRITY }).score
|
|
121
|
+
).toBeGreaterThan(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("keeps a healthy empty scan passed with a perfect score", () => {
|
|
125
|
+
const agentFormat = buildAgentFormat({
|
|
126
|
+
filesScanned: 1,
|
|
127
|
+
findings: [],
|
|
128
|
+
integrity: HEALTHY_INTEGRITY,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(agentFormat).toMatchObject({
|
|
132
|
+
schemaVersion: AGENT_FORMAT_SCHEMA_VERSION,
|
|
133
|
+
passed: true,
|
|
134
|
+
score: 100,
|
|
135
|
+
integrity: HEALTHY_INTEGRITY,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("fails an inert zero-rule scan with score zero and a machine-readable reason", () => {
|
|
140
|
+
const agentFormat = buildAgentFormat({
|
|
141
|
+
filesScanned: 1,
|
|
142
|
+
findings: [],
|
|
143
|
+
integrity: {
|
|
144
|
+
rulesLoaded: 0,
|
|
145
|
+
status: "inert",
|
|
146
|
+
healthy: false,
|
|
147
|
+
inert: true,
|
|
148
|
+
reasons: ["no_enforceable_rules"],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(agentFormat).toMatchObject({
|
|
153
|
+
passed: false,
|
|
154
|
+
score: 0,
|
|
155
|
+
integrity: {
|
|
156
|
+
rulesLoaded: 0,
|
|
157
|
+
healthy: false,
|
|
158
|
+
inert: true,
|
|
159
|
+
reasons: ["no_enforceable_rules"],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
expect(agentFormat.summary).toContain("no_enforceable_rules");
|
|
163
|
+
expect(() => agentFormatSchema.parse(agentFormat)).not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("validates the structured error envelope against the shared agent output schema", () => {
|
|
167
|
+
expect(() =>
|
|
168
|
+
agentOutputSchema.parse({
|
|
169
|
+
schemaVersion: AGENT_FORMAT_SCHEMA_VERSION,
|
|
170
|
+
passed: false,
|
|
171
|
+
error: { code: "check_failed", message: "Scan failed." },
|
|
172
|
+
})
|
|
173
|
+
).not.toThrow();
|
|
104
174
|
});
|
|
105
175
|
});
|
package/src/agent-format.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { severityLevel } from "./severity.js";
|
|
2
|
-
import type
|
|
2
|
+
import { AGENT_FORMAT_SCHEMA_VERSION, type AgentFormat, type Finding } from "./schemas/index.js";
|
|
3
3
|
import type { SuppressionDirectiveFact } from "./facts/index.js";
|
|
4
4
|
|
|
5
5
|
export interface BuildAgentFormatInput {
|
|
6
6
|
findings: readonly Finding[];
|
|
7
7
|
filesScanned: number;
|
|
8
|
+
integrity: AgentFormat["integrity"];
|
|
8
9
|
passed?: boolean;
|
|
9
10
|
suppressions?: readonly SuppressionDirectiveFact[];
|
|
10
11
|
iteration?: number;
|
|
@@ -16,7 +17,8 @@ export interface BuildAgentFormatInput {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function buildAgentFormat(input: BuildAgentFormatInput): AgentFormat {
|
|
19
|
-
const
|
|
20
|
+
const integrityFailed = !input.integrity.healthy || input.integrity.inert;
|
|
21
|
+
const score = integrityFailed ? 0 : scoreFindings(input.findings);
|
|
20
22
|
const visibleFindings = uniqueFindings(input.findings).slice(0, input.maxFindings ?? 50);
|
|
21
23
|
const agentFindings = visibleFindings.map((finding) => ({
|
|
22
24
|
code: finding.code,
|
|
@@ -37,10 +39,13 @@ export function buildAgentFormat(input: BuildAgentFormatInput): AgentFormat {
|
|
|
37
39
|
const hasErrors = input.findings.some((finding) => severityLevel(finding.severity) === "error");
|
|
38
40
|
|
|
39
41
|
return {
|
|
40
|
-
schemaVersion:
|
|
41
|
-
passed: (input.passed ?? true) && !hasErrors,
|
|
42
|
+
schemaVersion: AGENT_FORMAT_SCHEMA_VERSION,
|
|
43
|
+
passed: !integrityFailed && (input.passed ?? true) && !hasErrors,
|
|
42
44
|
score,
|
|
43
|
-
summary:
|
|
45
|
+
summary: integrityFailed
|
|
46
|
+
? `failed with score ${score}; ${input.integrity.reasons.join("; ") || "governance integrity is unhealthy"}`
|
|
47
|
+
: agentSummary(input.findings.length, input.filesScanned, score),
|
|
48
|
+
integrity: input.integrity,
|
|
44
49
|
fix_first: agentFindings.filter((finding) => finding.plan.confidence >= 0.8),
|
|
45
50
|
remaining: agentFindings.filter((finding) => finding.plan.confidence < 0.8),
|
|
46
51
|
suppression_template: {
|
|
@@ -3,18 +3,36 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
bridgeSourceViolation,
|
|
10
|
-
byCode,
|
|
11
|
-
byRuleId,
|
|
12
|
-
} from "../index.js";
|
|
6
|
+
import { CODES, EXPLAIN_URL_BASE, bridgeSourceViolation, byCode, byRuleId } from "../index.js";
|
|
7
|
+
import { RULE_FIX_AVAILABLE, type RuleWithFixAvailability } from "../../rules/fix-availability.js";
|
|
8
|
+
import { RULES } from "../../rules/index.js";
|
|
13
9
|
|
|
14
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
11
|
const __dirname = path.dirname(__filename);
|
|
16
12
|
const registryPath = path.resolve(__dirname, "../../../../../docs/codes/registry.json");
|
|
17
13
|
|
|
14
|
+
const SUBSYSTEM_EMITTED_CODES = ["FUI9001", "FUI9002", "FUI9003"] as const;
|
|
15
|
+
|
|
16
|
+
const RESERVED_CODES = {
|
|
17
|
+
FUI1001: "Reserved for canonical primitive findings; no core rule emits it.",
|
|
18
|
+
FUI1005: "Reserved for component allow-set policy; no core rule emits it.",
|
|
19
|
+
FUI1006: "Reserved for component deny-set policy; no core rule emits it.",
|
|
20
|
+
FUI2001: "Reserved for generic design-token requirements; no core rule emits it.",
|
|
21
|
+
FUI2002: "Reserved for token-prefix policy; no core rule emits it.",
|
|
22
|
+
FUI2013: "Reserved for brand-seed enforcement; no core rule emits it.",
|
|
23
|
+
FUI3002: "Reserved for general accessibility standards; no core rule emits it.",
|
|
24
|
+
FUI5001: "Reserved for required-child composition; no core rule emits it.",
|
|
25
|
+
FUI5002: "Deprecated canonical-composition alias; no active core rule emits it.",
|
|
26
|
+
FUI6001: "Reserved for generic unknown props; no core rule emits it.",
|
|
27
|
+
FUI6003: "Reserved for banned prop patterns; no core rule emits it.",
|
|
28
|
+
FUI8001: "Reserved for event-handler safety policy; no core rule emits it.",
|
|
29
|
+
FUI8002: "Reserved for dangerous-prop safety policy; no core rule emits it.",
|
|
30
|
+
FUI8003: "Reserved for controlled-prop safety policy; no core rule emits it.",
|
|
31
|
+
FUI8004: "Reserved for function-prop safety policy; no core rule emits it.",
|
|
32
|
+
FUI8005: "Reserved for internal-prop safety policy; no core rule emits it.",
|
|
33
|
+
FUI8006: "Reserved for href-sanitization safety policy; no core rule emits it.",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
18
36
|
describe("FUI code registry", () => {
|
|
19
37
|
it("does not contain duplicate codes", () => {
|
|
20
38
|
expect(new Set(CODES.map((entry) => entry.code)).size).toBe(CODES.length);
|
|
@@ -32,6 +50,38 @@ describe("FUI code registry", () => {
|
|
|
32
50
|
}
|
|
33
51
|
});
|
|
34
52
|
|
|
53
|
+
it("maps every implemented rule to a code", () => {
|
|
54
|
+
const unmappedRuleIds = RULES.filter((rule) => !byRuleId.has(rule.id)).map((rule) => rule.id);
|
|
55
|
+
expect(unmappedRuleIds).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("partitions every code into exactly one implementation state", () => {
|
|
59
|
+
const ruleBackedCodes = RULES.map((rule) => byRuleId.get(rule.id)?.code).filter(
|
|
60
|
+
(code): code is string => code !== undefined
|
|
61
|
+
);
|
|
62
|
+
const classifiedCodes = [
|
|
63
|
+
...ruleBackedCodes,
|
|
64
|
+
...SUBSYSTEM_EMITTED_CODES,
|
|
65
|
+
...Object.keys(RESERVED_CODES),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
expect(new Set(classifiedCodes).size).toBe(classifiedCodes.length);
|
|
69
|
+
expect(classifiedCodes.sort()).toEqual(CODES.map((entry) => entry.code).sort());
|
|
70
|
+
expect(
|
|
71
|
+
Object.values(RESERVED_CODES).every((reason) => reason.length > 0 && !reason.includes("\n"))
|
|
72
|
+
).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("keeps rule fix capability in lock-step with RULES and rule-backed codes", () => {
|
|
76
|
+
const ruleIds = RULES.map((rule) => rule.id).sort();
|
|
77
|
+
expect(Object.keys(RULE_FIX_AVAILABLE).sort()).toEqual(ruleIds);
|
|
78
|
+
|
|
79
|
+
for (const rule of RULES) {
|
|
80
|
+
const fixAvailable = RULE_FIX_AVAILABLE[rule.id as RuleWithFixAvailability];
|
|
81
|
+
expect(byRuleId.get(rule.id)?.fixAvailable, rule.id).toBe(fixAvailable);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
35
85
|
it("matches the committed docs registry snapshot", () => {
|
|
36
86
|
const snapshot = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
37
87
|
expect(snapshot).toEqual(CODES);
|
package/src/codes/codes.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Severity } from "../severity.js";
|
|
2
|
+
import { RULE_FIX_AVAILABLE, type RuleWithFixAvailability } from "../rules/fix-availability.js";
|
|
2
3
|
export type { Severity } from "../severity.js";
|
|
3
4
|
|
|
4
5
|
export const EXPLAIN_URL_BASE = "https://usefragments.com/errors/";
|
|
@@ -36,6 +37,19 @@ function code(entry: Omit<FuiCode, "explainUrl"> & { explainUrl?: never }): FuiC
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
type RuleCodeEntry = Omit<FuiCode, "explainUrl" | "fixAvailable" | "ruleId"> & {
|
|
41
|
+
ruleId: RuleWithFixAvailability;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function ruleCode(entry: RuleCodeEntry): FuiCode {
|
|
45
|
+
const { evidenceRequired, ...metadata } = entry;
|
|
46
|
+
return code({
|
|
47
|
+
...metadata,
|
|
48
|
+
fixAvailable: RULE_FIX_AVAILABLE[entry.ruleId],
|
|
49
|
+
evidenceRequired,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export const CODES = [
|
|
40
54
|
code({
|
|
41
55
|
code: "FUI1001",
|
|
@@ -47,34 +61,31 @@ export const CODES = [
|
|
|
47
61
|
fixAvailable: true,
|
|
48
62
|
evidenceRequired: true,
|
|
49
63
|
}),
|
|
50
|
-
|
|
64
|
+
ruleCode({
|
|
51
65
|
code: "FUI1002",
|
|
52
66
|
ruleId: "components/preferred-component",
|
|
53
67
|
category: "canonical-usage",
|
|
54
68
|
defaultSeverity: "serious",
|
|
55
69
|
title: "Preferred component should be used",
|
|
56
70
|
lifecycle: "experimental",
|
|
57
|
-
fixAvailable: true,
|
|
58
71
|
evidenceRequired: true,
|
|
59
72
|
}),
|
|
60
|
-
|
|
73
|
+
ruleCode({
|
|
61
74
|
code: "FUI1003",
|
|
62
75
|
ruleId: "imports/preferred-path",
|
|
63
76
|
category: "canonical-usage",
|
|
64
77
|
defaultSeverity: "moderate",
|
|
65
78
|
title: "Import should use the preferred path",
|
|
66
79
|
lifecycle: "experimental",
|
|
67
|
-
fixAvailable: true,
|
|
68
80
|
evidenceRequired: true,
|
|
69
81
|
}),
|
|
70
|
-
|
|
82
|
+
ruleCode({
|
|
71
83
|
code: "FUI1004",
|
|
72
84
|
ruleId: "components/prefer-library",
|
|
73
85
|
category: "canonical-usage",
|
|
74
86
|
defaultSeverity: "moderate",
|
|
75
87
|
title: "Library component should be preferred",
|
|
76
88
|
lifecycle: "experimental",
|
|
77
|
-
fixAvailable: true,
|
|
78
89
|
evidenceRequired: true,
|
|
79
90
|
}),
|
|
80
91
|
code({
|
|
@@ -117,104 +128,94 @@ export const CODES = [
|
|
|
117
128
|
fixAvailable: true,
|
|
118
129
|
evidenceRequired: true,
|
|
119
130
|
}),
|
|
120
|
-
|
|
131
|
+
ruleCode({
|
|
121
132
|
code: "FUI2003",
|
|
122
133
|
ruleId: "tokens/require-dual-fallback",
|
|
123
134
|
category: "tokens",
|
|
124
135
|
defaultSeverity: "serious",
|
|
125
136
|
title: "Token fallback is required",
|
|
126
137
|
lifecycle: "experimental",
|
|
127
|
-
fixAvailable: true,
|
|
128
138
|
evidenceRequired: true,
|
|
129
139
|
}),
|
|
130
|
-
|
|
140
|
+
ruleCode({
|
|
131
141
|
code: "FUI2004",
|
|
132
142
|
ruleId: "styles/no-raw-dimensions",
|
|
133
143
|
category: "tokens",
|
|
134
144
|
defaultSeverity: "serious",
|
|
135
145
|
title: "Raw dimension should use a token",
|
|
136
146
|
lifecycle: "experimental",
|
|
137
|
-
fixAvailable: true,
|
|
138
147
|
evidenceRequired: true,
|
|
139
148
|
}),
|
|
140
|
-
|
|
149
|
+
ruleCode({
|
|
141
150
|
code: "FUI2005",
|
|
142
151
|
ruleId: "styles/no-raw-color",
|
|
143
152
|
category: "tokens",
|
|
144
153
|
defaultSeverity: "serious",
|
|
145
154
|
title: "Raw color should use a token",
|
|
146
155
|
lifecycle: "experimental",
|
|
147
|
-
fixAvailable: true,
|
|
148
156
|
evidenceRequired: true,
|
|
149
157
|
}),
|
|
150
|
-
|
|
158
|
+
ruleCode({
|
|
151
159
|
code: "FUI2006",
|
|
152
160
|
ruleId: "styles/no-raw-spacing",
|
|
153
161
|
category: "tokens",
|
|
154
162
|
defaultSeverity: "serious",
|
|
155
163
|
title: "Raw spacing should use the scale",
|
|
156
164
|
lifecycle: "experimental",
|
|
157
|
-
fixAvailable: true,
|
|
158
165
|
evidenceRequired: true,
|
|
159
166
|
}),
|
|
160
|
-
|
|
167
|
+
ruleCode({
|
|
161
168
|
code: "FUI2007",
|
|
162
169
|
ruleId: "tailwind/arbitrary-color",
|
|
163
170
|
category: "tokens",
|
|
164
171
|
defaultSeverity: "moderate",
|
|
165
172
|
title: "Tailwind arbitrary color should use a token",
|
|
166
173
|
lifecycle: "experimental",
|
|
167
|
-
fixAvailable: true,
|
|
168
174
|
evidenceRequired: true,
|
|
169
175
|
}),
|
|
170
|
-
|
|
176
|
+
ruleCode({
|
|
171
177
|
code: "FUI2008",
|
|
172
178
|
ruleId: "tailwind/arbitrary-spacing",
|
|
173
179
|
category: "tokens",
|
|
174
180
|
defaultSeverity: "moderate",
|
|
175
181
|
title: "Tailwind arbitrary spacing should use the scale",
|
|
176
182
|
lifecycle: "experimental",
|
|
177
|
-
fixAvailable: true,
|
|
178
183
|
evidenceRequired: true,
|
|
179
184
|
}),
|
|
180
|
-
|
|
185
|
+
ruleCode({
|
|
181
186
|
code: "FUI2009",
|
|
182
187
|
ruleId: "tailwind/forbidden-palette",
|
|
183
188
|
category: "tokens",
|
|
184
189
|
defaultSeverity: "moderate",
|
|
185
190
|
title: "Tailwind palette is forbidden",
|
|
186
191
|
lifecycle: "experimental",
|
|
187
|
-
fixAvailable: false,
|
|
188
192
|
evidenceRequired: true,
|
|
189
193
|
}),
|
|
190
|
-
|
|
194
|
+
ruleCode({
|
|
191
195
|
code: "FUI2010",
|
|
192
196
|
ruleId: "tailwind/raw-color-via-token",
|
|
193
197
|
category: "tokens",
|
|
194
198
|
defaultSeverity: "moderate",
|
|
195
199
|
title: "Tailwind color should use a resolved token",
|
|
196
200
|
lifecycle: "experimental",
|
|
197
|
-
fixAvailable: true,
|
|
198
201
|
evidenceRequired: true,
|
|
199
202
|
}),
|
|
200
|
-
|
|
203
|
+
ruleCode({
|
|
201
204
|
code: "FUI2011",
|
|
202
205
|
ruleId: "tailwind/off-scale-spacing-token",
|
|
203
206
|
category: "tokens",
|
|
204
207
|
defaultSeverity: "moderate",
|
|
205
208
|
title: "Tailwind spacing token is off scale",
|
|
206
209
|
lifecycle: "experimental",
|
|
207
|
-
fixAvailable: true,
|
|
208
210
|
evidenceRequired: true,
|
|
209
211
|
}),
|
|
210
|
-
|
|
212
|
+
ruleCode({
|
|
211
213
|
code: "FUI2012",
|
|
212
214
|
ruleId: "tailwind/unknown-class",
|
|
213
215
|
category: "tokens",
|
|
214
216
|
defaultSeverity: "moderate",
|
|
215
217
|
title: "Tailwind class is unknown",
|
|
216
218
|
lifecycle: "experimental",
|
|
217
|
-
fixAvailable: false,
|
|
218
219
|
evidenceRequired: true,
|
|
219
220
|
}),
|
|
220
221
|
code({
|
|
@@ -227,34 +228,40 @@ export const CODES = [
|
|
|
227
228
|
fixAvailable: true,
|
|
228
229
|
evidenceRequired: true,
|
|
229
230
|
}),
|
|
230
|
-
|
|
231
|
+
ruleCode({
|
|
231
232
|
code: "FUI2014",
|
|
232
233
|
ruleId: "theme/no-theme-coupled-literal",
|
|
233
234
|
category: "tokens",
|
|
234
235
|
defaultSeverity: "moderate",
|
|
235
236
|
title: "Theme-coupled literal should use a semantic token",
|
|
236
237
|
lifecycle: "experimental",
|
|
237
|
-
fixAvailable: false,
|
|
238
238
|
evidenceRequired: true,
|
|
239
239
|
}),
|
|
240
|
-
|
|
240
|
+
ruleCode({
|
|
241
241
|
code: "FUI2015",
|
|
242
242
|
ruleId: "tokens/css-vars-must-be-defined",
|
|
243
243
|
category: "tokens",
|
|
244
244
|
defaultSeverity: "moderate",
|
|
245
245
|
title: "CSS variable is not in the contract vocabulary",
|
|
246
246
|
lifecycle: "experimental",
|
|
247
|
-
fixAvailable: false,
|
|
248
247
|
evidenceRequired: true,
|
|
249
248
|
}),
|
|
250
|
-
|
|
249
|
+
ruleCode({
|
|
250
|
+
code: "FUI2016",
|
|
251
|
+
ruleId: "styles/no-raw-typography",
|
|
252
|
+
category: "tokens",
|
|
253
|
+
defaultSeverity: "serious",
|
|
254
|
+
title: "Raw typography should use a token",
|
|
255
|
+
lifecycle: "experimental",
|
|
256
|
+
evidenceRequired: true,
|
|
257
|
+
}),
|
|
258
|
+
ruleCode({
|
|
251
259
|
code: "FUI3001",
|
|
252
260
|
ruleId: "a11y/required-accessible-name",
|
|
253
261
|
category: "a11y",
|
|
254
262
|
defaultSeverity: "serious",
|
|
255
263
|
title: "Accessible name is required",
|
|
256
264
|
lifecycle: "experimental",
|
|
257
|
-
fixAvailable: false,
|
|
258
265
|
evidenceRequired: true,
|
|
259
266
|
}),
|
|
260
267
|
code({
|
|
@@ -288,24 +295,22 @@ export const CODES = [
|
|
|
288
295
|
fixAvailable: true,
|
|
289
296
|
evidenceRequired: true,
|
|
290
297
|
}),
|
|
291
|
-
|
|
298
|
+
ruleCode({
|
|
292
299
|
code: "FUI5003",
|
|
293
300
|
ruleId: "composition/cardinality",
|
|
294
301
|
category: "composition",
|
|
295
302
|
defaultSeverity: "moderate",
|
|
296
303
|
title: "Too many of a component in one region",
|
|
297
304
|
lifecycle: "experimental",
|
|
298
|
-
fixAvailable: false,
|
|
299
305
|
evidenceRequired: true,
|
|
300
306
|
}),
|
|
301
|
-
|
|
307
|
+
ruleCode({
|
|
302
308
|
code: "FUI5004",
|
|
303
309
|
ruleId: "composition/co-occurrence",
|
|
304
310
|
category: "composition",
|
|
305
311
|
defaultSeverity: "moderate",
|
|
306
312
|
title: "Required companion component is missing from the region",
|
|
307
313
|
lifecycle: "experimental",
|
|
308
|
-
fixAvailable: false,
|
|
309
314
|
evidenceRequired: true,
|
|
310
315
|
}),
|
|
311
316
|
code({
|
|
@@ -318,14 +323,13 @@ export const CODES = [
|
|
|
318
323
|
fixAvailable: false,
|
|
319
324
|
evidenceRequired: true,
|
|
320
325
|
}),
|
|
321
|
-
|
|
326
|
+
ruleCode({
|
|
322
327
|
code: "FUI6002",
|
|
323
328
|
ruleId: "props/invalid-value",
|
|
324
329
|
category: "props",
|
|
325
330
|
defaultSeverity: "serious",
|
|
326
331
|
title: "Prop value is invalid",
|
|
327
332
|
lifecycle: "experimental",
|
|
328
|
-
fixAvailable: true,
|
|
329
333
|
evidenceRequired: true,
|
|
330
334
|
}),
|
|
331
335
|
code({
|
|
@@ -338,24 +342,22 @@ export const CODES = [
|
|
|
338
342
|
fixAvailable: false,
|
|
339
343
|
evidenceRequired: true,
|
|
340
344
|
}),
|
|
341
|
-
|
|
345
|
+
ruleCode({
|
|
342
346
|
code: "FUI6004",
|
|
343
347
|
ruleId: "components/forbidden-prop-value",
|
|
344
348
|
category: "props",
|
|
345
349
|
defaultSeverity: "serious",
|
|
346
350
|
title: "Prop value is forbidden",
|
|
347
351
|
lifecycle: "experimental",
|
|
348
|
-
fixAvailable: true,
|
|
349
352
|
evidenceRequired: true,
|
|
350
353
|
}),
|
|
351
|
-
|
|
354
|
+
ruleCode({
|
|
352
355
|
code: "FUI6005",
|
|
353
356
|
ruleId: "components/unknown-prop",
|
|
354
357
|
category: "props",
|
|
355
358
|
defaultSeverity: "moderate",
|
|
356
359
|
title: "Component prop is unknown",
|
|
357
360
|
lifecycle: "experimental",
|
|
358
|
-
fixAvailable: false,
|
|
359
361
|
evidenceRequired: true,
|
|
360
362
|
}),
|
|
361
363
|
code({
|
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…);
|