Skip to content

fix(mcp): word-boundary linked-issue closing keywords - #1988

Merged
JSONbored merged 1 commit into
JSONbored:mainfrom
glorydavid03023:fix/mcp-linked-issue-keyword-boundary
Jul 1, 2026
Merged

fix(mcp): word-boundary linked-issue closing keywords#1988
JSONbored merged 1 commit into
JSONbored:mainfrom
glorydavid03023:fix/mcp-linked-issue-keyword-boundary

Conversation

@glorydavid03023

Copy link
Copy Markdown
Contributor

Summary

extractLinkedIssues in packages/gittensory-mcp/lib/local-branch.js extracts GitHub closing-keyword issue references from the branch name, title, body, and commit messages to populate linkedIssues — 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+)/gi

so a keyword embedded in a longer word matched and captured the trailing number:

extractLinkedIssues("hotfix 5")     // => [5]   (should be [])
extractLinkedIssues("prefixes 12")  // => [12]  (should be [])
extractLinkedIssues("unclosed 9")   // => [9]   (should be [])

A branch like hotfix-cache 5 or a commit postfix 3 would 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:5718 and src/signals/engine.ts:5241 both 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 #123 still 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

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked an issue, or this is small enough that the summary explains why an issue is not needed.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage (see note below)
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack (see note below)
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

If any required check was skipped, explain why:

  • The change is confined to the MCP package helper (packages/gittensory-mcp/lib/local-branch.js), which is plain JS outside Codecov's src/**/*.ts include, so it carries no patch-coverage obligation; a regression test was still added in test/unit/local-branch.test.ts (imported via the existing dynamic-import harness) and passes. build:mcp (the package's node --check) passes and typecheck is clean. I did not run the full test:coverage / UI / workers steps because they are unrelated to this MCP-only diff, and I could not run test:mcp-pack locally because this machine runs Node 24 while the repo pins Node 22 (.nvmrc); scripts/check-mcp-package.mjs throws ERR_INVALID_ARG_TYPE on Node 24 identically on unmodified main. CI runs the full validate matrix on the pinned Node.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — N/A: no auth/cookie/CORS/session changes.
  • API/OpenAPI/MCP behavior is updated and tested where needed. (Behavior fix in the MCP local helper; covered by the added regression test. No request/response schema changed.)
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. — N/A: no UI change.
  • Visible UI changes include a UI Evidence section below. — N/A: no visible UI change.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs. (No docs/changelog change; MCP changelog is a release-prep artifact.)

Notes

  • Regression test in test/unit/local-branch.test.ts asserts 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).
  • No UI Evidence section: there is no visible UI, frontend, docs, or extension change.

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.
@loopover-orb

loopover-orb Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Tip

🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩

✅ Gittensory review result - approve/merge recommended

Review updated: 2026-07-01 06:42:29 UTC

2 files · 1 AI reviewer · no blockers · readiness 73/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
The change correctly narrows the MCP linked-issue extractor so closing keywords only match at a word boundary while preserving bare # references. The added regression test covers both the intended standalone keyword cases and the previously false-positive embedded-keyword cases. I do not see a reachable correctness break in the visible diff.

Nits — 5 non-blocking
  • nit: packages/gittensory-mcp/lib/local-branch.js:563 now exports a helper only so the unit test can import it; if this package treats named exports as public API, prefer testing through the existing metadata collection path or document that this helper is intentionally exported.
  • nit: test/unit/local-branch.test.ts:1742 covers helper behavior directly, but it would be stronger to include one end-to-end metadata case proving a branch name or commit message like `hotfix 5` no longer populates `linkedIssues`.
  • packages/gittensory-mcp/lib/local-branch.js:563: either keep `extractLinkedIssues` explicitly public or move the regression through `collectLocalBranchMetadata` so the test exercises the same path the MCP uses.
  • test/unit/local-branch.test.ts:1742: add one collection-level assertion for an embedded-keyword branch or commit message to guard the wiring, not just the regex helper.
  • Readiness score is below the configured threshold — Use the readiness panel as advisory maintainer context; the score does not block this PR.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #5, #34
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (2 linked issues).
Validation posture ❌ 5/25 Preflight is holding this PR; address the blocker before review.
Contributor workload ✅ 10/10 Author activity: 197 registered-repo PR(s), 131 merged, 9 issue(s).
Contributor context ✅ Confirmed Gittensor contributor glorydavid03023; Gittensor profile; 197 PR(s), 9 issue(s).
Gate result ✅ Passing No configured blocker found.
Review context
  • Author: glorydavid03023
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: JavaScript, Python, TypeScript, C++, Kotlin, MDX, Rust
  • Official Gittensor activity: 197 PR(s), 9 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Fix the blocker.
  • Triage stale or unlinked PRs.
  • Refresh registry data or choose a registered active repo.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 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.

  • Re-run Gittensory review

@dosubot dosubot Bot added the lgtm label Jul 1, 2026
@JSONbored
JSONbored merged commit feea878 into JSONbored:main Jul 1, 2026
8 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 1, 2026
@github-actions github-actions Bot mentioned this pull request Jul 1, 2026
12 tasks
loopover-orb Bot pushed a commit that referenced this pull request Jul 4, 2026
…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.
JSONbored added a commit that referenced this pull request Jul 7, 2026
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.
JSONbored added a commit that referenced this pull request Jul 7, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants