From a013ff5a7947b1712377b1da18ca0dd4e7d25906 Mon Sep 17 00:00:00 2001 From: kai392 Date: Thu, 30 Jul 2026 07:55:55 +0800 Subject: [PATCH] fix(orb): canonicalize issue-body path tokens in checkContentLaneDeliverable (#9667) checkContentLaneDeliverable compares an issue-body path against the spec's entryFilePattern/providerFilePattern, which globToRegExp compiles against a CANONICALIZED (lowercased) path with no `i` flag -- so they only match lowercase input. The changedFiles side already canonicalizes each path before testing; the issue-body side did not. An issue naming the path with any capitalization (`Registry/Subnets/Foo.json`) produced no mentionedPath, so with no issueTitleImpliesEntryPattern the check returned "not-applicable" and the deliverable gate silently did not run -- the exact "test-only PR closes a content issue without delivering the content" gap the function exists to close (classifyRegistryPrScope in the same file already documents this canonicalization hazard). - Canonicalize each token from extractPathTokens before matchesSpec, exactly as the changedFiles side does. `.find` still returns the ORIGINAL token, so the "missing" verdict's mentionedPath stays the path as written in the issue body (rendered verbatim into the public PR comment). Tests: a mixed-case issue path is "missing" (original case preserved in mentionedPath) and "delivered" when the real lowercase file is changed; the all-lowercase behaviour is unchanged; a path matching no spec pattern still returns not-applicable (no over-broadening). The mixed-case cases fail against the current code. Closes #9667 Co-Authored-By: Claude Opus 4.8 --- src/review/content-lane/registry-logic.ts | 7 ++++- test/unit/content-lane-registry-logic.test.ts | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/review/content-lane/registry-logic.ts b/src/review/content-lane/registry-logic.ts index 921b018b2e..0c3b6b2cd1 100644 --- a/src/review/content-lane/registry-logic.ts +++ b/src/review/content-lane/registry-logic.ts @@ -930,7 +930,12 @@ export function checkContentLaneDeliverable( issueTitle?: string, ): ContentLaneDeliverableCheck { const matchesSpec = (candidate: string): boolean => spec.entryFilePattern.test(candidate) || (spec.providerFilePattern?.test(candidate) ?? false); - const mentionedPath = extractPathTokens(issueText).find(matchesSpec); + // #9667: entryFilePattern/providerFilePattern are compiled by globToRegExp against a CANONICALIZED path (no `i` + // flag), so they only match lowercase. Canonicalize each issue-body token before testing it, exactly as the + // changedFiles side already does below — otherwise an issue naming `Registry/Subnets/Foo.json` produces no + // mentionedPath and the deliverable gate silently doesn't run. `.find` still returns the ORIGINAL token, so the + // "missing" verdict's mentionedPath stays what the contributor/maintainer see in the issue body. + const mentionedPath = extractPathTokens(issueText).find((token) => matchesSpec(canonicalize(token))); const titleImplies = !mentionedPath && Boolean(issueTitle && spec.issueTitleImpliesEntryPattern?.test(issueTitle)); if (!mentionedPath && !titleImplies) return { verdict: "not-applicable" }; const delivered = changedFiles.some((file) => matchesSpec(canonicalize(file))); diff --git a/test/unit/content-lane-registry-logic.test.ts b/test/unit/content-lane-registry-logic.test.ts index f017c0798b..b8ac3001e9 100644 --- a/test/unit/content-lane-registry-logic.test.ts +++ b/test/unit/content-lane-registry-logic.test.ts @@ -676,6 +676,32 @@ describe("checkContentLaneDeliverable (generic, spec-driven — #content-lane-de expect(checkContentLaneDeliverable(entryOnlySpec, issueText, ["tests/foo.test.mjs"]).verdict).toBe("missing"); }); + it("REGRESSION (#9667): is 'missing' for a MIXED-CASE issue-body path, preserving the original case in mentionedPath", () => { + // The spec patterns are compiled against a canonicalized (lowercased) path, so before the fix a mixed-case + // issue path produced no mentionedPath and the gate silently returned not-applicable. + const issueText = "Add the missing surfaces to Registry/Subnets/Foo.json before merging."; + expect(checkContentLaneDeliverable(spec, issueText, ["tests/foo-verify.test.mjs"])).toEqual({ + verdict: "missing", + mentionedPath: "Registry/Subnets/Foo.json", // original case, as the contributor/maintainer see it + }); + }); + + it("REGRESSION (#9667): is 'delivered' for that mixed-case issue body when the PR changes the real lowercase file", () => { + const issueText = "Add the missing surfaces to Registry/Subnets/Foo.json before merging."; + expect(checkContentLaneDeliverable(spec, issueText, ["registry/subnets/foo.json"])).toEqual({ verdict: "delivered" }); + }); + + it("#9667: all-lowercase issue-path behaviour is unchanged (delivered when the file is touched, missing otherwise)", () => { + const issueText = "Add registry/subnets/foo.json."; + expect(checkContentLaneDeliverable(spec, issueText, ["registry/subnets/foo.json"])).toEqual({ verdict: "delivered" }); + expect(checkContentLaneDeliverable(spec, issueText, ["tests/foo.test.mjs"])).toEqual({ verdict: "missing", mentionedPath: "registry/subnets/foo.json" }); + }); + + it("#9667: an issue path matching NO spec pattern still returns not-applicable (the fix does not over-broaden)", () => { + // A mixed-case path that is not a registry entry/provider file must not now spuriously match. + expect(checkContentLaneDeliverable(spec, "See Docs/README.md for details.", ["docs/readme.md"])).toEqual({ verdict: "not-applicable" }); + }); + // #content-lane-deliverable follow-up (metagraphed #7060-class gap): the literal-path scan alone is BLIND // to metagraphed's own ~120 "MCP execute: verify + wire SN*" issues, whose bodies write the registry path // with a generic `` documentation placeholder rather than the real resolved filename — confirmed