From b261719b3231373bc2bd0a6f4cf87207a78dbc46 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:53:25 +0100 Subject: [PATCH 1/3] fix: require a delimiter for bare TODO/FIXME markers (iss-111) The open-questions marker scan grounded evidence/open-questions with a pattern that admitted a bare uppercase TODO or FIXME followed by whitespace or end-of-line. On a repository that documents its own conventions every prose mention of a marker then matched: 18 false positives across this repo's durable record, all documentation about markers, none a real work marker. Split the marker set by trailing boundary: TODO and FIXME now require a trailing ':' or '(' (the conventional TODO: and TODO(alice): spellings, how genuine markers are almost always written); XXX, HACK, and BUG keep their bare-word form, since they are rarely written as bare uppercase words in prose and dropping it would lose real recall for no measured precision gain. This kills 100% of the bare-word documentation false-positive class while preserving every colon-form fixture marker. spc-12's pattern description moves in the same change. The pinning test gains the prose-mention reject cases and a probe-level test proving a repo whose prose merely names markers stays blank. Assisted-by: Claude:claude-opus-4-8 --- ...ifeboat-s-open-questions-on-a-repo-s-to.md | 28 ++++++---- CHANGELOG.md | 14 +++++ internal/core/lifeboat/sources_conventions.go | 56 +++++++++++++++---- .../core/lifeboat/sources_conventions_test.go | 38 ++++++++++++- 4 files changed, 111 insertions(+), 25 deletions(-) diff --git a/.abcd/development/specs/open/spc-12-disembark-grounds-a-lifeboat-s-open-questions-on-a-repo-s-to.md b/.abcd/development/specs/open/spc-12-disembark-grounds-a-lifeboat-s-open-questions-on-a-repo-s-to.md index 62e6fda..9ee07cd 100644 --- a/.abcd/development/specs/open/spc-12-disembark-grounds-a-lifeboat-s-open-questions-on-a-repo-s-to.md +++ b/.abcd/development/specs/open/spc-12-disembark-grounds-a-lifeboat-s-open-questions-on-a-repo-s-to.md @@ -83,16 +83,22 @@ The primitive therefore adds no second read path to audit. - **Recognised markers:** `TODO`, `FIXME`, `XXX`, `HACK`, `BUG` — uppercase only, matched by - `(^|[^A-Za-z0-9_-])(TODO|FIXME|XXX|HACK|BUG)(:|\(|\s|$)`. The leading class - is the word boundary that stops `TODO` matching inside `TODOS` or - `todo_list`; the trailing class admits the two conventional spellings - (`TODO:` and `TODO(alice):`) plus a bare word. The hyphen is excluded from - the leading class so the common redaction placeholder shape (`XXX-XXX-XXX`) - is rejected at every one of its triples — with the hyphen admitted, the last - triple matches on its leading `-` and a support phone number becomes a - fabricated open question. `NOTE` and `OPTIMIZE` are *not* recognised: `NOTE` - marks explanation rather than unfinished work, and `OPTIMIZE` is rare enough - that its false-positive cost exceeds its value. + `(^|[^A-Za-z0-9_-])(?:(TODO|FIXME)(?::|\()|(XXX|HACK|BUG)(?::|\(|\s|$))`. + The leading class is the word boundary that stops a marker matching inside a + longer identifier (`TODOS`, `todo_list`). The trailing boundary splits the + markers into two classes: `TODO` and `FIXME` require a trailing `:` or `(` + (the conventional `TODO:` and `TODO(alice):` spellings), because these are the + markers a project documents by name and their bare-word form is + indistinguishable from prose that merely mentions the marker (iss-111); `XXX`, + `HACK`, and `BUG` additionally admit a bare word (trailing whitespace or + end-of-line), because they are rarely written as bare uppercase words in prose + and the bare spelling is how they are conventionally written. The hyphen is + excluded from the leading class so the common redaction placeholder shape + (`XXX-XXX-XXX`) is rejected at every one of its triples — with the hyphen + admitted, the last triple matches on its leading `-` and a support phone + number becomes a fabricated open question. `NOTE` and `OPTIMIZE` are *not* + recognised: `NOTE` marks explanation rather than unfinished work, and + `OPTIMIZE` is rare enough that its false-positive cost exceeds its value. - **Binary files are skipped** by a NUL byte in the first 8 KiB — the conventional heuristic, and dependency-free. No extension allow-list is maintained. @@ -131,7 +137,7 @@ The primitive therefore adds no second read path to audit. | Question | Decision | |---|---| -| Which markers? | `TODO`, `FIXME`, `XXX`, `HACK`, `BUG`; uppercase only; word-boundary anchored, trailing `:`/`(`/space/EOL. `NOTE`, `OPTIMIZE` excluded. | +| Which markers? | `TODO`, `FIXME`, `XXX`, `HACK`, `BUG`; uppercase only; word-boundary anchored. `TODO`/`FIXME` require a trailing `:`/`(` (a bare word reads as prose that names the marker, iss-111); `XXX`/`HACK`/`BUG` also admit a bare word (trailing space/EOL). `NOTE`, `OPTIMIZE` excluded. | | Which tier — conventions or git? | **Conventions.** A working-tree file scan through the `SourceContext` file surface. The adapter never touches git, so it grounds a bare snapshot as readily as a working tree. | | Scan scope and the missing primitive | Option (a): add a bounded recursive-walk primitive, `WalkFiles`, to `SourceContext`. It is shared with itd-96, so the walk lands once. | | Which files to scan | Every regular file the walk yields, minus the skip set (`.git`, `node_modules`, `vendor`, `generated`), minus symlinks, minus binaries (NUL-byte heuristic), minus oversized files (`ReadFile`'s cap). | diff --git a/CHANGELOG.md b/CHANGELOG.md index d768733..d00fc33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,20 @@ called out in a **Breaking** section. user-facing by definition). `capture wontfix` is unchanged — a non-action ships nothing, so `wontfix/` carries no impact. +### Fixed + +- **The open-questions marker scan no longer reads documentation about markers as + open questions** (iss-111). The pattern that grounds `evidence/open-questions` + admitted a bare uppercase `TODO`/`FIXME` followed by whitespace, so on a + repository that documents its own conventions every prose mention of a marker + was cited as a work marker — 18 such false positives across the durable record + on this repository, all documentation, none a real marker. `TODO` and `FIXME` + now require a trailing `:` or `(` (the conventional `TODO:` / `TODO(alice):` + spellings), which is how genuine markers are almost always written; `XXX`, + `HACK`, and `BUG` still admit their conventional bare form, because they are + rarely written as bare words in prose and carry no measured false-positive + cost. + ## [0.4.0] - 2026-07-22 ### Breaking diff --git a/internal/core/lifeboat/sources_conventions.go b/internal/core/lifeboat/sources_conventions.go index cd33a5d..515a580 100644 --- a/internal/core/lifeboat/sources_conventions.go +++ b/internal/core/lifeboat/sources_conventions.go @@ -697,18 +697,52 @@ func (convInternalsSource) probeLimited(ctx *SourceContext, walkLimit int) Evide // convMarkerNames are the in-code work markers recognised as open questions, // uppercase only. NOTE and OPTIMIZE are deliberately absent: NOTE marks // explanation rather than unfinished work, and OPTIMIZE is rare enough that its -// false-positive cost exceeds its value. -var convMarkerNames = []string{"TODO", "FIXME", "XXX", "HACK", "BUG"} +// false-positive cost exceeds its value. The set splits by how the pattern +// anchors each marker's trailing boundary (see convMarkerRe). +var convMarkerNames = append(append([]string{}, convDelimitedMarkers...), convBareMarkers...) + +// convDelimitedMarkers must carry a trailing ':' or '(' to be recognised. TODO +// and FIXME are the markers a project documents by name, so their bare-word form +// (the marker followed by whitespace or end-of-line) is indistinguishable from +// prose that merely mentions the marker — the dominant false-positive class on a +// repository that documents its own conventions (iss-111). Requiring the +// conventional delimiter (TODO: or TODO(alice):) keeps the genuine markers and +// drops the mentions. +var convDelimitedMarkers = []string{"TODO", "FIXME"} + +// convBareMarkers additionally match a bare word (a trailing whitespace or +// end-of-line). XXX, HACK, and BUG are rarely written as bare uppercase words in +// prose, so their bare form carries no measured false-positive cost, and the bare +// spellings (XXX, // HACK around it, -- BUG --) are how these three are +// conventionally written — requiring a delimiter would lose real markers for no +// precision gain. +var convBareMarkers = []string{"XXX", "HACK", "BUG"} // convMarkerRe matches one recognised marker on a line. The leading class is the -// word boundary that stops TODO matching inside TODOS or todo_list; the trailing -// class admits the two conventional spellings (TODO: and TODO(alice):) plus a -// bare word. The hyphen is excluded from the leading class so the redaction -// placeholder shape (XXX-XXX-XXX) is rejected at every one of its triples — -// without it the last triple matches on its leading hyphen and a support phone -// number becomes a fabricated open question. Built from convMarkerNames so the -// set and the pattern cannot drift, and compiled once. -var convMarkerRe = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(` + strings.Join(convMarkerNames, "|") + `)(:|\(|\s|$)`) +// word boundary that stops a marker matching inside a longer identifier (TODOS, +// todo_list); the hyphen is excluded from it so the redaction placeholder shape +// (XXX-XXX-XXX) is rejected at every one of its triples — without that exclusion +// the last triple matches on its leading hyphen and a support phone number +// becomes a fabricated open question. The two marker classes differ only in their +// trailing boundary: convDelimitedMarkers (captured in group 2) require ':' or +// '('; convBareMarkers (group 3) also accept whitespace or end-of-line. Exactly +// one of the two marker groups is non-empty on a match — convMarkerName reads +// whichever fired. Built from the marker slices so the set and the pattern cannot +// drift, and compiled once. +var convMarkerRe = regexp.MustCompile( + `(^|[^A-Za-z0-9_-])` + + `(?:(` + strings.Join(convDelimitedMarkers, "|") + `)(?::|\()` + + `|(` + strings.Join(convBareMarkers, "|") + `)(?::|\(|\s|$))`) + +// convMarkerName returns the marker a convMarkerRe match captured. The pattern +// puts a delimited marker in group 2 and a bare-allowed marker in group 3, and +// exactly one is non-empty. +func convMarkerName(m []string) string { + if m[2] != "" { + return m[2] + } + return m[3] +} // maxMarkerCitations caps how many path:line citations the marker scan reports. // Beyond it the scan keeps counting — the headline stays truthful — but stops @@ -805,7 +839,7 @@ func (convOpenQuestionsSource) probeLimited(ctx *SourceContext, budget int) Evid hits++ markers++ if len(citations) < maxMarkerCitations { - citations = append(citations, fmt.Sprintf("%s:%d (%s)", p, i+1, m[2])) + citations = append(citations, fmt.Sprintf("%s:%d (%s)", p, i+1, convMarkerName(m))) } } if hits > 0 { diff --git a/internal/core/lifeboat/sources_conventions_test.go b/internal/core/lifeboat/sources_conventions_test.go index 47e2a86..0fb77d8 100644 --- a/internal/core/lifeboat/sources_conventions_test.go +++ b/internal/core/lifeboat/sources_conventions_test.go @@ -434,6 +434,31 @@ func TestConvOpenQuestionsIgnoresRedactionPlaceholders(t *testing.T) { } } +// TestConvOpenQuestionsIgnoresProseMentioningMarkers holds the iss-111 contract: +// a repository that documents its own conventions mentions TODO and FIXME by name +// in prose, and those mentions are not the team's open questions. A bare TODO or +// FIXME followed by whitespace reads exactly like such a mention, so the scan must +// come back blank rather than fabricate open questions out of the documentation. +func TestConvOpenQuestionsIgnoresProseMentioningMarkers(t *testing.T) { + dir := t.TempDir() + writeTree(t, dir, map[string]string{ + "go.mod": "module example.com/documented\n\ngo 1.22\n", + "CONTRIBUTING.md": "# Conventions\n\n" + + "Leave a TODO for unfinished work and a FIXME where a bug hides.\n" + + "We grep for TODO and FIXME markers before every release.\n", + }) + ctx, err := newSourceContext(dir) + if err != nil { + t.Fatal(err) + } + defer ctx.Close() + + ev := convSourceForSection(t, "evidence/open-questions").Probe(ctx) + if ev.Status != StatusBlank { + t.Fatalf("prose that merely names markers gives status %s, want blank; fabricated evidence %v", ev.Status, ev.Sources) + } +} + // TestConvMarkerRePinsTheRecognisedSpellings pins the marker pattern against the // spellings it must accept and the near-misses it must reject, so the word // boundary cannot be loosened or tightened without a test saying so. @@ -441,9 +466,7 @@ func TestConvMarkerRePinsTheRecognisedSpellings(t *testing.T) { match := []string{ "// TODO: handle the retry case", "//TODO: no space after the slashes", - "# TODO", "- TODO: a list item", - "* FIXME check this", "FIXME(alice): leaks a connection", "-- BUG --", "// HACK around the driver", @@ -455,6 +478,15 @@ func TestConvMarkerRePinsTheRecognisedSpellings(t *testing.T) { "XXX-XXX-XXX", "TODOS are not markers", "todo_list := nil", + // iss-111: TODO and FIXME require a trailing ':' or '('. A bare word is + // indistinguishable from prose that merely names the marker — the + // false-positive class that swamped a repo documenting its own + // conventions — so these documentation mentions must not match. + "# TODO", + "* FIXME check this", + "names TODO and FIXME markers as a read", + "a codebase dense with TODO markers", + "the TODO and FIXME conventions this project follows", } for _, line := range match { if convMarkerRe.FindStringSubmatch(line) == nil { @@ -463,7 +495,7 @@ func TestConvMarkerRePinsTheRecognisedSpellings(t *testing.T) { } for _, line := range reject { if m := convMarkerRe.FindStringSubmatch(line); m != nil { - t.Errorf("convMarkerRe matches %q as a %s marker", line, m[2]) + t.Errorf("convMarkerRe matches %q as a %s marker", line, convMarkerName(m)) } } } From fdb4d2a689c2b98634727c7fc78d28b4d8ee334e Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:53:48 +0100 Subject: [PATCH 2/3] chore: resolve iss-111 with fix impact The marker-pattern false-positive fix landed in the preceding commit; move the issue to resolved/ with the measured numbers and a fix impact. Assisted-by: Claude:claude-opus-4-8 --- ...-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md | 2 ++ 1 file changed, 2 insertions(+) rename .abcd/work/issues/{open => resolved}/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md (56%) diff --git a/.abcd/work/issues/open/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md b/.abcd/work/issues/resolved/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md similarity index 56% rename from .abcd/work/issues/open/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md rename to .abcd/work/issues/resolved/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md index 6b712ad..17f3153 100644 --- a/.abcd/work/issues/open/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md +++ b/.abcd/work/issues/resolved/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md @@ -7,6 +7,8 @@ category: "tech-debt" source: "impl-review" found_during: "itd-95 P1 review" found_at: "internal/core/lifeboat/sources_conventions.go" +resolution: "Marker pattern now splits by trailing boundary: TODO/FIXME require ':' or '(', XXX/HACK/BUG keep the bare word (option 2 of the two recorded). On this repo the durable-record documentation FP corpus dropped 18 to 3 (the 3 survivors are prose that literally quotes 'TODO:' with the colon, irreducible for this mechanism); 100% of the bare-word 'mentions a marker' FP class killed. All colon-form fixture markers preserved; only the bare 'FIXME check this' and '# TODO' pinning spellings reclassified to reject, correctly, as indistinguishable from prose. spc-12 pattern description amended in the same change." +impact: fix --- The open-questions marker pattern admits a bare uppercase word followed by whitespace, so prose that merely mentions a marker matches. Measured against this repository the adapter reports 42 markers across 11 files at medium confidence, and every one is documentation about markers rather than a work marker. The precision cost lands on repos that document their own conventions. Options: require the trailing colon or parenthesis, or drop the bare-word alternative for the ambiguous markers only. spc-12 fixes the current pattern, so this is a design revisit. \ No newline at end of file From e4158bd797ee7c6646edbbb9822bdf8122a36b0d Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 26 Jul 2026 05:06:11 +0100 Subject: [PATCH 3/3] docs: correct iss-111 FP figure and de-self-match the pattern comments Review found two factual-accuracy defects in the iss-111 fix's prose: 1. The "18 false positives" figure did not reproduce. Re-measured the old pattern against the base-commit tree, scoped explicitly to the durable record (.abcd/development/): 14 before, 3 after. Corrected the figure and named the scope in the CHANGELOG entry and the resolved iss-111 note. 2. The new doc-comments naming XXX/HACK/BUG and the TODO: spelling were themselves bare-word / delimited marker tokens, so the scan self-cited its own prose (the file went 2 -> 3 self-citations). Reworded so the comments name the markers only as slice values, not inline; the file's self-match count is now 0 (verified by re-running the pattern over the file). No behaviour change: the pattern, tests, and spec are untouched. Assisted-by: Claude:claude-opus-4-8 --- ...rker-pattern-admits-a-bare-uppercase-wo.md | 2 +- CHANGELOG.md | 6 +++-- internal/core/lifeboat/sources_conventions.go | 24 ++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.abcd/work/issues/resolved/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md b/.abcd/work/issues/resolved/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md index 17f3153..a265ce2 100644 --- a/.abcd/work/issues/resolved/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md +++ b/.abcd/work/issues/resolved/iss-111-the-open-questions-marker-pattern-admits-a-bare-uppercase-wo.md @@ -7,7 +7,7 @@ category: "tech-debt" source: "impl-review" found_during: "itd-95 P1 review" found_at: "internal/core/lifeboat/sources_conventions.go" -resolution: "Marker pattern now splits by trailing boundary: TODO/FIXME require ':' or '(', XXX/HACK/BUG keep the bare word (option 2 of the two recorded). On this repo the durable-record documentation FP corpus dropped 18 to 3 (the 3 survivors are prose that literally quotes 'TODO:' with the colon, irreducible for this mechanism); 100% of the bare-word 'mentions a marker' FP class killed. All colon-form fixture markers preserved; only the bare 'FIXME check this' and '# TODO' pinning spellings reclassified to reject, correctly, as indistinguishable from prose. spc-12 pattern description amended in the same change." +resolution: "Marker pattern now splits by trailing boundary: TODO/FIXME require ':' or '(', XXX/HACK/BUG keep the bare word (option 2 of the two recorded). On this repo the durable-record documentation FP corpus (scope: .abcd/development/) dropped from 14 to 3 (the 3 survivors are prose that literally quotes the colon form, irreducible for this mechanism); 100% of the bare-word 'mentions a marker' FP class killed. All colon-form fixture markers preserved; only the bare 'FIXME check this' and '# TODO' pinning spellings reclassified to reject, correctly, as indistinguishable from prose. spc-12 pattern description amended in the same change." impact: fix --- diff --git a/CHANGELOG.md b/CHANGELOG.md index d00fc33..b67ec4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,10 @@ called out in a **Breaking** section. open questions** (iss-111). The pattern that grounds `evidence/open-questions` admitted a bare uppercase `TODO`/`FIXME` followed by whitespace, so on a repository that documents its own conventions every prose mention of a marker - was cited as a work marker — 18 such false positives across the durable record - on this repository, all documentation, none a real marker. `TODO` and `FIXME` + was cited as a work marker — 14 such false positives across the durable record + (`.abcd/development/`) on this repository, all documentation, none a real + marker, down to 3 after the fix (each an irreducible prose quotation of the + literal `TODO:` form). `TODO` and `FIXME` now require a trailing `:` or `(` (the conventional `TODO:` / `TODO(alice):` spellings), which is how genuine markers are almost always written; `XXX`, `HACK`, and `BUG` still admit their conventional bare form, because they are diff --git a/internal/core/lifeboat/sources_conventions.go b/internal/core/lifeboat/sources_conventions.go index 515a580..eeb08cf 100644 --- a/internal/core/lifeboat/sources_conventions.go +++ b/internal/core/lifeboat/sources_conventions.go @@ -701,21 +701,23 @@ func (convInternalsSource) probeLimited(ctx *SourceContext, walkLimit int) Evide // anchors each marker's trailing boundary (see convMarkerRe). var convMarkerNames = append(append([]string{}, convDelimitedMarkers...), convBareMarkers...) -// convDelimitedMarkers must carry a trailing ':' or '(' to be recognised. TODO -// and FIXME are the markers a project documents by name, so their bare-word form -// (the marker followed by whitespace or end-of-line) is indistinguishable from -// prose that merely mentions the marker — the dominant false-positive class on a +// convDelimitedMarkers must carry a trailing ':' or '(' to be recognised. They +// are the markers a project documents by name, so their bare-word form (the +// marker followed by whitespace or end-of-line) is indistinguishable from prose +// that merely mentions the marker — the dominant false-positive class on a // repository that documents its own conventions (iss-111). Requiring the -// conventional delimiter (TODO: or TODO(alice):) keeps the genuine markers and -// drops the mentions. +// conventional trailing delimiter — a colon or an open-paren, as in an authored +// marker naming its owner — keeps the genuine markers and drops the mentions. +// (This comment states the markers as slice values below rather than inline, so +// the scan does not cite its own prose.) var convDelimitedMarkers = []string{"TODO", "FIXME"} // convBareMarkers additionally match a bare word (a trailing whitespace or -// end-of-line). XXX, HACK, and BUG are rarely written as bare uppercase words in -// prose, so their bare form carries no measured false-positive cost, and the bare -// spellings (XXX, // HACK around it, -- BUG --) are how these three are -// conventionally written — requiring a delimiter would lose real markers for no -// precision gain. +// end-of-line). These three are rarely written as bare uppercase words in running +// prose, so their bare form carries no measured false-positive cost, and that +// bare spelling is how they are conventionally written — requiring a delimiter +// would lose real markers for no precision gain. (Named only as slice values +// below, for the same reason as convDelimitedMarkers.) var convBareMarkers = []string{"XXX", "HACK", "BUG"} // convMarkerRe matches one recognised marker on a line. The leading class is the