diff --git a/src/review/content-lane/registry-logic.ts b/src/review/content-lane/registry-logic.ts index 0cbb1d3605..a74d779614 100644 --- a/src/review/content-lane/registry-logic.ts +++ b/src/review/content-lane/registry-logic.ts @@ -679,13 +679,17 @@ export interface RegistryScopeResult { /** * Generic surface-model scope classifier: in scope when the PR edits exactly ONE entry file (or, entry-free, - * one flat provider file); the spec's provider + artifact files are allowed companions; any other path — - * including a retired candidate path the registry no longer accepts — makes it mixed-files. + * one flat provider file); the spec's provider + artifact files are allowed companions. A registry-looking + * submission with too many entry/provider files is malformed and stays in the lane as mixed-files; an unrelated + * PR with no direct registry files remains not-direct-submission. */ export function classifyRegistryPrScope(spec: RegistryLaneSpec, changedFiles: string[]): RegistryScopeResult { const files = (changedFiles ?? []).map((f) => String(f || "").trim()).filter(Boolean); const entryFiles = files.filter((f) => spec.entryFilePattern.test(f)); const providerFiles = spec.providerFilePattern ? files.filter((f) => spec.providerFilePattern!.test(f)) : []; + if (entryFiles.length > 1 || (entryFiles.length === 0 && providerFiles.length > 1)) { + return { scope: "mixed-files", directFile: null, isProvider: false }; + } const isEntryPr = entryFiles.length === 1; const isProviderPr = entryFiles.length === 0 && providerFiles.length === 1; if (!isEntryPr && !isProviderPr) { diff --git a/test/unit/content-lane-orchestrator.test.ts b/test/unit/content-lane-orchestrator.test.ts index 861233ca46..8915092aa4 100644 --- a/test/unit/content-lane-orchestrator.test.ts +++ b/test/unit/content-lane-orchestrator.test.ts @@ -48,6 +48,20 @@ describe("runSurfaceReview (deterministic + decisive: merge/close, rarely manual expect((await review([SUBNET, "src/index.ts"], {}))?.verdict).toBe("close"); }); + it("closes a multi-entry registry submission instead of deferring to the generic gate", async () => { + const calls: string[] = []; + const result = await runSurfaceReview(METAGRAPHED_LANE_SPEC, { + changedFiles: [SUBNET, "registry/subnets/bar.json"], + loadFile: (path, ref) => { + calls.push(`${ref}:${path}`); + return Promise.resolve(null); + }, + }); + + expect(result?.verdict).toBe("close"); + expect(calls).toEqual([]); + }); + it("merges a valid provider submission and CLOSES an invalid one (never manual)", async () => { const okProvider = { [`head:${PROVIDER}`]: JSON.stringify({ provider: { id: "acme", name: "Acme", website_url: "https://acme.example" } }) }; expect((await review([PROVIDER], okProvider))?.verdict).toBe("merge"); diff --git a/test/unit/content-lane-registry-logic.test.ts b/test/unit/content-lane-registry-logic.test.ts index 4907703401..7e8505bc95 100644 --- a/test/unit/content-lane-registry-logic.test.ts +++ b/test/unit/content-lane-registry-logic.test.ts @@ -300,9 +300,10 @@ describe("classifyRegistryPrScope (generic surface model, metagraphed spec)", () expect(classifyRegistryPrScope(spec, ["registry/subnets/actual.json", "registry/candidates/community/foo.json"]).scope).toBe("mixed-files"); }); - it("is not-direct for no submission, and >1 entry file", () => { + it("is not-direct for no submission, but mixed for too many direct registry files", () => { expect(classifyRegistryPrScope(spec, ["README.md"]).scope).toBe("not-direct-submission"); - expect(classifyRegistryPrScope(spec, ["registry/subnets/a.json", "registry/subnets/b.json"]).scope).toBe("not-direct-submission"); + expect(classifyRegistryPrScope(spec, ["registry/subnets/a.json", "registry/subnets/b.json"]).scope).toBe("mixed-files"); + expect(classifyRegistryPrScope(spec, ["registry/providers/a.json", "registry/providers/b.json"]).scope).toBe("mixed-files"); expect(isRegistrySubmissionScope("not-direct-submission")).toBe(false); }); @@ -314,7 +315,7 @@ describe("classifyRegistryPrScope (generic surface model, metagraphed spec)", () it("works for a minimal spec with no provider/artifact patterns (a bare registry)", () => { const bare: RegistryLaneSpec = { entryFilePattern: /^data\/[a-z]+\.json$/, collectionField: "entries" }; expect(classifyRegistryPrScope(bare, ["data/x.json"]).scope).toBe("entry-submission"); - expect(classifyRegistryPrScope(bare, ["data/x.json", "data/y.json"]).scope).toBe("not-direct-submission"); + expect(classifyRegistryPrScope(bare, ["data/x.json", "data/y.json"]).scope).toBe("mixed-files"); expect(classifyRegistryPrScope(bare, ["data/x.json", "other.json"]).scope).toBe("mixed-files"); }); });