import { describe, expect, it, vi } from "vitest"; import { componentId as makeComponentId, defineConfig, defineFragment, factId, FactIndex, compileComponentFacts, compileGlobalGovernanceFacts, g, makeUsageNodeFact, } from "../index.js"; import type { ComponentId, FactId, PropValueForbiddenFact } from "../index.js"; type ButtonProps = { variant?: "primary" | "secondary" | "ghost" | "link"; size?: "sm" | "md" | "lg"; disabled?: boolean; }; function Button(_props: ButtonProps) { return null; } function buildSampleFragment() { return defineFragment({ component: Button, meta: { name: "Button", description: "Interactive element for user-triggered actions.", category: "forms", }, guidance: { when: ["Submitting forms"], whenNot: ["Simple navigation without side effects"], }, props: { variant: { type: "enum", values: ["primary", "secondary", "ghost", "link"], description: "Visual style", required: false, }, size: { type: "enum", values: ["sm", "md", "lg"], description: "Size of the button", required: false, }, disabled: { type: "boolean", description: "Disables the button", required: false, }, }, govern: (govern) => [ govern.capability("dom.button"), govern.prop("variant").avoid("link", { because: "Use Link for ordinary navigation.", severity: "warn", }), govern.prop("variant").forbid("secondary", { when: { path: "apps/checkout/**" }, because: "Checkout CTAs should stay visually dominant.", fix: { replaceWith: "primary" }, severity: "error", }), govern.accessibility().requireName({ because: "Buttons must announce their action.", severity: "error", }), ], }); } const buttonId = makeComponentId("@fragments-sdk/ui", "Button"); describe("fact IR — content-addressed IDs", () => { it("emits the same fact IDs across runs for the same input", () => { const a = compileComponentFacts(buttonId, buildSampleFragment()); const b = compileComponentFacts(buttonId, buildSampleFragment()); expect(a.map((f) => f.id)).toEqual(b.map((f) => f.id)); }); it("does not change component or policy fact IDs when source location moves", () => { const baseline = compileComponentFacts(buttonId, buildSampleFragment()); // Different filePath simulates a moved component file. Identity inputs for // component/policy facts are componentId/prop/value/path-pattern only — // never the source location of the .fragment.ts file. const movedFragment = buildSampleFragment(); movedFragment._provenance = { source: "manual", verified: true, sourceFile: "src/components/Button.fragment.ts", } as never; const moved = compileComponentFacts(buttonId, movedFragment); expect(moved.map((f) => f.id)).toEqual(baseline.map((f) => f.id)); }); it("usage_node fact IDs include stable node identity inputs, not raw line/col", () => { const a = makeUsageNodeFact({ file: "apps/checkout/page.tsx", nodePath: "0/2/1", element: "Button", location: { file: "apps/checkout/page.tsx", line: 42, column: 8 }, }); const b = makeUsageNodeFact({ file: "apps/checkout/page.tsx", nodePath: "0/2/1", element: "Button", // Same identity inputs, different location — moved several lines down. location: { file: "apps/checkout/page.tsx", line: 87, column: 12 }, }); expect(a.id).toBe(b.id); // Identity changes when any of file / nodePath / element changes. const moved = makeUsageNodeFact({ file: "apps/checkout/page.tsx", nodePath: "0/2/2", element: "Button", location: { file: "apps/checkout/page.tsx", line: 42, column: 8 }, }); expect(moved.id).not.toBe(a.id); }); it("factId() canonicalizes attribute order", () => { const a = factId("prop_value_forbidden", { componentId: buttonId, prop: "variant", value: "secondary", pathPattern: "apps/checkout/**", }); const b = factId("prop_value_forbidden", { pathPattern: "apps/checkout/**", value: "secondary", prop: "variant", componentId: buttonId, }); expect(a).toBe(b); }); }); describe("FactIndex — query layer", () => { function buildIndex(): FactIndex { const ix = new FactIndex(); ix.addMany(compileComponentFacts(buttonId, buildSampleFragment())); ix.addMany( compileGlobalGovernanceFacts({ scales: { space: g.scale.px([0, 4, 8, 12, 16]), }, styles: [ g.styles.rawColors().forbid({ except: ["transparent"], prefer: "token", severity: "error", }), g.styles.rawSpacing().mustMatchScale("space", { appliesTo: ["padding", "gap"], severity: "error", }), ], jsx: [g.jsx.unknownProps().forbid({ severity: "error" })], }) ); return ix; } it("resolves components.byId() to the component metadata fact", () => { const ix = buildIndex(); const component = ix.components.byId(buttonId); expect(component?.kind).toBe("component"); expect(component?.componentId).toBe(buttonId); expect(component?.name).toBe("Button"); expect(component?.category).toBe("forms"); }); it("resolves prop metadata for a component", () => { const ix = buildIndex(); const props = ix.components.propsOf(buttonId); expect(props.map((p) => p.prop).sort()).toEqual(["disabled", "size", "variant"]); const variant = ix.components.propOf(buttonId, "variant"); expect(variant?.type).toBe("enum"); expect(variant?.values).toEqual(["primary", "secondary", "ghost", "link"]); expect(variant?.required).toBe(false); const disabled = ix.components.propOf(buttonId, "disabled"); expect(disabled?.type).toBe("boolean"); }); it("resolves component capabilities", () => { const ix = buildIndex(); const capabilities = ix.components.capabilitiesOf(buttonId); expect(capabilities.map((c) => c.capability)).toEqual(["dom.button"]); }); it("resolves policy records by component, property, and path", () => { const ix = buildIndex(); const allForbidden = ix.policy.forbiddenPropValues(buttonId); expect(allForbidden).toHaveLength(1); const forVariant = ix.policy.forbiddenPropValues(buttonId, { prop: "variant" }); expect(forVariant).toHaveLength(1); expect(forVariant[0].value).toBe("secondary"); expect(forVariant[0].pathPattern).toBe("apps/checkout/**"); const inCheckout = ix.policy.forbiddenPropValues(buttonId, { prop: "variant", path: "apps/checkout/cart/page.tsx", }); expect(inCheckout).toHaveLength(1); const inMarketing = ix.policy.forbiddenPropValues(buttonId, { prop: "variant", path: "apps/marketing/home.tsx", }); expect(inMarketing).toHaveLength(0); const avoided = ix.policy.avoidedPropValues(buttonId, { prop: "variant" }); expect(avoided).toHaveLength(1); expect(avoided[0].value).toBe("link"); const a11y = ix.policy.a11yNameRequired(buttonId); expect(a11y?.severity).toBe("error"); }); it("resolves global policy facts", () => { const ix = buildIndex(); expect( ix.policy .scales() .map((s) => s.name) .sort() ).toEqual(["space"]); expect( ix.policy .scaleValues("space") .map((v) => v.value) .sort((a, b) => a - b) ).toEqual([0, 4, 8, 12, 16]); const rawColors = ix.policy.rawColorPolicy(); expect(rawColors?.except).toEqual(["transparent"]); const padding = ix.policy.propertyScale("padding"); expect(padding?.scale).toBe("space"); const gap = ix.policy.propertyScale("gap"); expect(gap?.scale).toBe("space"); const unknownProps = ix.policy.jsxUnknownPropsForbidden(); expect(unknownProps?.severity).toBe("error"); }); it("activates prefer-library from top-level canonical sources", () => { const ix = new FactIndex(); ix.addMany( compileGlobalGovernanceFacts({ canonicalSources: [ { kind: "npm", specifier: "@fragments-sdk/ui", include: ["Button"], }, ], }) ); expect(ix.policy.ruleConfig("components/prefer-library")).toMatchObject({ enabled: true, severity: "warn", options: { canonicalSources: [ { kind: "npm", specifier: "@fragments-sdk/ui", include: ["Button"], }, ], }, }); }); it("resolves preferred import and component policy facts", () => { const ix = new FactIndex(); ix.addMany( compileGlobalGovernanceFacts({ jsx: [ g.jsx.importPath().prefer("@legacy/ui", "@fragments-sdk/ui", { imported: "Button", severity: "warn", }), g.jsx.component().prefer("@legacy/ui#LegacyButton", "@fragments-sdk/ui#Button", { severity: "error", }), ], }) ); expect(ix.policy.jsxImportPathPreferred({ from: "@legacy/ui" })).toMatchObject([ { kind: "jsx_import_path_preferred", from: "@legacy/ui", to: "@fragments-sdk/ui", imported: "Button", severity: "warn", }, ]); expect(ix.policy.jsxComponentPreferred()).toMatchObject([ { kind: "jsx_component_preferred", from: "@legacy/ui#LegacyButton", to: "@fragments-sdk/ui#Button", severity: "error", }, ]); }); it("evidence([]) rejects missing fact IDs", () => { const ix = buildIndex(); const componentFact = ix.components.byId(buttonId)!; const propFact = ix.components.propOf(buttonId, "variant")!; const ghost = "prop_value_forbidden:ghosts0123456789" as FactId; const ok = ix.evidence([componentFact.id, propFact.id]); expect(ok.map((e) => e.factId)).toEqual([componentFact.id, propFact.id]); expect(() => ix.evidence([componentFact.id, ghost])).toThrow(/missing/i); }); it("warns and keeps the first fact when a conflicting fact reuses an id", () => { const ix = new FactIndex(); const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); const id = factId("prop_value_forbidden", { componentId: buttonId, prop: "variant", value: "secondary", }); const a: PropValueForbiddenFact = { id, kind: "prop_value_forbidden", componentId: buttonId, prop: "variant", value: "secondary", because: "first", severity: "error", }; const b: PropValueForbiddenFact = { ...a, because: "second", }; ix.add(a); expect(() => ix.add(b)).not.toThrow(); expect(warn).toHaveBeenCalledWith(expect.stringContaining("conflicting facts")); expect(ix.get(id)).toEqual(a); warn.mockRestore(); }); }); describe("fact IR — deterministic snapshot", () => { it("compiles the sample fragment to a stable, sorted snapshot", () => { const config = defineConfig({ include: ["src/**/*.fragment.ts"], govern: { scales: { space: g.scale.px([0, 4, 8]), }, styles: [ g.styles.rawColors().forbid({ except: ["transparent"], prefer: "token", severity: "error", }), ], }, }); const facts = [ ...compileGlobalGovernanceFacts(config.govern), ...compileComponentFacts(buttonId, buildSampleFragment()), ].sort((a, b) => a.id.localeCompare(b.id)); expect(facts).toMatchSnapshot(); }); }); // Convenience type assertion to keep TS happy if FactId is unused above. const _typeWitness: ComponentId = buttonId; void _typeWitness;