@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.
Files changed (42) hide show
  1. package/dist/{chunk-R3X7UBHU.js → chunk-BQQTMGLA.js} +24 -2
  2. package/dist/chunk-BQQTMGLA.js.map +1 -0
  3. package/dist/{chunk-243QYRUF.js → chunk-DBVTSUMT.js} +71 -44
  4. package/dist/chunk-DBVTSUMT.js.map +1 -0
  5. package/dist/codes/index.d.ts +1 -1
  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.d.ts +8 -6
  11. package/dist/index.js +156 -19
  12. package/dist/index.js.map +1 -1
  13. package/dist/react-types.d.ts +1 -1
  14. package/dist/schemas/index.d.ts +797 -4
  15. package/dist/schemas/index.js +9 -1
  16. package/dist/test-utils.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/src/agent-format.test.ts +72 -2
  19. package/src/agent-format.ts +10 -5
  20. package/src/codes/__tests__/codes.test.ts +57 -7
  21. package/src/codes/codes.ts +44 -42
  22. package/src/conform-prove.ts +4 -8
  23. package/src/contract/index.ts +3 -0
  24. package/src/contract/preimage.test.ts +50 -0
  25. package/src/contract/preimage.ts +56 -0
  26. package/src/facts/builders.ts +2 -0
  27. package/src/facts/fact-index.ts +4 -0
  28. package/src/facts/facts.test.ts +49 -0
  29. package/src/facts/types.ts +1 -0
  30. package/src/governance-integrity.test.ts +2 -0
  31. package/src/governance-integrity.ts +7 -1
  32. package/src/index.ts +9 -0
  33. package/src/rules/components-prefer-library.ts +165 -7
  34. package/src/rules/emit-gate.test.ts +24 -2
  35. package/src/rules/emit-gate.ts +15 -7
  36. package/src/rules/fix-availability.ts +33 -0
  37. package/src/rules/rules.test.ts +178 -23
  38. package/src/rules/taxonomy.test.ts +6 -3
  39. package/src/rules/tiers.ts +1 -1
  40. package/src/schemas/index.ts +25 -1
  41. package/dist/chunk-243QYRUF.js.map +0 -1
  42. package/dist/chunk-R3X7UBHU.js.map +0 -1
@@ -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-R3X7UBHU.js";
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,
@@ -1,4 +1,4 @@
1
- import { C as CompiledBlock, b as CompiledFragment } from './governance-BOa3KyiJ.js';
1
+ import { C as CompiledBlock, b as CompiledFragment } from './governance-DYSirwJu.js';
2
2
  import 'zod';
3
3
  import 'react';
4
4
  import './topology/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefragments/core",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Core types, schemas, and runtime API for Fragments component definitions",
6
6
  "author": "Conan McNicholl",
@@ -1,9 +1,22 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { buildAgentFormat } from "./agent-format.js";
4
- import { agentFormatSchema, type Finding } from "./schemas/index.js";
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(buildAgentFormat({ filesScanned: 3, findings }).score).toBeGreaterThan(0);
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
  });
@@ -1,10 +1,11 @@
1
1
  import { severityLevel } from "./severity.js";
2
- import type { AgentFormat, Finding } from "./schemas/index.js";
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 score = scoreFindings(input.findings);
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: "typestyle.agent.v1",
41
- passed: (input.passed ?? true) && !hasErrors,
42
+ schemaVersion: AGENT_FORMAT_SCHEMA_VERSION,
43
+ passed: !integrityFailed && (input.passed ?? true) && !hasErrors,
42
44
  score,
43
- summary: agentSummary(input.findings.length, input.filesScanned, score),
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
- CODES,
8
- EXPLAIN_URL_BASE,
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);
@@ -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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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
- code({
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({
@@ -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…);