From 58888f2e631347383afd96446c3d283df7cddfc2 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:26:54 -0700 Subject: [PATCH] fix(content-lane): reject multi-entry registry submissions --- src/review/content-lane/registry-logic.ts | 8 ++++++-- test/unit/content-lane-orchestrator.test.ts | 14 ++++++++++++++ test/unit/content-lane-registry-logic.test.ts | 7 ++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/review/content-lane/registry-logic.ts b/src/review/content-lane/registry-logic.ts index c4f2d06742..769cda18d3 100644 --- a/src/review/content-lane/registry-logic.ts +++ b/src/review/content-lane/registry-logic.ts @@ -838,13 +838,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 e2907311fb..2366e7fb65 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 b24086dc7e..a4229a244c 100644 --- a/test/unit/content-lane-registry-logic.test.ts +++ b/test/unit/content-lane-registry-logic.test.ts @@ -385,9 +385,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); }); @@ -399,7 +400,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"); }); });