← board

A [[wikilink]] to a ticket that does not exist is never detected

Measured

Restricted to links matching this repo's ticket slug conventions (bug-, feature-, decide-, chore-, meta-, regression-, compat-, idea-, tstate-), so memory-namespace links (project_*, feedback_*), devdocs filenames and session notes are excluded:

distinct targets resolving to no ticket 52
total references 71
of those, cited by a live (non-done) ticket 13

The first count I took was 252, because I matched "not a ticket slug" rather than "a link that was meant to be a ticket" -- [[...]] here spans several namespaces. The narrowing is recorded because a sweep built on the wide number would have been mostly noise, and the wide number looked more alarming.

Two different defects wearing one shape

The second is why this is worth doing.

Not exhaustively classified. I spot-checked five of the thirteen live ones. Which bucket each falls into is the sweep's job, and the distinction matters because a rename wants a link fixed while a never-filed wants a ticket written.

Why it is not detected today

tools/progress.sh check validates board state -- working/ locks, stale boards, dead commit citations -- but nothing resolves [[...]] targets. The existing DEAD-COMMIT check is the exact precedent: it exists because a citation pointing at nothing is worth catching, and it covers only shas.

Mechanism, with a worked example that is mine

These are written from memory of what a ticket is about rather than from its filename. Mine, filed yesterday: I cited a Track T ticket as ...-a-skipped-lib-test-job-reports-green-and-manufactures-a-false-last-good when the ticket I had just written that day is bug-t-a-skipped-job-is-passlike-so-it-becomes-a-false-last-good. Both describe the same defect; only one is a filename. Fixed in the same commit as this ticket.

That is why the count is 52 rather than a handful -- the failure needs no carelessness, only a descriptive slug and a writer who remembers the subject.

Fix sketch

A check rule beside DEAD-COMMIT: resolve every [[target]] under devdocs/progress/** against the set of ticket basenames, ignoring the project_* / feedback_* memory namespaces and devdocs filenames, and report the unresolved ones -- loudest for links in live tickets, since a dead trail in done/ is history and a dead trail in backlog/ is a live obstacle.

Worth deciding rather than guessing: whether unresolved links should fail check or only warn. The 52 existing ones argue for warn-first, or the rule lands red on day one and gets ignored, which is the usual fate of a check that starts failing.

Gate

Track T's own: the quick tier green, plus the new rule reporting a known-dangling link and staying silent on a resolving one -- negative-controlled in both directions, because a checker that reports nothing looks identical to a clean board.


2026-08-30 (frankD) — the check SHIPPED, with half of this ticket's fix sketch

DANGLING-LINK landed in 5944ee686. This ticket stayed open, correctly: the fix sketch above prescribes two exclusions and the implementation has one.

resolve every [[target]] … against the set of ticket basenames, ignoring the project_* / feedback_* memory namespaces and devdocs filenames

tools/progress.py:1781 excludes self._doc_basenames — devdocs, the second half. The memory-namespace half is absent, and the extractor is \[\[([a-z0-9][a-z0-9_-]{6,})\]\], whose _ admits the memory namespace by construction.

Measured over the ranked folders, same run:

extractor names tickets
[a-z0-9][a-z0-9_-]{6,} — as shipped 40 25
SLUGISH — already defined at progress.py:1664 12 6
ticket-prefix, as this ticket specifies 11 6

The strict instrument is in the same method, 117 lines up. SLUGISH = [a-z0-9]+(?:-[a-z0-9]+){3,} is hyphen-only, so it excludes project_* for free, and the PARK-CONDITION-REWRITTEN scan four lines above the dangling loop uses it. The dangling loop reaches past it for a looser regex. That is not a missing exclusion so much as an available instrument declined at the point of use — the shape devdocs/dev/README.md §4 calls the section stating a rule being the section breaking it.

Do not simply switch to SLUGISH: it needs four hyphen groups, so it drops feature-nilpy-dataclasses (a real, and as it turns out renamed, ticket). The prefix filter this ticket specifies is the right one, and the prefix vocabulary should be derived from the board rather than hand-listed — ls */*.md | sed 's/-.*//' | sort -u gives it, and a hand-list written today would have missed refactor- (37 tickets), which owns one of the eleven.

Scale of the false positive, because it argues against warn-first being enough. project_* is not a handful of typos: 270 references, 129 distinct names, 164 of them in done/. Nothing in the tree explains the convention except feature-dynamic-compiler-tables:149"see [[project_dynamic_compiler_arrays_pattern.md]] in agent memory" — and a devdocs/developer/historic/ handover heading "Related memory:". Zero project_* files have ever existed in this repo, in any commit.

So the check's remedy line is the dangerous part, not the count. It reads "Fix the slug or delete the link; do not leave it to be re-counted" — an instruction that, followed on the six tickets it currently flags, deletes references into a second namespace, and followed board-wide deletes 270 of them. A false positive that merely wastes a reader is cheap. This one is phrased as an imperative, and the imperative is destructive. Fix the extractor before anyone acts on the advice.

One clause in the message is also false against its own code. It says the links are "near a blocking phrase". They are not: the loop filling dangling scans the whole body, deliberately, and the comment directly above it says so in capitals. The message claims a bound the code declines to apply, which makes each finding read as more load-bearing than it is — and a bound is exactly the thing a reader cannot check from the output.

What the sweep found once the noise was removed

Ran the prefix instrument and judged all eleven by hand. Your two buckets both exist, and there is a third you did not name:

All four board fixes are landed. The six findings the check still reports are, every one of them, project_*.

Gate note

Your gate line asks for negative controls in both directions. Add a third: a project_* link must produce no finding. It is the case that shipped broken, and a checker that is silent on a clean board looks identical to one that is silent on the wrong namespace.


2026-08-30 (frankD, second pass) — the fixed check reports 0, and four real ones sit where it does not look

8dc8fa5cb fixed the namespace and the remedy line. The board then reported 0 DANGLING-LINK, and that 0 is genuine for the folders the rule visits.

It visits three. progress.py:1672if t.status not in ("unfinished", "blocked", "working"): continue. The rule was built inside the STALE-PARK family, where a resume condition is load-bearing, and it inherited that family's aperture. But this ticket's own defect is not about parks: a dangling link is wrong wherever it sits, which is why the scan inside that loop deliberately reads the whole body rather than the neighbourhood of a blocking phrase. The inner aperture was widened and the outer one was not.

Running the prefix instrument over all ranked folders: 0 visible, 4 invisible, every one in backlog/.

citing ticket dangling name verdict
feature-p-assertions-directive-and-position bug-p-assert-does-not-raise-eassertionfailed renamecompat-pascal-assert-halts-instead-of-raising-eassertionfailed, in done/
compat-pascal-four-type-sizes-… compat-pascal-subrange-storage-size merged into the citing ticket
feature-c-csmith-differential-fuzzing bug-c-bitfield-packing-sizeof-vs-gcc never filed
feature-demo-songformatter-pxx-target bug-pascal-subclass-inherited-members unresolvable — left dangling on purpose, see below

Two of those four are the very examples this ticket uses to define its buckets. The spot-checks at the top name bug-p-assert-… as a rename and compat-pascal-subrange-storage-size as never-filed. Both were still dangling tonight, and one of the two classifications was wrong. The check reported a clean board while its own specification's worked examples were unfixed and one was mis-bucketed — which is the same shape as the namespace bug: the instrument agreed with the hypothesis that built it.

A fifth outcome: MERGED INTO THE CITING TICKET

compat-pascal-subrange-storage-size was not renamed and not never-filed. It was absorbed into compat-pascal-four-type-sizes-disagree-with-fpc-and-every-value-agrees, which still carries *(was compat-pascal-subrange-storage-size, prio 22)* at the head of the section that used to be it. The absorbed ticket's citations came with it, so the merged document was citing itself as a separate dependency to coordinate with"Do it together with compat-pascal-subrange-storage-size: they are the same job", six lines above the section that IS it.

This is why the spot-check above put it in the wrong bucket, and the reason generalises: a merged ticket is indistinguishable from a never-filed one by resolving its name. The evidence is inside the citing file, not in the board. It is the only outcome where the answer is already in the reader's hands and the link is what stops them looking. A merge that keeps the absorbed ticket's citations manufactures one dangling link per citation, all pointing at a section of the document doing the citing.

So the buckets are five, and only two of them are fixed by resolving a name: rename · never filed · never filed but already delivered · merged into the citing ticket · never was a ticket (a method maxim in brackets).

One left dangling deliberately

bug-pascal-subclass-inherited-members is cited four times by feature-demo-songformatter-pxx-target, called "on the critical path for two modules" and given a "prio-60 filing" it does not have. The nearest candidate, bug-a-nilpy-subclass-overlays-parent-layout [A p70], is done and covers the layout arm alone, where the prose names four arms. Re-pointing would silently mark three arms resolved, so the link stays and carries a block quote saying what is and is not known. Counter ships as a mode flag because of it and feature-nilpy-configparser is blocked in practice by it, so this is worth an owning-lane judgement rather than a slug edit.

Suggested, not done — the aperture

Widening the folder filter for the dangling half is a one-line change in Track T's file and is not mine to make. Note it needs a decision, not just a wider tuple: done/ holds most of the board and a dead trail there is history, exactly as this ticket's fix sketch says. urgent/, backlog/, backlog_new/ are the ones where a dangling link is a live obstacle. Negative control for it: a link that dangles in backlog/ must produce a finding — it did not tonight, four times.

Deprioritised 2026-09-02 — the Track T tooling backlog was cut as a pile

This ticket is not being called wrong. It was moved as part of a pile, not judged individually, and nothing here disputes its finding.

Owner decision. 73 of the 74 open track: T tickets were filed between 2026-08-31 and 2026-09-02, 58 on one day. The pile was too large to work through and returned almost nothing, and a ticket nobody will fix does not sit neutrally — it stays in the ranker forever at zero value, which is the argument CLAUDE.md already makes for a terminal folder over a low prio.

Four were kept in the ranker on a purely structural test — an active umbrella or a hard blocked-by: edge from live work: umbrella-one-full-tier-run-with-no-red-tier, feature-t-freebsd-image-and-runner, and the two regression-test-core-* reds that block the umbrella.

Kept, not deleted, for two reasons: so the finding is not rediscovered and refiled from scratch by the next agent who trips over it, and so it can be pulled back if what it touches becomes load-bearing.

To revive it: move it to the owning lane's backlog, set status: backlog, and say in the ticket WHAT CHANGED to make it matter now. Restoring it because it reads well is how the pile comes back.