@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
@@ -1775,7 +1775,7 @@ describe("components/prefer-library", () => {
1775
1775
  expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
1776
1776
  });
1777
1777
 
1778
- it("flags imported bespoke components when package canonical sources include the exported component", () => {
1778
+ it("keeps same-name local import redirects advisory without a verified source contract", () => {
1779
1779
  const ix = new FactIndex();
1780
1780
  ix.addMany(
1781
1781
  compileGlobalGovernanceFacts({
@@ -1812,13 +1812,57 @@ describe("components/prefer-library", () => {
1812
1812
  expect(findings[0]).toMatchObject({
1813
1813
  ruleId: "components/prefer-library",
1814
1814
  code: "FUI1004",
1815
+ fix: {
1816
+ kind: "replaceImport",
1817
+ from: "../components/Card",
1818
+ to: "@acme/ui",
1819
+ deterministic: false,
1820
+ },
1815
1821
  attributes: {
1816
1822
  suggestedComponent: "Card",
1817
1823
  suggestedImport: "@acme/ui",
1824
+ advisory: true,
1825
+ contractProof: "missing",
1818
1826
  },
1819
1827
  });
1820
1828
  });
1821
1829
 
1830
+ it("does not mistake TanStack Router Link for the canonical Link by name alone", () => {
1831
+ const ix = new FactIndex();
1832
+ ix.addMany(
1833
+ compileGlobalGovernanceFacts({
1834
+ rules: {
1835
+ "components/prefer-library": {
1836
+ enabled: true,
1837
+ severity: "error",
1838
+ options: {
1839
+ canonicalSources: [{ kind: "npm", specifier: "@acme/ui", include: ["Link"] }],
1840
+ },
1841
+ },
1842
+ },
1843
+ })
1844
+ );
1845
+ ix.add(
1846
+ makeUsageNodeFact({
1847
+ file: "src/app.tsx",
1848
+ nodePath: "0:0",
1849
+ element: "Link",
1850
+ location: { file: "src/app.tsx", line: 5, column: 4 },
1851
+ })
1852
+ );
1853
+ ix.add(
1854
+ makeUsageImportFact({
1855
+ file: "src/app.tsx",
1856
+ local: "Link",
1857
+ imported: "Link",
1858
+ source: "@tanstack/react-router",
1859
+ location: { file: "src/app.tsx", line: 1, column: 0 },
1860
+ })
1861
+ );
1862
+
1863
+ expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
1864
+ });
1865
+
1822
1866
  it("recognizes a current import through legacy npm source and mapping spellings", () => {
1823
1867
  const ix = new FactIndex();
1824
1868
  ix.addMany(
@@ -2159,7 +2203,7 @@ describe("components/prefer-library", () => {
2159
2203
  expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
2160
2204
  });
2161
2205
 
2162
- it("attaches a deterministic component fix from confirmed canonical mappings", () => {
2206
+ it("keeps a complete mapped replacement guided until the conform AST preflight", () => {
2163
2207
  const ix = new FactIndex();
2164
2208
  ix.addMany(
2165
2209
  compileGlobalGovernanceFacts({
@@ -2183,6 +2227,15 @@ describe("components/prefer-library", () => {
2183
2227
  },
2184
2228
  })
2185
2229
  );
2230
+ ix.add(
2231
+ makeUsageImportFact({
2232
+ file: "src/app.tsx",
2233
+ local: "Button",
2234
+ imported: "Button",
2235
+ source: "@acme/ui",
2236
+ location: { file: "src/app.tsx", line: 1, column: 0 },
2237
+ })
2238
+ );
2186
2239
  ix.add(
2187
2240
  makeUsageNodeFact({
2188
2241
  file: "src/app.tsx",
@@ -2200,14 +2253,102 @@ describe("components/prefer-library", () => {
2200
2253
  kind: "replaceComponent",
2201
2254
  from: "button",
2202
2255
  to: "Button",
2203
- deterministic: true,
2256
+ deterministic: false,
2204
2257
  },
2205
2258
  attributes: {
2206
2259
  suggestedComponent: "Button",
2207
2260
  suggestedImport: "@acme/ui",
2208
2261
  propMapping: [{ rawProp: "className", canonicalProp: "variant" }],
2262
+ propCompatibility: "observed-complete",
2209
2263
  },
2210
2264
  });
2265
+ expect(findings[0].attributes).not.toHaveProperty("canonicalMappingConfirmed");
2266
+ });
2267
+
2268
+ it("withholds a native-element fix when a native-only prop has no confirmed mapping", () => {
2269
+ const ix = new FactIndex();
2270
+ ix.addMany(
2271
+ compileGlobalGovernanceFacts({
2272
+ rules: {
2273
+ "components/prefer-library": {
2274
+ enabled: true,
2275
+ severity: "warning",
2276
+ options: {
2277
+ canonicalMappings: [
2278
+ {
2279
+ name: "Button",
2280
+ canonical: "Button",
2281
+ htmlEquivalent: "button",
2282
+ importPath: "@acme/ui",
2283
+ },
2284
+ ],
2285
+ },
2286
+ },
2287
+ },
2288
+ })
2289
+ );
2290
+ ix.add(
2291
+ makeUsageImportFact({
2292
+ file: "src/app.tsx",
2293
+ local: "Button",
2294
+ imported: "Button",
2295
+ source: "@acme/ui",
2296
+ location: { file: "src/app.tsx", line: 1, column: 0 },
2297
+ })
2298
+ );
2299
+ addRawNode(ix, {
2300
+ element: "button",
2301
+ nodePath: "0:0",
2302
+ props: [{ prop: "formAction", value: "/save" }],
2303
+ });
2304
+
2305
+ const findings = ruleComponentsPreferLibrary(ix);
2306
+ expect(findings).toHaveLength(1);
2307
+ expect(findings[0]).toMatchObject({
2308
+ fix: { kind: "replaceComponent", deterministic: false },
2309
+ attributes: {
2310
+ propCompatibility: "incomplete",
2311
+ unprovenProps: ["formAction"],
2312
+ },
2313
+ });
2314
+ });
2315
+
2316
+ it("keeps replacement metadata but marks it non-deterministic when its import is absent", () => {
2317
+ const ix = new FactIndex();
2318
+ ix.addMany(
2319
+ compileGlobalGovernanceFacts({
2320
+ rules: {
2321
+ "components/prefer-library": {
2322
+ enabled: true,
2323
+ severity: "warning",
2324
+ options: {
2325
+ canonicalMappings: [
2326
+ {
2327
+ name: "Button",
2328
+ canonical: "Button",
2329
+ htmlEquivalent: "button",
2330
+ importPath: "@acme/ui",
2331
+ },
2332
+ ],
2333
+ },
2334
+ },
2335
+ },
2336
+ })
2337
+ );
2338
+ addRawNode(ix, { element: "button", nodePath: "0:0", text: "Save" });
2339
+
2340
+ const findings = ruleComponentsPreferLibrary(ix);
2341
+ expect(findings).toHaveLength(1);
2342
+ expect(findings[0].attributes).toMatchObject({
2343
+ suggestedComponent: "Button",
2344
+ suggestedImport: "@acme/ui",
2345
+ });
2346
+ expect(findings[0].fix).toMatchObject({
2347
+ kind: "replaceComponent",
2348
+ from: "button",
2349
+ to: "Button",
2350
+ deterministic: false,
2351
+ });
2211
2352
  });
2212
2353
 
2213
2354
  it("does not flag generic <div>/<span> from an inferred component primitive (anti-flood)", () => {
@@ -2561,29 +2702,38 @@ describe("components/prefer-library", () => {
2561
2702
  expect(findings.filter((f) => f.attributes?.suggestedComponent === "Hint")).toHaveLength(0);
2562
2703
  });
2563
2704
 
2564
- it("STILL fires the legit name-based import-redirect (<Modal> from wrong path → import @acme/ui)", () => {
2705
+ it("STILL fires the legit local name-based import redirect (<Modal> → import @acme/ui)", () => {
2565
2706
  // Regression guard: removing the canonical selector must not kill the
2566
2707
  // real name-match import-redirect. <Modal> (the component's own name)
2567
2708
  // imported from the wrong path is genuine drift.
2568
2709
  const findings = importedFindings(
2569
2710
  [{ name: "Modal", canonical: "Dialog", importPath: "@acme/ui" }],
2570
2711
  "Modal",
2571
- "react-aria"
2712
+ "../Modal"
2572
2713
  );
2573
2714
  expect(findings).toHaveLength(1);
2574
2715
  expect(findings[0].fix?.kind).toBe("replaceImport");
2575
2716
  expect(findings[0].attributes?.suggestedComponent).toBe("Modal");
2576
2717
  });
2577
2718
 
2578
- it("STILL fires when name === canonical (<Dialog> for name 'Dialog') from wrong path", () => {
2719
+ it("STILL fires for a local import when name === canonical (<Dialog> for name 'Dialog')", () => {
2579
2720
  const findings = importedFindings(
2580
2721
  [{ name: "Dialog", canonical: "Dialog", importPath: "@acme/ui" }],
2581
2722
  "Dialog",
2582
- "react-aria"
2723
+ "../Dialog"
2583
2724
  );
2584
2725
  expect(findings).toHaveLength(1);
2585
2726
  expect(findings[0].attributes?.suggestedComponent).toBe("Dialog");
2586
2727
  });
2728
+
2729
+ it("does not redirect a package-owned same-name Link without an explicit import policy", () => {
2730
+ const findings = importedFindings(
2731
+ [{ name: "Link", canonical: "Link", importPath: "@acme/ui" }],
2732
+ "Link",
2733
+ "@tanstack/react-router"
2734
+ );
2735
+ expect(findings).toHaveLength(0);
2736
+ });
2587
2737
  });
2588
2738
 
2589
2739
  describe("directory canonical source (unfiltered include — dogfood shape)", () => {
@@ -2672,7 +2822,7 @@ describe("components/prefer-library", () => {
2672
2822
  expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
2673
2823
  });
2674
2824
 
2675
- it("flags raw <button> → Button when Button is a declared directory canonical", () => {
2825
+ it("flags raw <button> → Button without advertising it as deterministic when Button is not imported", () => {
2676
2826
  const ix = new FactIndex();
2677
2827
  ix.addMany(
2678
2828
  compileGlobalGovernanceFacts({
@@ -2704,18 +2854,18 @@ describe("components/prefer-library", () => {
2704
2854
  expect(findings).toHaveLength(1);
2705
2855
  expect(findings[0]).toMatchObject({
2706
2856
  ruleId: "components/prefer-library",
2707
- fix: {
2708
- kind: "replaceComponent",
2709
- from: "button",
2710
- to: "Button",
2711
- deterministic: true,
2712
- },
2713
2857
  attributes: {
2714
2858
  rawValue: "button",
2715
2859
  suggestedComponent: "Button",
2716
2860
  suggestedImportSourceKind: "directory",
2717
2861
  },
2718
2862
  });
2863
+ expect(findings[0].fix).toMatchObject({
2864
+ kind: "replaceComponent",
2865
+ from: "button",
2866
+ to: "Button",
2867
+ deterministic: false,
2868
+ });
2719
2869
  });
2720
2870
 
2721
2871
  it("flags a raw element by canonical vocab id when display name diverges (Modal → Dialog)", () => {
@@ -2825,7 +2975,9 @@ describe("components/prefer-library", () => {
2825
2975
 
2826
2976
  const findings = ruleComponentsPreferLibrary(ix);
2827
2977
 
2828
- // type-specific mappings win over the generic tag-level one; text falls back
2978
+ // Type-specific mappings win over the generic tag-level one; text falls
2979
+ // back. No canonical bindings exist in this fixture, so suggestions remain
2980
+ // useful but are not advertised as deterministic edits.
2829
2981
  expect(findings.map((finding) => finding.attributes?.suggestedComponent)).toEqual([
2830
2982
  "Checkbox",
2831
2983
  "RadioGroup",
@@ -2836,7 +2988,7 @@ describe("components/prefer-library", () => {
2836
2988
  "RadioGroup",
2837
2989
  "TextField",
2838
2990
  ]);
2839
- expect(findings.every((finding) => finding.fix?.deterministic === true)).toBe(true);
2991
+ expect(findings.every((finding) => finding.fix?.deterministic === false)).toBe(true);
2840
2992
  });
2841
2993
 
2842
2994
  it("does not let a type-constrained mapping match a different or untyped input", () => {
@@ -2872,7 +3024,7 @@ describe("components/prefer-library", () => {
2872
3024
  expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
2873
3025
  });
2874
3026
 
2875
- it("attaches a deterministic import fix for canonical components from the wrong path", () => {
3027
+ it("keeps a same-name local import redirect advisory without source-contract proof", () => {
2876
3028
  const ix = new FactIndex();
2877
3029
  ix.addMany(
2878
3030
  compileGlobalGovernanceFacts({
@@ -2918,9 +3070,10 @@ describe("components/prefer-library", () => {
2918
3070
  kind: "replaceImport",
2919
3071
  from: "../Button",
2920
3072
  to: "@acme/ui",
2921
- deterministic: true,
3073
+ deterministic: false,
2922
3074
  },
2923
3075
  location: { line: 1, column: 0 },
3076
+ attributes: { advisory: true, contractProof: "missing" },
2924
3077
  });
2925
3078
  });
2926
3079
 
@@ -2967,13 +3120,14 @@ describe("components/prefer-library", () => {
2967
3120
  suggestedComponent: "Alert",
2968
3121
  matchedRole: "status",
2969
3122
  confidence: 0.9,
3123
+ advisory: true,
2970
3124
  },
2971
3125
  });
2972
3126
  // Role matches are advisory: no deterministic tag→component rewrite.
2973
3127
  expect(findings[0].fix).toBeUndefined();
2974
3128
  });
2975
3129
 
2976
- it("flags input type primitives config-free with deterministic fixes", () => {
3130
+ it("flags input type primitives config-free without unsafe missing-import fixes", () => {
2977
3131
  const ix = preferLibraryIndex();
2978
3132
  addRawNode(ix, {
2979
3133
  element: "input",
@@ -3003,7 +3157,7 @@ describe("components/prefer-library", () => {
3003
3157
  "input-type",
3004
3158
  "input-type",
3005
3159
  ]);
3006
- expect(findings.map((finding) => finding.fix?.deterministic)).toEqual([true, true, true]);
3160
+ expect(findings.every((finding) => finding.fix?.deterministic === false)).toBe(true);
3007
3161
  });
3008
3162
 
3009
3163
  it("does not infer generic text inputs as deterministic primitives", () => {
@@ -3093,7 +3247,7 @@ describe("components/prefer-library", () => {
3093
3247
  kind: "replaceComponent",
3094
3248
  from: "button",
3095
3249
  to: "Button",
3096
- deterministic: true,
3250
+ deterministic: false,
3097
3251
  });
3098
3252
  });
3099
3253
 
@@ -3139,13 +3293,13 @@ describe("components/prefer-library", () => {
3139
3293
  kind: "replaceComponent",
3140
3294
  from: "button",
3141
3295
  to: "Button",
3142
- deterministic: true,
3296
+ deterministic: false,
3143
3297
  });
3144
3298
  expect(findings[2].fix).toMatchObject({
3145
3299
  kind: "replaceComponent",
3146
3300
  from: "button",
3147
3301
  to: "Button",
3148
- deterministic: true,
3302
+ deterministic: false,
3149
3303
  });
3150
3304
  });
3151
3305
 
@@ -3430,6 +3584,7 @@ describe("styles/no-raw-typography (BUG 10)", () => {
3430
3584
  );
3431
3585
  const findings = ruleStylesNoRawTypography(ix);
3432
3586
  expect(findings).toHaveLength(1);
3587
+ expect(findings[0].code).toBe("FUI2016");
3433
3588
  expect(findings[0].fix).toMatchObject({
3434
3589
  value: "var(--font-size-md)",
3435
3590
  deterministic: true,
@@ -217,18 +217,21 @@ describe("§3 contract authored", () => {
217
217
  expect(ruleIds.every(isContractTierRule)).toBe(true);
218
218
  });
219
219
 
220
- it("ranks canonical-component bypass first (vocabulary-first), with a directed fix", () => {
220
+ it("ranks canonical-component bypass first without claiming an unsafe missing-import fix", () => {
221
221
  const findings = runRules(withButtonText(contractIndex()));
222
222
  const ranked = findings.map((f) => f.ruleId).sort(compareByVocabularyRank);
223
223
  expect(ranked).toEqual(["components/prefer-library", "tokens/css-vars-must-be-defined"]);
224
224
 
225
225
  const canonical = findings.find((f) => f.ruleId === "components/prefer-library");
226
- // "Replace your raw <button> with your <Button>" a directed, deterministic fix.
226
+ // The direction is explicit, but this fixture has no Button binding. A
227
+ // generic source patcher cannot safely rename the element without also
228
+ // importing Button, so the replacement stays visible but is not advertised
229
+ // as deterministic.
227
230
  expect(canonical?.fix).toMatchObject({
228
231
  kind: "replaceComponent",
229
232
  from: "button",
230
233
  to: "Button",
231
- deterministic: true,
234
+ deterministic: false,
232
235
  });
233
236
  expect(canonical?.attributes).toMatchObject({
234
237
  rawValue: "button",
@@ -27,7 +27,7 @@ export type RuleTier = "contract" | "hygiene";
27
27
  */
28
28
  export const RULE_TIER: Readonly<Record<string, RuleTier>> = {
29
29
  // ---- Tier A — contract vocabulary -------------------------------------
30
- "components/prefer-library": "contract", // canonical-component bypass (FUI1001/1004)
30
+ "components/prefer-library": "contract", // canonical-component bypass (FUI1004)
31
31
  "tokens/css-vars-must-be-defined": "contract", // token drift (FUI2015)
32
32
 
33
33
  // ---- Tier B — generic hygiene -----------------------------------------
@@ -228,11 +228,22 @@ export const agentFindingSchema = z.object({
228
228
  plan: agentPlanSchema,
229
229
  });
230
230
 
231
+ export const AGENT_FORMAT_SCHEMA_VERSION = "typestyle.agent.v2" as const;
232
+
233
+ export const agentIntegritySchema = z.object({
234
+ rulesLoaded: z.number().int().nonnegative(),
235
+ status: z.enum(["healthy", "degraded", "inert"]),
236
+ healthy: z.boolean(),
237
+ inert: z.boolean(),
238
+ reasons: z.array(z.string()),
239
+ });
240
+
231
241
  export const agentFormatSchema = z.object({
232
- schemaVersion: z.literal("typestyle.agent.v1"),
242
+ schemaVersion: z.literal(AGENT_FORMAT_SCHEMA_VERSION),
233
243
  passed: z.boolean(),
234
244
  score: z.number(),
235
245
  summary: z.string(),
246
+ integrity: agentIntegritySchema,
236
247
  fix_first: z.array(agentFindingSchema),
237
248
  remaining: z.array(agentFindingSchema),
238
249
  suppression_template: z.object({
@@ -250,6 +261,19 @@ export const agentFormatSchema = z.object({
250
261
  });
251
262
  export type AgentFormat = z.infer<typeof agentFormatSchema>;
252
263
 
264
+ export const agentErrorEnvelopeSchema = z.object({
265
+ schemaVersion: z.literal(AGENT_FORMAT_SCHEMA_VERSION),
266
+ passed: z.literal(false),
267
+ error: z.object({
268
+ code: z.string().min(1),
269
+ message: z.string().min(1),
270
+ }),
271
+ });
272
+ export type AgentErrorEnvelope = z.infer<typeof agentErrorEnvelopeSchema>;
273
+
274
+ export const agentOutputSchema = z.union([agentFormatSchema, agentErrorEnvelopeSchema]);
275
+ export type AgentOutput = z.infer<typeof agentOutputSchema>;
276
+
253
277
  export function normalizeSeverity(severity: Severity | LegacySeverityLevel): Severity {
254
278
  return severitySchema.safeParse(severity).success
255
279
  ? (severity as Severity)