Skip to content

fix(issue-ref): read a bare #1234 only when the number stands alone - #1392

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev2/fix/1391-issue-ref-false-positives
Oct 27, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev2/fix/1391-issue-ref-false-positives

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

fix(issue-ref): read a bare #1234 only when the number stands alone

Fixes #1391
Owner: Dev2
Reviewer: Reviewer
Written-by: Dev-2
Tip SHA: 0a0887327804685aae28a977fde7a26d0833f894
Base: 10884b8b (main, #1385)
Tree: cfee808d8b6e9deeac63b16c54abb5b8ea92274c

What changed

core/src/shared/issue-ref.js guarded only the character after the digits (#(\d{1,7})(?![0-9])), so a # glued to anything counted as a reference: #3366ff → #3366, C#7 → #7, 1.2#3 → #3, abc#12 → #12, #1360abc → #1360, and a URL fragment help/#123 → #123. A group line carrying one of those dispatched a card for an issue nobody opened, and groups.js empties speakers once a card is written, so the line got no answer.

The matcher now keeps three separate shapes.

  • A bare #N counts only with a separator in front — (?<![\w./-]), so a word character, ., - or / glues it to something else — and a non-word character behind ((?![\w]), which also refuses #1360abc).
  • owner/repo#N keeps its own branch and is tried first, so the separator above cannot swallow a repository-qualified mention (# there follows o, which is exactly the shape a lookbehind on the bare branch would kill).
  • github.com/<owner>/<repo>/(issues|pull)/<N> stays its own branch and ends its number the same way.

MENTION (cardMentions, and withoutMentions in work-dispatch.js) follows the same three shapes, so a card title that carries #3366ff names no issue either. namesIssue, cardNamesIssue and issueCardForTask keep their signatures; the dispatch protocol and the card it writes are unchanged; what the dispatch reads is now the number the matcher returns.

The three shapes that must keep resolving are pinned in the new test and are green in both halves: bare #1360, owner/repo#1360 (repository-qualified, tried before the bare branch), and https://github.com/o/r/issues/1360.

core/src/host/work-dispatch.js took the id from the raw match ('#' + (match[1] ?? match[2])) while the matcher now keeps the number in branch groups 1/4/7, so @Dev-2 rebase owner/repo#1360 named #owner and a GitHub issue URL named #undefined; cardNamesIssue refuses a non-numeric id, so a second dispatch on the same issue wrote a second card. Both issueRefs and workDispatch now call one refOf(match) helper, and the test pins #1360 for both shapes plus one card for two dispatches.

Deliberate behaviour change — a URL fragment stops parsing, and so does the tail of a #55/#198 shorthand. …/help/#123 used to be read as #123 and is now not a reference at all, because # follows / there and / is one of the gluing characters. The same boundary ends the copy-editing shorthand the repo itself writes: in #55/#198 only #55 is read now (it named both before), because that # also follows /; the spaced form is untouched — '#876 / #892' → ["#876","#892"] — and so is '#55, #198' → ["#55","#198"], so write #55, #198 where two ids must both be read from a card title or a dispatch line. Dependency search on base 0542cfa: git grep -n -I -E '/#[0-9]' → 67 lines, of which 56 are that glued #N/#N shorthand (14 .github/**, 14 docs/**, 12 plugins/**, 4 core/src, 3 core/test, 2 root *.md, plus 7 more in the generated patches/ + ds-bot/ mirrors) and 11 are JS regex literals inside assertions that each name one id (/#812/, /#3B5BFD/, /#1281/, …), all still parsed; no line in that output is a …/#<n> URL fragment reference. Two readers, not one: .github/** never imports issue-ref.js — enqueue-rules.mjs carries its own const ISSUE_REF = /^[\s>#-]*(?:fixes|closes)…/im (:30), and merge-gate.mjs / enqueue-guard.mjs do the same — so a Related #n line is not a closing ref under that rule either way, it is never this matcher's call, and this PR changes nothing under .github/. No test imports core/src/shared/issue-ref.js; the only consumer under test is core/test/work-dispatch.test.js, which imports workDispatch and asserts that a bare #1234 still parses (65 lines there carry a #[0-9], 43 of them #1234). That file contains no /#-glued shape, and no test in core/test asserts a URL-fragment or glued reference — the closest hits are a comment (approval-binding.test.js:5, #49/#48) and colour literals (branding.test.js:40-41, /#3B5BFD/). So nothing pins the old glued behaviour; the bare form stays pinned as parsing. The prose that does use the shorthand — docs/subsystems/core-host.md:267 (#55/#198), docs/autonomous-dev.md:255 (#1094/#1095/#1108/#1115), docs/subsystems/core-client.md:145 (#51/#149/#55/#57), CHANGELOG.md:389 (#499/#504), AGENTS.md:192 (#711/#713) — is not read by the matcher, so it changes no behaviour. External callers are unknown; the returned set only gets stricter, and a caller that needs a fragment read out of a URL can read new URL(href).hash itself.

Red first, same command in both halves

cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/issue-ref.test.js

Half (a): this tree with the new test but the old issue-ref.js (base 0542cfa):

not ok 1 - a hex colour is not an issue reference
  error: |-
    @Dev-2 set the accent colour to #3366ff
    + actual - expected
    + [ '#3366' ]
    - []
not ok 2 - a language version is not an issue reference          C#7       -> [ '#7' ]
not ok 3 - a version string is not an issue reference            1.2#3     -> [ '#3' ]
not ok 4 - an identifier is not an issue reference               abc#12    -> [ '#12' ]
not ok 5 - a number running into letters is not an issue ref     #1360abc  -> [ '#1360' ]
not ok 6 - a URL fragment is not an issue reference              help/#123 -> [ '#123' ]
not ok 14 - namesIssue answers the same question, so a colour cannot name an issue
not ok 15 - a card whose title only carries a colour names no issue
# tests 15
# pass 7
# fail 8

Half (b): the same command on this tip — the same 8 green, plus the 7 that must keep working:

ok 7 - a bare number still names the issue: #1360
ok 8 - a repository-qualified number still names the issue: owner/repo#1360
ok 9 - a github issue URL still names the issue: https://github.com/o/r/issues/1360
ok 10 - a github pull URL still names the issue: https://github.com/o/r/pull/1360
ok 11 - a number after a Bot mention still names the issue: @Dev-2 rebase #1360
ok 12 - a number in a sentence still names the issue: please rebase #1360 onto main
ok 13 - numbers in brackets and punctuation still names the issue: (#1360), #1361.
# tests 15
# pass 15
# fail 0

The dispatch consumer, same command, two states

cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/work-dispatch.test.js

At this tip (0a088732): 19/19 green. With core/src/shared/issue-ref.js and core/src/host/work-dispatch.js taken back to the reviewed tip ccb0c521 — the new test kept — the same command is 17 pass / 2 fail: the pull link fails '#undefined' !== '#1234', the repository-qualified mention on the next line reads #owner (its own assertion, '#owner' !== '#1234', is reached once the pull link passes), and the two-dispatch case fails 2 !== 1.

not ok 1 - the dispatch shape is an issue id and an imperative, never any @ (#1254)
  error: |-
    a pull link names its number, not undefined
    + '#undefined'
    - '#1234'
not ok 4 - two dispatches on one issue through a qualified or URL id write one card (#1254, #1331)
  error: |-
    the same issue through a URL id reuses that card, so the id is the number
    2 !== 1
# tests 19
# pass 17
# fail 2

Knife

Removing only the bare branch's left separator (keeping the right-hand rule) on this same tree: 5 red / 10 green — C#7, 1.2#3, abc#12, help/#123 and the namesIssue case go red, while #3366ff and #1360abc stay green. Each half of the rule carries a different class of false positive, so the test is bound to the fix rather than to the shape of an assertion. Restored: 15/15 green.

Counts at this base

Core at this tip (0a088732): 1822/1822; at base 10884b8b: 1805/1805 — the 17 added tests are 15 in issue-ref.test.js and 2 in work-dispatch.test.js. The other suites were not re-run for this body, so their counts are the 0542cfa readings: social 525, tree 125, timeline 125, workflow 27, .github/scripts 543, interop 37, live-e2e 24.

What I ran

  • TZ=UTC npm run verify at this tree (cfee808d8b6e9deeac63b16c54abb5b8ea92274c, base 10884b8b): exit 0; arch-docs: OK (8 changed file(s)).
  • node core/scripts/build-client.mjs --check: exit 0.
  • node scripts/ds-bot-patches.mjs check: three ok (ds-bot/ = the pin, 10 patches apply, core/ = ds-bot/ + patches/ds-bot/); the four patches/ds-bot/files/** mirrors (src/shared/issue-ref.js, src/host/work-dispatch.js, test/issue-ref.test.js, test/work-dispatch.test.js) come from generate, never by hand.
  • The red/green pair above, and the knife.

What I did not run

  • core/dev/bench.sh smoke — this changes a pure function in src/shared/ and touches neither index.js nor src/host/; the DSH harness mount is also read-only in this sandbox ([EROFS] EROFS: read-only file system, the same reason as feat(tasks): post the room notice's one-line claim cue #1288).
  • Live rooms and a real Bot round.

Docs-not-needed: this is the matcher's byte boundary, not a new mechanism — core-host.md describes issue-ref.js's API and that a dispatch names an issue/PR id, and that stays true; the return set of issueRefs / cardMentions is only stricter at the edges (the URL-fragment change is the one deliberate one, written out above), so no caller has to change and no subsystem description moved.

.github/** is untouched.

Checkpoint

{"pr":1392,"claim":"Dev2","issue":1391,"summary":"`issue-ref.js` read a bare `#N` with only the right-hand character guarded, so a `#` glued to a colour, a language version, a version string, an identifier or a URL fragment dispatched a card for a phantom issue. The three shapes are now separate: a bare number needs a separator in front and a non-word character behind, `owner/repo#N` is tried first so the separator cannot swallow it, and the github.com issues/pull branch ends its number the same way; `MENTION` follows all three. A URL fragment (`help/#123`) deliberately stops parsing.","files":["core/src/shared/issue-ref.js","core/src/host/work-dispatch.js","core/test/issue-ref.test.js","core/test/work-dispatch.test.js","patches/ds-bot/files/src/shared/issue-ref.js","patches/ds-bot/files/src/host/work-dispatch.js","patches/ds-bot/files/test/issue-ref.test.js","patches/ds-bot/files/test/work-dispatch.test.js"],"checks":{"ran":"TZ=UTC npm run verify; node core/scripts/build-client.mjs --check; node scripts/ds-bot-patches.mjs check","result":"green","at":"0a0887327804685aae28a977fde7a26d0833f894"},"next":"none — ready for review at 0a088732. issue-ref red 8/15 then green 15/15; work-dispatch 19/19 at the tip and 2 red with the two sources back at ccb0c521; verify exit 0 at this tree."}

@HugoXK

HugoXK commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Self-test: INC at ccb0c52
Written-by: Test

Head read before this comment: ccb0c521c5a3be28a4996630618221c457825809 (base 0542cfa, single parent, 4 files +130/−6). The body was edited while I was measuring (the URL-fragment / dependency paragraph is new), so everything below quotes the new text; I do not vouch for the old wording.

The matcher does what the body says

issueRefs, on this tree:

@Dev-2 set the accent colour to #3366ff   []
fix C#7 codegen                           []
version in 1.2#3                          []
abc#12                                    []
#1360abc                                  []
help/#123                                 []
#1360                                     ["#1360"]
owner/repo#1360                           ["#1360"]
https://github.com/o/r/issues/1360        ["#1360"]
(#1360)                                   ["#1360"]
#1360,                                    ["#1360"]
**#1360**                                 ["#1360"]
see #1360.                                ["#1360"]
rebase #1360 and push                     ["#1360"]
#55/#198                                  ["#55"]

So the body's new sentence holds: in the copy-editing shorthand #55/#198 only #55 is read now.

What it breaks: the dispatch layer in front of it

workDispatch reads the number by group index — core/src/host/work-dispatch.js:32, const issue = '#' + (match[1] ?? match[2]). The new regex moved the numbers to groups 1 / 4 / 7, and that line was not moved with it:

line base 0542cfa this tip
@Dev-2 rebase #1360 #1360 #1360
@Dev-2 rebase owner/repo#1360 #1360 #owner
@Dev-2 rebase https://github.com/o/r/issues/1360 #1360 #undefined

assignFromDispatch finds the existing card with cardNamesIssue(task, issue), and namesIssue refuses a non-numeric id, so the reuse path never fires. Driven through group_relay on a real room, two dispatches on the same issue:

            base 0542cfa   this tip
bare        cards 1        cards 1
qualified   cards 1        cards 2
url         cards 1        cards 2

One issue growing two cards is the invariant #1254/#1331 exists to keep, on a shape the body pins as one that must keep resolving. The existing case does not catch it because core/test/work-dispatch.test.js:134 asserts only truthiness — assert.ok(workDispatch('@Dev1 https://github.com/o/r/pull/1234 land it', …)) — and '#undefined' is truthy.

End to end, the lines that were asked for

Same harness as work-dispatch.test.js (a bot named Dev-2), one group_relay round each:

  • @Dev-2 set the accent colour to #3366ff → cards 0, and the line still gets its answer: a member Session is opened for Dev-2, because groups.js:826 empties speakers only when a card was actually written.
  • @Dev-2 rebase #1360 → cards 1, title: 'rebase #1360', owner Dev-2, and the relay returns {replies: [], passed: []} — no group part.
  • @Dev-2 rebase help/#123 → cards 0, still answered.

The body's greps reproduce

At base 0542cfa, git grep -n -I -E '/#[0-9]' → 67 lines: 56 #N/#N shorthand and 11 JS regex literals (/#812/, /#3B5BFD/, /#1281/, /#123456/, /#909/ ×2, …). Of the 56, 19 sit in test/fixture files: 13 comments (one of them trailing — enqueue-guard.test.mjs:536), 6 test('…') names. None of the 19 is matcher input; no fixture, card title or dispatch string carries the shape. Head adds exactly 6 lines (3 in core/, mirrored into patches/ds-bot/files/) → 73, and they are the new comment and the two fragment assertions. The only matcher inputs remain work-dispatch.js's dispatch line and cardMentions over title / target.href.

Ran: core/test/issue-ref.test.js 15/15 pass; core/test/work-dispatch.test.js 17/17 pass (the second passes despite the table above — see the truthiness line); the probes above, kept outside the tree. Not the full verify, as asked. Nothing deployed, nothing live.

as of my read: 0 comments, newest none

@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Verdict: FAIL at ccb0c52
Written-by: Reviewer2
Attestation: ccb0c52 role=Reviewer2

Ran:

  • cd core && DSH_HARNESS=/data/har020 TZ=UTC node --import ./test/harness-resolve.mjs --test test/issue-ref.test.js at the tip: # pass 15 / # fail 0.
  • same command with core/src/shared/issue-ref.js swapped for the base 0542cfac copy (test kept): # pass 7 / # fail 8.
  • same command with only the bare branch's left separator removed from both patterns: # pass 10 / # fail 5; file then restored byte-for-byte from the tip and cmp against patches/ds-bot/files/src/shared/issue-ref.js is clean.
  • node scripts/ds-bot-patches.mjs check and node scripts/ds-bot-patches.mjs generate.
  • a probe that imports core/src/host/work-dispatch.js from the tip tree and from the base tree and prints issue for three dispatch lines.

Blocking: workDispatch() still reads the old group numbers

ISSUE_REF gained a branch, so its groups moved — bare is 1, owner/repo#N is 2/3/4, the github.com URL is 5/6/7 — and issueRefs() was updated to match[1] ?? match[4] ?? match[7]. Its only other consumer was not:

core/src/host/work-dispatch.js:32

const issue = `#${match[1] ?? match[2]}`

The same probe at both tips (a dispatch line as a task lead writes it):

tip ccb0c521:

"@Dev1 fix https://github.com/o/r/issues/1234" -> {"issue":"#undefined","title":"fix https://github.com/o/r/issues/1234"}
"@Dev1 owner/repo#1234 rebase and push"        -> {"issue":"#owner","title":"owner/repo#1234 rebase and push"}
"@Dev1 #1234 rebase and push"                  -> {"issue":"#1234","title":"#1234 rebase and push"}

base 0542cfac: all three -> "#1234".

assignFromDispatch looks for the member's own open card with cardNamesIssue(task, issue) (core/src/host/tasks.js:520-522), and namesIssue requires digits (/^\d+$/), so #undefined and #owner match no card: every dispatch line that names a pull URL or an owner/repo#N writes a second card instead of reusing the one for that issue — the duplicate/unattached-card shape this PR exists to remove, moved from the matcher to the dispatch.

Why CI is green anyway: core/test/work-dispatch.test.js:134 pins only truthiness for the URL shape —

assert.ok(workDispatch('@Dev1 https://github.com/o/r/pull/1234 land it', { callees: [one[0]] }), 'a pull link is an id too')

— and the new issue-ref.test.js exercises issueRefs / namesIssue / cardMentions, never workDispatch. The fix is match[1] ?? match[4] ?? match[7] (or one shared helper both callers use) plus an assertion on issue for the URL and owner/repo#N shapes, so a later group renumbering cannot pass silently again.

What holds

  • Red without the change. Base issue-ref.js + the new test at this tip: # tests 15 / # pass 7 / # fail 8, and the eight are the intended ones — test 1 actual: ['#3366'], 2 ['#7'], 3 ['#3'], 4 ['#12'], 5 ['#1360'], 6 ['#123'], plus 14 (namesIssue) and 15 (cardMentions). With the tip file: # pass 15 / # fail 0.
  • Both halves are load-bearing. Removing only the left separator (keeping the right-hand rule) gives # pass 10 / # fail 5 — exactly tests 2, 3, 4, 6 and 14 — while #3366ff and #1360abc stay green. Each half kills a different class of false positive, so the test is bound to the fix and not to one assertion shape.
  • Mirrors. Both patches/ds-bot/files/** files are byte-identical to their core/ counterparts (cmp clean), and check prints:
skipped pin (no git): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
  • generate was not run, and the verdict does not depend on it: this box has no git and the script refuses itself — ds-bot-patches: generate needs git: it asks git for the diff of ds-bot/ against core/. Without git, edit core/ and patches/ds-bot/ together and run check. The content proof is check's third line above, plus cmp on both mirrors, plus CI's package job (which does have git) green at this tip.
  • Tip and checks. 4 files +130/−6 on base 0542cfac, 1 commit, base not behind. All 15 checks are completed; 14 success and the single failure is enqueue-guard at 08:49:37Z, which is the expected "no report names the tip" red before any conclusion exists.

Not blocking

  • The deliberate narrowing is documented and, inside this tree, empty: #55/#198 now reads only #55, and a URL fragment …/#123 stops parsing. I grepped every /#<digit> occurrence in JS/MJS/JSON (44 lines, excluding ds-bot/, the files/ mirrors and generated core/client.js): all of them are // comments, test('…') names, regex-literal assertions (/#794/, /#3B5BFD/, /#1281/, …) or this test's own input. None is a card title, a dispatch line or a fixture fed to the matcher, so no in-repo caller changes behaviour. External callers are unknown; a task lead who writes #55/#198 must now write #55, #198, which the body already says.

  • The tip carries no labels ([]), so agent-pr is not on it yet. It is a fork, which the guard exempts, so this neither blocks nor belongs to me.

  • Tier: T3 by .github/agent/risk-rules.json (core/src/shared/**), so this tip needs a PASS before any queue — which this verdict is not.

  • The Self-test: INC at ccb0c521… comment (09:03:07Z) reports the same regression. Its first line is not a Verdict: line, so it is not a conclusion and this FAIL is the first one at this tip; the reading above comes from my own probe at both tips, not from that text.

  • I also measured the consequence instead of inferring it: cardNamesIssue({ title: 'rebase owner/repo#1360', target: { href: '' } }, '#owner') → false and … , '#undefined') → false, while '#1360' → true at both tips — so the reuse lookup misses exactly the values workDispatch now returns.

A fix at work-dispatch.js:32 plus the missing assertion produces a new tip; that tip gets a fresh conclusion, and this one is void with it.

…group positions

The three shapes carry their number in groups 1, 4 and 7, but
`work-dispatch.js` still built the id from `match[1] ?? match[2]`: a
repository-qualified mention named `#owner` and a GitHub issue URL named
`#undefined`. `cardNamesIssue` refuses a non-numeric id, so a second dispatch
on the same issue wrote a second card, which is the invariant ThinkFlowLab#1254/ThinkFlowLab#1331
exists to keep.

`issue-ref.js` now exports `refOf(match)` and both `issueRefs` and
`workDispatch` call it, so no reader repeats the group numbers. The new tests
pin the exact id for a bare, a repository-qualified and a URL mention, refuse
`#3366ff`, and pin one card for two dispatches on one issue.

Red first, same command: with the old source and these tests,
`work-dispatch.test.js` is 2 red / 19 (`'#owner' !== 'ThinkFlowLab#1234'`, `2 !== 1`);
with the fix it is 19/19 green. `issue-ref.test.js` stays 15/15.

Docs-not-needed: this is the matcher's byte boundary and one internal helper extraction — `core-host.md` describes `issue-ref.js`'s API and that a dispatch names an issue/PR id, and both stay true; no caller outside this repository changes.
@HugoXK
HugoXK force-pushed the dev2/fix/1391-issue-ref-false-positives branch from ccb0c52 to 0a08873 Compare October 27, 2026 09:15
@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 0a08873
Written-by: Reviewer2
Attestation: 0a08873 role=Reviewer2

Ran:

  • GET /repos/ThinkFlowLab/FlyBot/pulls/1392 — head 0a0887327804685aae28a977fde7a26d0833f894, base main 10884b8b8664f791bcd55f9a62f0b5fd77edfa47, 1 commit, 8 files +186/-14, mergeable: true, no labels.
  • GET /repos/ThinkFlowLab/FlyBot/commits/0a088732…/check-runs?per_page=100 — 14 runs, every one completed: 13 success (tests-ok, package, merge-gate, judge, reviewed tip, risk tier, agent-audience ×2, Node 24, plugins ×4) and enqueue-guard failure at 09:15:41Z, which is the guard refusing a T3 head that carries no PASS yet — this comment is that PASS.
  • Tip tree, both touched suites: cd core && DSH_HARNESS=/data/har020 TZ=UTC node --import ./test/harness-resolve.mjs --test test/issue-ref.test.js test/work-dispatch.test.js → # tests 34 / # pass 34 / # fail 0 (issue-ref 15, work-dispatch 19).
  • Red against base 10884b8b — the tip's two test files dropped into a tree of the base source: node --import ./test/harness-resolve.mjs --test test/work-dispatch.test.js → # pass 17 / # fail 2: a hex colour is not a dispatch (a dispatch came back with issue: '#3366') and a card names an issue through the same number… (cardNamesIssue({title:'accent #3366ff'}, '#3366') → true, expected false); --test test/issue-ref.test.js → # pass 7 / # fail 8 (tests 1–6, 14, 15).
  • Red against the previously judged tip ccb0c521 — the same two test files with that source: test/work-dispatch.test.js → # pass 17 / # fail 2, expected: '#1234' / actual: '#undefined' on the URL assertion and expected: 1 / actual: 2 in two dispatches on one issue through a qualified or URL id write one card; test/issue-ref.test.js → # pass 15 / # fail 0. The two reds this commit closes are exactly the two its tests name, and the lookbehind half still carries its own 15.
  • Probe on the tip source: @Dev1 owner/repo#1234 rebase → #1234; @Dev1 https://github.com/o/r/issues/1234 rebase → #1234; @Dev1 https://github.com/o/r/pull/1234 rebase → #1234; @Dev1 #1234 rebase → #1234; @Dev1 请按 #1254 修一下 → #1254; @Dev1 set the accent colour to #3366ff, fix C#7 codegen, version in 1.2#3, the room abc#12 resolved, #1360abc, see https://example.com/help/#123 → all no dispatch. issueRefs agrees on the same strings (accent #3366ff, C#7, 1.2#3, abc#12, #1360abc, help/#123 → []; owner/repo#1360, the two github URLs, #1360, @Dev-2 rebase #1360 → ['#1360']; (#1360), #1361. → ['#1360','#1361']).
  • cmp for the four mirrors: core/src/host/work-dispatch.js, core/src/shared/issue-ref.js, core/test/issue-ref.test.js, core/test/work-dispatch.test.js are each byte-identical to patches/ds-bot/files/<same path>.
  • node scripts/ds-bot-patches.mjs check in the tip tree → three lines, exit 0: skipped pin (no git): the tree hash cannot be computed here / ok 10 patches apply cleanly in order / ok core/ equals ds-bot/ + patches/ds-bot/. generate was not run: this workstation has no git and the script refuses for that reason (generate needs git: it asks git for the diff of ds-bot/ against core/), so the third line of check together with the cmp above is what stands in for it.

Checked:

  • refOf is the single place an ISSUE_REF match becomes an id: core/src/shared/issue-ref.js:20 (issueRefs) and core/src/host/work-dispatch.js:32 (workDispatch) both call it, and a tree-wide grep for d{1,7} over core/ and plugins/ finds only ISSUE_REF, MENTION and core/src/host/groups.js:49.
  • MENTION's alternation order did not move — qualified 1/2/3, URL 4/5/6, bare 7 — so mentionOf's match[3], match[6], match[7] still read their own shape; cardMentions({title:'owner/repo#1360'}) → [{repo:'owner/repo', number:'1360'}], a colour title → [].
  • core/src/host/groups.js:49 is not a third reader of these groups: it is claimCueText's own /^#(\d{1,7})\b/ over a card title for the room's claim line, reads its own group 1, and this pull request does not touch it.
  • The reuse path the previous tip broke is covered end to end by two dispatches on one issue through a qualified or URL id write one card (#1254, #1331): qualified first, URL second, one card, same id.

Non-blocking:

  • refOf would return #undefined for a match that is not an ISSUE_REF match; every caller today passes one, so no path reaches it.
  • Pre-existing and unchanged here: the qualified shape also reads a/b#123 out of a non-github URL such as https://example.com/a/b#123. Base does the same, so this is not a regression of this pull request.
  • generate was not run on this workstation (no git); the package job, which has git, is green on this head.

@github-actions github-actions Bot added the pass label Oct 27, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 27, 2026
@HugoXK

HugoXK commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Self-test: PASS at 0a08873
Written-by: Test

Head read before this comment: 0a0887327804685aae28a977fde7a26d0833f894 (base main 10884b8b, tree cfee808d, 1 commit, 8 files +186/−14). The review's PASS at this tip is comment 6438305255; this is a self-test record beside it, not a replacement for it, and it authorises no merge. My earlier comment and the FAIL at ccb0c521 name a tip that is no longer this PR's head.

First, the stop condition: one issue, two dispatches

Same harness as work-dispatch.test.js — one room, a bot named Dev-2, one group_relay round per line: @Dev-2 rebase owner/repo#1360, then @Dev-2 https://github.com/o/r/issues/1360 then push. Four trees, same probe:

tree id handed to the board, per line cards
base 0542cfa #1360 / #1360 1
base 10884b8b #1360 / #1360 1
old tip ccb0c521 #owner / #undefined 2
this tip 0a088732 #1360 / #1360 1

On this tip the second dispatch lands on the first card: one record, two "by": "dispatch" actor entries, updatedAt moved (…642 → …651), note is the second line, and the card's title/target name #1360. So both symptoms the old tip had are gone: there is no second card ("did not land"), and nothing hangs under #owner or #undefined ("landed under the wrong id"). The old tip had both, and its second card carried a #1360 title that its own dispatch id could never match.

The dispatch matrix

workDispatch(line, { callees: [{ id: 'b' }] })?.issue, and issueRefs(line) beside it:

@Dev-2 rebase #1360                               #1360    ["#1360"]
@Dev-2 rebase owner/repo#1360                     #1360    ["#1360"]
@Dev-2 rebase https://github.com/o/r/issues/1360  #1360    ["#1360"]
@Dev-2 set the accent colour to #3366ff           none     []
@Dev-2 fix C#7 codegen                            none     []
@Dev-2 bump version in 1.2#3                      none     []
@Dev-2 check abc#12                               none     []
@Dev-2 check #1360abc                             none     []
@Dev-2 look at help/#123                          none     []
@Dev-2 read #55/#198                              #55      ["#55"]
@Dev-2 rebase #876 / #892                         #876     ["#876","#892"]

The card layer

cardNamesIssue on this tree, against base 10884b8b:

{title:'rebase owner/repo#1360'}                         '#1360'  true   (base true)
{title:'rebase https://github.com/o/r/issues/1360 那条'} '#1360'  true   (base true)
{title:'set accent to #3366ff'}                          '#3366'  false  (base TRUE, issue #3366)
{title:'rebase #1360'}                                   '#1360'  true   (base true)

The first two are the regression guard; the third is a real improvement over the base, which handed a colour's digits to the board as an issue.

The fix is one reader: refOf is exported from core/src/shared/issue-ref.js:14 and both issueRefs (calls it at line 20) and workDispatch (core/src/host/work-dispatch.js:32) go through it, so match[1] ?? match[4] ?? match[7] exists once. MENTION's alternation order did not move, so mentionOf still reads its own 3/6/7.

The grep, rerun on this head

git grep -n -I -E '/#[0-9]' at 0a088732 → 73 lines.

  • Relative to ccb0c521's 73: +0. The set is identical apart from core/src/host/room-membership.js:1558 → :1566 (and its patches/ mirror), a line-number shift from the base moving under this branch, not a new hit.
  • Relative to the new base 10884b8b's 67: +6, and each one is accounted for:
core/src/shared/issue-ref.js:7                 comment (the C#7 / 1.2#3 / #3366ff / help/#123 sentence)
core/test/issue-ref.test.js:15                 test data string, asserted NOT to parse
core/test/issue-ref.test.js:46                 assertion argument, asserted NOT to parse
patches/ds-bot/files/src/shared/issue-ref.js:7                mirror of the first
patches/ds-bot/files/test/issue-ref.test.js:15,46             mirrors of the last two

No third class: nothing added is a string a matcher eats. The two test-file lines are inputs to issueRefs/namesIssue assertions that expect []/false.

verify

TZ=UTC npm run verify at the tip: exit 0 (core lint, plugin lint/pack import, client build check, ds-bot-patches check three ok lines, then every suite). Counts: core 1822/1822, plugins/social 525, tree 125, timeline 125, workflow 27, .github/scripts 543, interop 37, live-e2e 24; zero failures anywhere.

One environment note, not a code finding: the first attempt exited 1 inside scripts/lint-plugins.mjs because npm pack writes to ~/.npm/_cacache, which is read-only on this box (EROFS … /home/xuke/.npm/_cacache/tmp/…). Rerun with the npm cache pointed at a writable directory gave the exit 0 above; the failure was the sandbox, identical on any tree.

Ran: core/test/issue-ref.test.js + core/test/work-dispatch.test.js → 34/34 pass (15 + 19); the four dispatch/card probes above, kept outside the tree; git grep on 0542cfa, 10884b8b, ccb0c521, 0a088732; TZ=UTC npm run verify on the tip. No 300-run stress this time, as instructed. Nothing deployed, nothing live.

as of my read: 3 comments, newest 6438305255

Merged via the queue into ThinkFlowLab:main with commit 06bed53 Oct 27, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(dispatch): a # glued to a colour, version or URL fragment is not an issue reference

2 participants