diff --git a/__tests__/external/insight-provider.test.ts b/__tests__/external/insight-provider.test.ts index 93b774c..77e7c49 100644 --- a/__tests__/external/insight-provider.test.ts +++ b/__tests__/external/insight-provider.test.ts @@ -6,6 +6,7 @@ import { ExternalInsightSchema } from "agent-contracts-analyzer"; import { MicroContractsInsightProvider, buildExternalInsight, + filterInsight, } from "../../src/external/insight-provider.js"; const EXAMPLES_ROOT = path.resolve(import.meta.dirname, "../../examples"); @@ -85,6 +86,61 @@ describe("MicroContractsInsightProvider", () => { "packages/contract/billing/services/BillingServiceApi.ts#createInvoice", ); }); + + it("filters edges by changedFiles to those touching the matching module", async () => { + const provider = new MicroContractsInsightProvider(); + const filtered = await provider.provide({ + projectRoot: EXAMPLES_ROOT, + changedFiles: ["spec/billing/openapi/billing.yaml"], + }); + + expect(filtered.edges.length).toBeGreaterThan(0); + expect( + filtered.edges.some( + (e) => + e.from === "billing" && + e.to === "core.User.getUsers" && + e.kind === "api_dependency", + ), + ).toBe(true); + for (const edge of filtered.edges) { + const touchesBillingModule = + edge.from === "billing" || edge.from.startsWith("billing."); + const touchesBillingDependency = + edge.to === "billing" || edge.to.startsWith("billing."); + expect(touchesBillingModule || touchesBillingDependency).toBe(true); + } + expect(filtered.anchorMapping?.some((a) => a.domainId === "billing")).toBe( + true, + ); + }); + + it("filters edges by artifactIds", async () => { + const provider = new MicroContractsInsightProvider(); + const fullInsight = buildExternalInsight(EXAMPLES_ROOT); + const filtered = await provider.provide({ + projectRoot: EXAMPLES_ROOT, + artifactIds: ["billing.Billing.createInvoice"], + }); + + expect(filtered.edges.length).toBeLessThan(fullInsight.edges.length); + expect(filtered.edges).toHaveLength(1); + expect(filtered.edges[0]).toMatchObject({ + from: "billing.Billing.createInvoice", + to: "core.User.getUserById", + kind: "api_operation_dependency", + }); + expect(filtered.anchorMapping?.map((a) => a.domainId).sort()).toEqual( + ["billing.Billing.createInvoice", "core.User.getUserById"].sort(), + ); + }); + + it("returns all edges when no filters are specified", () => { + const insight = buildExternalInsight(EXAMPLES_ROOT); + const unfiltered = filterInsight(insight, { projectRoot: EXAMPLES_ROOT }); + expect(unfiltered.edges).toEqual(insight.edges); + expect(unfiltered.anchorMapping).toEqual(insight.anchorMapping); + }); }); describe("buildExternalInsight error handling", () => { diff --git a/examples/packages/.generated-manifest.json b/examples/packages/.generated-manifest.json index 475ef87..a6f0f92 100644 --- a/examples/packages/.generated-manifest.json +++ b/examples/packages/.generated-manifest.json @@ -1,6 +1,6 @@ { "version": "1.0", - "generatorVersion": "0.17.7", + "generatorVersion": "0.17.8", "files": { "contract-published/billing/docs/api-reference.html": { "sha256": "5b69e88fc9f60578261a8231132678550a40a70eb52dfcc611f48937f448ca44" @@ -144,6 +144,6 @@ "sha256": "d791f51b36215aba3e7cf7c5f69fc4b9fd6d4f2cf515718fdc93589fa05e2608" } }, - "inputHash": "338fbc40d41809be96cfe07d8b97e9e7e652387d2bb290593310df6d3f3345d8", - "updatedAt": "2026-06-06T16:42:53.184Z" + "inputHash": "9bcb8bb05aacf77b0826790904e5bedbf889468a39bb1369af7d340c587e5041", + "updatedAt": "2026-06-06T16:59:45.158Z" } diff --git a/package.json b/package.json index f1f4b84..a913163 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "micro-contracts", - "version": "0.17.7", + "version": "0.17.8", "description": "Contract-first OpenAPI toolchain that keeps TypeScript UI and microservices aligned via code generation", "type": "module", "main": "dist/index.js", diff --git a/src/external/insight-provider.ts b/src/external/insight-provider.ts index ddfa437..6147098 100644 --- a/src/external/insight-provider.ts +++ b/src/external/insight-provider.ts @@ -350,11 +350,57 @@ export function buildExternalInsight( }; } +export function filterInsight( + insight: ExternalInsight, + query: InsightQuery, +): ExternalInsight { + const { changedFiles, artifactIds } = query; + if (!changedFiles?.length && !artifactIds?.length) return insight; + + const relevantIds = new Set(); + + if (changedFiles?.length) { + const changedSet = new Set(changedFiles); + for (const anchor of insight.anchorMapping ?? []) { + if (anchor.filePaths.some((fp) => changedSet.has(fp))) { + relevantIds.add(anchor.domainId); + } + } + } + + if (artifactIds?.length) { + for (const id of artifactIds) { + relevantIds.add(id); + } + } + + const filteredEdges = insight.edges.filter( + (e) => relevantIds.has(e.from) || relevantIds.has(e.to), + ); + + const referencedIds = new Set(); + for (const edge of filteredEdges) { + referencedIds.add(edge.from); + referencedIds.add(edge.to); + } + const filteredAnchors = (insight.anchorMapping ?? []).filter((a) => + referencedIds.has(a.domainId), + ); + + return { + ...insight, + edges: filteredEdges, + anchorMapping: + filteredAnchors.length > 0 ? filteredAnchors : undefined, + }; +} + export class MicroContractsInsightProvider implements InsightProvider { readonly name = MICRO_CONTRACTS_INSIGHT_SOURCE; async provide(query: InsightQuery): Promise { - return buildExternalInsight(query.projectRoot); + const insight = buildExternalInsight(query.projectRoot); + return filterInsight(insight, query); } }