fix(mcp): word-boundary linked-issue closing keywords - #1988
Conversation
extractLinkedIssues populates the linkedIssues the MCP sends for eligibility, score preview, and gate prediction. Its regex matched the closing keywords without a word boundary, so a keyword embedded in a longer word captured the trailing number: `hotfix 5` -> [5], `prefixes 12` -> [12], `unclosed 9` -> [9]. Anchor the keyword alternatives with `\b`, matching the two canonical server-side extractors (src/db/repositories.ts and src/signals/engine.ts, which both use `\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)`). The bare `#` branch stays boundary-free so `JSONbored#123` still matches anywhere. Adds a regression test for the embedded-keyword non-matches and the standalone-keyword matches. No issue because issue creation is restricted on this repo; this is a small, self-evident correctness fix in a pure helper with no schema or API change.
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-01 06:42:29 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 5 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
…rence (#2860) extractLinkedIssueNumbers only matched the bare `KEYWORD #N` closing form, so GitHub`s other documented auto-close syntax -- the fully-qualified `KEYWORD owner/repo#N` (routinely emitted by Renovate/Dependabot and by contributors pasting a qualified reference) -- was silently dropped. A PR whose body says `Fixes myorg/myrepo#42` was therefore scored as having NO linked issue: it wrongly tripped the missing_linked_issue preflight finding and fed slop a false hasLinkedIssue=false, penalizing a properly-linked PR. Match the qualified form too, repo-scoped: count `owner/repo#N` only when owner/repo case-insensitively equals this repo, so a cross-repo reference (which closes an issue elsewhere) never spoofs a same-repo link. The bare form and the #1988 word-boundary invariant are unchanged.
The linked-issue detector that populates the stored pr.linkedIssues field (what the actual gate-close disposition reads) only matched GitHub's bare `Closes #123` closing-keyword form, not the equally valid, fully-qualified `Closes owner/repo#123` form. A PR whose only closing reference used the qualified form was scored "missing linked issue" and closed under the linked-issue-required policy, even though it correctly referenced a real, open issue. A separate, already-correct implementation of the same qualified-form matching existed in signals/engine.ts (added for #1988), but was only used for pre-open preflight planning, not the post-open gate-evaluation path that actually decides to close a PR -- the two implementations had drifted apart. Consolidated to one: db/repositories.ts now owns the canonical extractLinkedIssueNumbers/extractLinkedIssueNumbersWithOverflow, extended to accept a repoFullName and match owner/repo#N only when owner/repo case-insensitively equals the PR's own repo (a reference to a different repo closes an issue there, not here). engine.ts's local duplicate is removed in favor of importing the canonical version.
…ax (#3880) * fix(signals): recognize the qualified owner/repo#N closing-issue syntax The linked-issue detector that populates the stored pr.linkedIssues field (what the actual gate-close disposition reads) only matched GitHub's bare `Closes #123` closing-keyword form, not the equally valid, fully-qualified `Closes owner/repo#123` form. A PR whose only closing reference used the qualified form was scored "missing linked issue" and closed under the linked-issue-required policy, even though it correctly referenced a real, open issue. A separate, already-correct implementation of the same qualified-form matching existed in signals/engine.ts (added for #1988), but was only used for pre-open preflight planning, not the post-open gate-evaluation path that actually decides to close a PR -- the two implementations had drifted apart. Consolidated to one: db/repositories.ts now owns the canonical extractLinkedIssueNumbers/extractLinkedIssueNumbersWithOverflow, extended to accept a repoFullName and match owner/repo#N only when owner/repo case-insensitively equals the PR's own repo (a reference to a different repo closes an issue there, not here). engine.ts's local duplicate is removed in favor of importing the canonical version. * test(github): cover the nullish body fallback in unlinkedPullRequests counting
Summary
extractLinkedIssuesinpackages/gittensory-mcp/lib/local-branch.jsextracts GitHub closing-keyword issue references from the branch name, title, body, and commit messages to populatelinkedIssues— which the MCP sends to the API for eligibility, score preview, and gate prediction. Its regex matched the closing keywords with no word boundary:/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|#)\s*#?(\d+)/giso a keyword embedded in a longer word matched and captured the trailing number:
A branch like
hotfix-cache 5or a commitpostfix 3would spuriously link an issue, feeding a false linked-issue signal into the local preflight.Fix: anchor the keyword alternatives with
\b, matching the two canonical server-side extractors that already do this —src/db/repositories.ts:5718andsrc/signals/engine.ts:5241both use/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)…/. The MCP client-side extractor was the outlier that dropped the\b. The bare#branch stays boundary-free so#123still matches anywhere. Pure helper — no schema or API change.No issue because issue creation is restricted on this repo for outside accounts; this is a small, self-evident correctness fix.
Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coverage(see note below)npm run test:workersnpm run build:mcpnpm run test:mcp-pack(see note below)npm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderateIf any required check was skipped, explain why:
packages/gittensory-mcp/lib/local-branch.js), which is plain JS outside Codecov'ssrc/**/*.tsinclude, so it carries no patch-coverage obligation; a regression test was still added intest/unit/local-branch.test.ts(imported via the existing dynamic-import harness) and passes.build:mcp(the package'snode --check) passes andtypecheckis clean. I did not run the fulltest:coverage/ UI / workers steps because they are unrelated to this MCP-only diff, and I could not runtest:mcp-packlocally because this machine runs Node 24 while the repo pins Node 22 (.nvmrc);scripts/check-mcp-package.mjsthrowsERR_INVALID_ARG_TYPEon Node 24 identically on unmodifiedmain. CI runs the fullvalidatematrix on the pinned Node.Safety
UI Evidencesection below. — N/A: no visible UI change.Notes
test/unit/local-branch.test.tsasserts standalone keywords still link (fixes #5,Closes 12 and resolves #34,see #7,closes#3) while embedded-keyword substrings no longer do (hotfix 5,prefixes 12,unclosed 9).