Development record
disembark-grounds-a-lifeboat-s-open-questions-on-a-repo-s-to
Summary
spc-12 delivers itd-95: a conventions-tier Source adapter that grounds
evidence/open-questions on the in-code work markers a team left in its source
tree, plus the bounded recursive-walk primitive the adapter needs. A repository
with no .abcd/ record but a codebase full of TODO: and FIXME: now reports
that section non-blank, citing file:line for every marker it found, instead of
falling through to a blank a rescuer has to fill from nothing.
Scope
- Core primitive (
internal/core/lifeboat/probe.go):(*SourceContext).WalkFiles(rel string) (paths []string, truncated bool)— a bounded, contained recursive walk of regular files under a repo-relative root. It is the missing primitive both itd-95 and itd-96 raise, landed once here (one-canonical-primitive) and consumed by itd-96's internals adapter. - Adapter (
internal/core/lifeboat/sources_conventions.go):convOpenQuestionsSource, a sibling ofconvGlossarySource, groundingevidence/open-questionsatTierConventions. - Registration (
conventionSources()inprobe.go): one line. - No mapping change. The
evidence/open-questionsrow already names "TODO and FIXME markers" as a read and predicts a partial conventions status; this spec delivers the adapter that makes the prediction true, and honours the prediction as a ceiling rather than editing the contract. - No new dependency.
regexp,io/fs, and the existingSourceContextsurface only.
Approach
The WalkFiles primitive
SourceContext today exposes a non-recursive ListDir and a bounded
ReadFile; there is no recursive walk. WalkFiles adds one, mirroring
embark.go's walkLifeboatFiles — the repository's existing canonical
fs.WalkDir(root.FS(), …) walk — rather than inventing a second traversal
idiom.
- Containment. The walk descends through a sub-root per directory
(
os.Root.OpenRoot);os.Rootrefuses any component that escapes the repository root, symlinked intermediates included, and a symlinked directory is detected from itsReadDirtype and skipped before it is ever opened. The containment property holds unchanged while each child opens in O(1). Anilroot (unopenable repository) returns(nil, false). - Symlinks are skipped, never followed. Unlike embark — where a symlink in a packed lifeboat is a trust violation and therefore fatal — a probe reads an arbitrary foreign tree where symlinks are ordinary. The walk skips symlinked entries (files and directories) and continues; it never errors on one.
- Skip set. Directory names never descended into, matched by name at any
depth:
.git(VCS);node_modules(Node);vendor,generated(Go/generic);.venv,venv,.tox,__pycache__(Python);target,build,dist(build/distribution output);Pods(CocoaPods). These are dependency, VCS-internal, language-cache, and generated trees across the common ecosystems — never a team's own open questions, and the dominant cost of an unfiltered walk; walked as source, a vendoredTODOis cited as this project's own open question. - Caps.
maxWalkFilesmirrorsmaxDirEntries(50 000) and bounds regular files and directories visited: a tree of directories holding nothing regular yields no path, so a file cap alone never fires there and the walk would run to exhaustion over a foreign tree.maxDirEntries(50 000) is also the per-directory read bound: each directory is read with a boundedReadDir— the one canonical guardListDiruses — so a single directory of millions of entries cannot balloon memory before the file cap applies.maxWalkDepth(32) bounds descent: the walk holds a sub-root per directory (os.Root.OpenRoot), so each child opens in O(1) and a chain costs O(depth) rather than the square of its depth, but an unbounded chain is still an unbounded recursion and a pathological cost, so the cap prunes it — pruning the chain rather than abandoning the tree. Real trees are shallow. Any bound firing returnstruncated = true. Truncation is reported, never silent (loud-staging): an adapter that hits a cap says so in its cited evidence, and a blank drawn from a truncated walk says the walk was truncated. - Non-regular files (FIFOs, devices, sockets) are skipped, so the walk cannot hand an adapter a path whose read would block.
- Output is repo-relative POSIX paths, sorted, so every consumer is deterministic.
WalkFiles returns paths only; content is read through the existing
ReadFile, which already enforces the containment root, the
maxProbeReadBytes per-file cap, regular-file-only, and the non-blocking open.
The primitive therefore adds no second read path to audit.
The marker adapter
convOpenQuestionsSource.Probe walks the tree from ., reads each file through
ReadFile, and records every recognised marker as a file:line citation.
- 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 a marker matching inside a longer identifier (TODOS,todo_list). The trailing boundary splits the markers into two classes:TODOandFIXMErequire a trailing:or((the conventionalTODO:andTODO(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, andBUGadditionally 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.NOTEandOPTIMIZEare not recognised:NOTEmarks explanation rather than unfinished work, andOPTIMIZEis 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.
- Caps. Files walked:
maxWalkFiles(inherited). Bytes per file:maxProbeReadBytes(inherited — an oversized file is skipped byReadFile). Bytes across the whole scan:maxMarkerScanBytes, reusingmaxPlanTotalBytes(512 MiB). The file cap and the per-file cap bound one walk and one read but their product does not bound the scan, so the aggregate budget is the third dimensionmaxEmbarkTotalBytesalready caps on the embark side; every byteReadFilereturns is charged against it, before the binary test, so a tree of blobs spends it exactly as a tree of source does. Citations reported:maxMarkerCitations(200); beyond it the scan keeps counting but stops citing, and says so. - Output shape mirrors
convADRsSource: a headline count ("N work marker(s) across M file(s)") followed by up tomaxMarkerCitationspath:line (TODO)entries, all passed throughdedupeSortedso repeated identical citations collapse and the order is stable. - Status ceiling:
StatusPartial. Markers are a thread, not a framed set of open questions — aTODOsays something is unfinished, not what the question is. This also honours the mapping row's conventions prediction. Confidence carries the strength instead:ConfidenceMediumat ten or more markers,ConfidenceLowbelow that. - No markers → a blank carrying
Searched(the marker set and the skip set) and the humanQuestion, exactly as every other adapter's blank contract requires. When a bound cut the scan short, the blank says so:Searchednames the cap that stopped it and theQuestionclaims only that the part of the tree the scan reached carries no markers. A blank is a first-class result (adr-35) only while it is trustworthy, so it never generalises from a partial read. - Read-only by construction. The adapter opens nothing for writing and touches nothing outside the containment root; a byte-for-byte tree-invariance test proves it.
Resolved open questions (itd-95 § Open Questions)
| Question | Decision |
|---|---|
| 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, .venv, venv, .tox, __pycache__, target, build, dist, Pods), minus symlinks, minus binaries (NUL-byte heuristic), minus oversized files (ReadFile's cap). |
| Per-repo caps | maxWalkFiles = 50 000 files and 50 000 directories (mirrors maxDirEntries), maxDirEntries = 50 000 entries read per directory, maxWalkDepth = 32 levels of descent, maxProbeReadBytes per file, maxMarkerScanBytes = 512 MiB across the scan (reuses maxPlanTotalBytes), maxMarkerCitations = 200 citations. Every bound that fires is reported in the cited evidence. |
| Output shape and framing | Headline count + up to 200 path:line (MARKER) citations, dedupeSorted. |
| Dedup | dedupeSorted on the rendered citation string, so an identical path:line (MARKER) appears once. Multiple distinct markers in one file each keep their own line. |
| Status and confidence thresholds | Ceiling StatusPartial. ConfidenceMedium at ≥ 10 markers, else ConfidenceLow. Never StatusGrounded. |
| Interaction with the native adapter | Unchanged: richest-tier-wins. nativeOpenQuestionsSource displaces the marker evidence on a repository carrying both, deterministically, and the coverage report names the winning tier. Merging across tiers stays out of scope. |
Acceptance-criteria satisfaction
- Record-less repo with markers → non-blank, cites the files —
convOpenQuestionsSourceover a fixture repo carryingTODO/FIXME; asserted non-blank,Tier == TierConventions, evidence carryingfile:line. - No markers → an honest blank — a marker-free fixture asserts
StatusBlankwith a populatedSearchedand a non-emptyQuestion. - Read-only — a byte-for-byte tree-invariance test (walk the fixture, hash every file, probe, re-hash) proves the probe writes nothing.
- Pathological tree stays inside the caps — a fixture exceeding
maxWalkFilesasserts the walk returnstruncated = trueand terminates; a fixture exceeding an injected scan budget asserts the scan stops partway and names the budget in its own evidence; a fixture with a symlink escaping the root asserts nothing outside the containment root is ever read. - No fabricated evidence — a fixture whose only uppercase triples are
XXX-XXX-XXXredaction placeholders assertsStatusBlank, and a table test pins the marker pattern against the spellings it accepts and the near-misses it rejects. - Both tiers present → one deterministic result — the existing
beats/tierRankreduction is unchanged and already tested; the new adapter adds no second winner.
Frontmatter
| id | spc-12 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-12-disembark-grounds-a-lifeboat-s-open.md
open on GitHub ↗ · commit history ↗
References1
- itd-95Disembark Grounds A Lifeboat's Open Questions On A Repo's TODO And FIXME Markersimplements
mentions20
- adr-35The lifeboat is a coverage experiment — read-only, out-of-tree, and proven before it is packed (supersedes adr-4)
- dcn-26071700000000042026-07-17 — Burst 5 (M5, itd-43/spc-8): GL002 enforces a deliberate subset
- iss-111The 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.
- iss-116The WalkFiles skip set covers only Node and Go dependency trees, so a Python .venv, a Rust target/, __pycache__, dist/, build/ and Pods/ are walked as if they were the team's own source. Two consequences on a record-less repo: the open-questions adapter cites a vendored dependency's TODO as this project's open question, and because fs.WalkDir reads directories in lexical order a large dot-prefixed dependency tree can consume the 50000-file walk cap before the project's own src/ is reached. Found by an independent review of the itd-95 diff; spc-12 names the current skip set, so widening it is a design revisit rather than an implementation fix.
- iss-364capture validates related_specs against the retired fn-N namespace (reFnID) instead of the live spc-N; adr-11 renamed fn to spec, fn- is a record-lint blocker, and 0 of 340 records populate the field, so it is unusable for its documented purpose
- iss-2608271804178239filename-slug agreement is unenforced across the record stores and two records have drifted: superseded itd-47's frontmatter slug embeds the foreign handle spc-12 (now a live unrelated spec) and its H1 carries the same stale prefix, and iss-2608231237300997 is the single ledger record whose filename slug disagrees with its frontmatter slug. Fix the two instances (frontmatter+H1 edit for itd-47; rename to the frontmatter slug for the iss record, id unchanged) and close the class: capture validateInvariants and the record-lint schema check filename id but never slug.
- iss-2609261536147903Planned intents and disciplines still cite specs of the retired predecessor store unqualified, and such an id at or below spc-70 resolves to a live spec on another subject (spc-12, for one, is live as the disembark grounding spec): itd-48, itd-50 and itd-53 name spc-12, spc-28, spc-29, spc-31, spc-33, spc-43 and spc-52 for the fidelity reviewer, lifecycle hook and cleanup work (which of these are predecessor ids is itself the reading this record owes); itd-24 names spc-66 as the phase-audit receipt; itd-1 and itd-37 name spc-12 as the manual reviewer. The specs charter (Two spc-N Namespaces) rules that every predecessor citation carries the '(predecessor store)' qualifier; iss-239 applied it to the draft corpus only. Each site needs a reading to tell a predecessor citation from a live one, which is why the sweep was not folded into iss-239.
- itd-1Every Intent Has Acceptance Criteria; Every Audit Verifies Them
- itd-4Nothing You Notice Gets Lost
- itd-37Every Spec Externalises How It Should And Shouldn't Be Modified
- itd-44A Capture-Time `decision` Verdict That Routes Infrastructure Choices Into The Existing ADR Store
- itd-47Oracle-Backed Gates Pass Honestly Without A Human In The Loop
- itd-48`intent-fidelity-reviewer` Gains Its Cross-Doc And Kind-Classification Roles
- itd-50The Audit Loop Drives An Intent To Acceptance — Or Calls For A Replan
- itd-53A Shipped Intent No Longer Drifts Out Of Audit Just Because Nobody Ran The Review
- itd-55abcd Can Tell Whether Its Own Reasoning Rests On Bedrock Or On An Unexamined Assumption
- itd-62abcd Routes The Invisible Risks An Amateur Cannot See To A Real, Fail-Closed Gate That Wraps A Trusted Scanner
- itd-96Disembark Reads A Repo's Naming And Internals Conventions Into Its Lifeboat
- spc-8epic-to-spec-terminology
- spc-13disembark-reads-a-repo-s-naming-and-internals-conventions-in