diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f122e26c0..d09c9ed01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: outputs: backend: ${{ steps.filter.outputs.backend }} backendConfig: ${{ steps.filter.outputs.backendConfig }} + cocoDev: ${{ steps.filter.outputs.cocoDev }} ui: ${{ steps.filter.outputs.ui }} uiContract: ${{ steps.filter.outputs.uiContract }} mcp: ${{ steps.filter.outputs.mcp }} @@ -160,6 +161,11 @@ jobs: observability: - 'grafana/dashboards/**' - 'prometheus/rules/**' + # #9649: the coco-dev version manifest and the KBS kustomization that must stay in lockstep, plus the + # checker itself. Nothing else in the workflow references k8s/, so this is its own narrow filter. + cocoDev: + - 'k8s/coco-dev/**' + - 'scripts/check-coco-dev-versions*.ts' # The UI's own app code -- triggers the FULL toolchain (lint/typecheck/test/build). # A dependency bump (package.json/package-lock.json) stays here too since it can break the UI # build or types in ways only that full toolchain would catch. @@ -406,6 +412,18 @@ jobs: - name: Re-gate sort-key check if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} run: npm run regate-sort-key:check + # #9649: coco-dev-versions:check keeps k8s/coco-dev/versions.json and the KBS kustomization in lockstep, but + # was wired into test:ci only -- no CI job ran it, and nothing in this workflow references k8s otherwise. + # Gated on the new cocoDev path filter plus the standard push clause every drift step above uses. + - name: Coco-dev versions drift check + if: ${{ github.event_name == 'push' || needs.changes.outputs.cocoDev == 'true' }} + run: npm run coco-dev-versions:check + # #9649: import-specifiers:check (#9221) scans import specifiers across src/scripts/test and every + # packages/* workspace (BUNDLER_ROOTS/NODENEXT_ROOTS), but no CI job ran it -- #9240/#9249 both drifted in + # after it shipped. Gate on every filter covering one of those scanned roots. + - name: Import-specifiers drift check + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.miner == 'true' || needs.changes.outputs.discoveryIndex == 'true' }} + run: npm run import-specifiers:check # #9563: every webhook-owned handler must decide about #9312's redelivery guard out loud. Three had # silently skipped it -- one writing duplicate permanent suppression rows, two spending a second paid diff --git a/src/review/content-lane/registry-logic.ts b/src/review/content-lane/registry-logic.ts index 921b018b2..0c3b6b2cd 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/check-import-specifiers-script.test.ts b/test/unit/check-import-specifiers-script.test.ts index 070af6b07..d5e3447c2 100644 --- a/test/unit/check-import-specifiers-script.test.ts +++ b/test/unit/check-import-specifiers-script.test.ts @@ -131,4 +131,10 @@ describe("check-import-specifiers script", () => { expect(multi.map((v) => v.file)).toEqual(["src/a.ts", "src/z.ts", "src/z.ts"]); expect(multi.map((v) => v.specifier)).toEqual(["./c.js", "./a.js", "./b.js"]); }); + + it("REGRESSION (#9649): the real repo tree has zero import-specifier violations (the guard was wired into test:ci only, so #9240 and #9249 both reached main while it was local-only)", () => { + // No injected listSourceFiles/readFile: runs the real gate against the real src/scripts/test/packages tree, + // matching the real-tree regression guards on validate-no-hand-written-js and coverage-bolt-on-filenames. + expect(findImportSpecifierViolations()).toEqual([]); + }); }); diff --git a/test/unit/content-lane-registry-logic.test.ts b/test/unit/content-lane-registry-logic.test.ts index f017c0798..b8ac3001e 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