Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ suggested_kind: null
reclassification_history: []
builds_on: []
severity: minor
impact: additive
---

# Disembark Grounds A Lifeboat's Open Questions On A Repo's TODO And FIXME Markers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ suggested_kind: null
reclassification_history: []
builds_on: []
severity: minor
impact: additive
---

# Disembark Reads A Repo's Naming And Internals Conventions Into Its Lifeboat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,158 @@ intent: itd-95

## Summary

_Draft: describe what spc-10 delivers for itd-95 — scope, approach, and how it satisfies the intent's Acceptance Criteria. This spec is the design record the fidelity review audits against._
spc-10 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 of `convGlossarySource`, grounding
`evidence/open-questions` at `TierConventions`.
- **Registration** (`conventionSources()` in `probe.go`): one line.
- **No mapping change.** The `evidence/open-questions` row 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 existing `SourceContext`
surface 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 runs over `c.root.FS()`, so `os.Root` refuses any
component that escapes the repository root, symlinked intermediates included.
A `nil` root (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: `.git`, `node_modules`,
`vendor`, `generated`. These are dependency, VCS-internal, and generated
trees — never a team's own open questions, and the dominant cost of an
unfiltered walk.
- **Caps, in three dimensions.** `maxWalkFiles` mirrors `maxDirEntries`
(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.
`maxWalkDepth` (32) bounds descent, because `os.Root` resolves every
directory from the containment root one component at a time — a chain of
directories costs the square of its depth, and a few thousand of them are
trivial to create and take minutes to traverse. Real trees are shallow, so
the depth cap prunes only pathological chains, and prunes the chain rather
than abandoning the tree. Any bound firing returns `truncated = 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 `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.
- **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 by `ReadFile`).
Bytes across the whole scan: `maxMarkerScanBytes`, reusing `maxPlanTotalBytes`
(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
dimension `maxEmbarkTotalBytes` already caps on the embark side; every byte
`ReadFile` returns 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 to
`maxMarkerCitations` `path:line (TODO)` entries, all passed through
`dedupeSorted` so repeated identical citations collapse and the order is
stable.
- **Status ceiling: `StatusPartial`.** Markers are a thread, not a framed set of
open questions — a `TODO` says something is unfinished, not what the question
is. This also honours the mapping row's conventions prediction. Confidence
carries the strength instead: `ConfidenceMedium` at ten or more markers,
`ConfidenceLow` below that.
- **No markers → a blank** carrying `Searched` (the marker set and the skip
set) and the human `Question`, exactly as every other adapter's blank
contract requires. When a bound cut the scan short, the blank says so:
`Searched` names the cap that stopped it and the `Question` claims 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, 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). |
| Per-repo caps | `maxWalkFiles` = 50 000 files **and** 50 000 directories (mirrors `maxDirEntries`), `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** —
`convOpenQuestionsSource` over a fixture repo carrying `TODO`/`FIXME`;
asserted non-blank, `Tier == TierConventions`, evidence carrying `file:line`.
- **No markers → an honest blank** — a marker-free fixture asserts
`StatusBlank` with a populated `Searched` and a non-empty `Question`.
- **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
`maxWalkFiles` asserts the walk returns `truncated = true` and 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-XXX` redaction placeholders asserts `StatusBlank`, 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`/`tierRank` reduction is unchanged and already tested; the new
adapter adds no second winner.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,125 @@ intent: itd-96

## Summary

_Draft: describe what spc-11 delivers for itd-96 — scope, approach, and how it satisfies the intent's Acceptance Criteria. This spec is the design record the fidelity review audits against._
spc-11 delivers itd-96: two conventions-tier `Source` adapters that ground
`constraints/naming` and `internals` on the files a team naturally writes — a
`NAMING.md` or naming page under `docs/` for naming, an `ARCHITECTURE.md` or
architecture/explanation tree plus the package layout for internals. A
repository with conventional docs but no `.abcd/` record now carries both
sections in its lifeboat instead of two blanks. `glossary` is untouched:
`convGlossarySource` already owns it, and this spec adds no second adapter for
it.

## Scope

- **Adapters** (`internal/core/lifeboat/sources_conventions.go`):
`convNamingSource` (`constraints/naming`) and `convInternalsSource`
(`internals`), both `TierConventions`, both siblings of `convGlossarySource`.
- **Registration** (`conventionSources()` in `probe.go`): one line each. With
spc-10's `convOpenQuestionsSource` this brings the conventions tier to
fourteen adapters.
- **Tier gate** (`hasConventions` in `probe.go`): the new adapters' own name
lists are appended to the candidate union, per the comment that already
forbids narrowing the gate below what the adapters read — otherwise a repo
carrying *only* a `NAMING.md` or `ARCHITECTURE.md` would have the whole Tier-1
set skipped and blank falsely.
- **Depends on spc-10** for `(*SourceContext).WalkFiles`, which
`convInternalsSource` uses for its layout scan. spc-11 stacks on spc-10; it
does not re-land the primitive.
- **No mapping change, no new dependency.**

## Approach

### `convNamingSource` — `constraints/naming`

Preference order, first match wins:

1. A dedicated naming document — `ctx.FindFirst("NAMING.md", "NAMING",
"naming.md")`, then the first `docs/` entry whose lower-cased name starts
with `naming` (the same `ListDir`-prefix idiom `convGlossarySource` uses for
`docs/glossary*`). Cited alone, `ConfidenceMedium`.
2. **Fallback: the glossary document.** A project that never wrote a naming
registry usually encodes its reserved vocabulary in its glossary, so the
glossary is real evidence for naming — but weaker, because a glossary
defines terms rather than ruling on what may be renamed. Cited as
`"<path> (glossary fallback — no dedicated naming document)"`,
`ConfidenceLow`.
3. Neither → a blank naming both search sets in `Searched` and asking the human
question.

**Status is `StatusPartial` in both non-blank cases** — the ceiling the mapping
row predicts for this section, and the honest one: neither a naming page nor a
glossary enumerates a project's full reserved vocabulary. The strength of the
signal is carried by `Confidence` (medium for a dedicated document, low for the
glossary fallback), not by inflating the status.

**Distinctness from `glossary` is structural, not incidental.** The two
adapters declare different `Section()` values, so the orchestrator indexes them
into different coverage rows and neither can displace the other. On a
glossary-only repository the report shows `glossary` partial/medium cited to
`GLOSSARY.md`, and `constraints/naming` partial/low cited to the same file
*with the fallback qualifier* — visibly a weaker, derived reading rather than a
duplicate row. A test asserts the two rows differ in confidence and in cited
string on that fixture.

### `convInternalsSource` — `internals`

Two independent signals, combined:

- **Architecture prose.** `ctx.FindFirst("ARCHITECTURE.md", "ARCHITECTURE",
"architecture.md", "docs/architecture.md")`, then the directories
`docs/architecture`, `docs/design`, `docs/explanation` (Diátaxis), in that
preference order. A file is read and measured with the existing
`convProseBytes` / `convGroundedProseBytes` threshold — the same measure
`convContextSource` uses for a README, reused rather than re-invented. A
directory counts as prose evidence when it holds at least one Markdown file.
- **Package layout.** `WalkFiles` from `.`, keeping the top-two path segments
of every file under a recognised source root (`internal`, `pkg`, `src`,
`lib`, `cmd`, `app`) — the packages a rescuer must navigate. Cited as
`"<root>/<pkg>/"` entries, capped at `maxLayoutCitations` (50) with the
overflow reported as a count. `WalkFiles`'s `truncated` flag, when set, is
reported in the citation so a capped scan never reads as a complete one.

Outcomes:

| Signals present | Status | Confidence |
|---|---|---|
| Architecture prose above the threshold (± layout) | `StatusPartial` | `ConfidenceHigh` |
| Architecture doc present but thin, or a doc directory with no prose measured | `StatusPartial` | `ConfidenceMedium` |
| Layout only, no architecture doc | `StatusPartial` | `ConfidenceLow` |
| Neither | `StatusBlank` with `Searched` + `Question` | — |

As with naming, `StatusPartial` is the ceiling: an `ARCHITECTURE.md` plus a
package listing describes the shape of a system, not its internals chapters.
This matches the mapping row's conventions prediction, which stays unedited.

### Resolved open questions (itd-96 § Open Questions)

| Question | Decision |
|---|---|
| Naming vs glossary — what is naming's distinct source? | A **dedicated** naming document (`NAMING.md`, `docs/naming*`) is naming's primary source; the glossary is an explicitly-qualified **fallback** at lower confidence. The two sections are distinct rows with distinct evidence strings; no second `glossary` adapter is added. |
| Which paths map to `internals`? | `ARCHITECTURE.md` (and spellings) → `docs/architecture.md` → `docs/architecture/` → `docs/design/` → `docs/explanation/`, in that preference order, plus the package layout as an independent second signal. |
| What counts as "package layout", and the missing primitive? | Top-two path segments under `internal`, `pkg`, `src`, `lib`, `cmd`, `app`, gathered with spc-10's `WalkFiles`. A bounded recursive walk is needed (a top-level `ListDir` cannot see `internal/core/lifeboat`), and it is the shared primitive both intents asked for — landed once, in spc-10. |
| Extraction heuristics and status thresholds | The existing `convProseBytes` ≥ `convGroundedProseBytes` threshold separates real architecture prose from a stub. Status ceilings at `StatusPartial` for both sections (the mapping contract's conventions prediction); the distinction lives in `Confidence`. |
| Confidence levels | Naming: medium (dedicated doc) / low (glossary fallback). Internals: high (real prose) / medium (thin doc) / low (layout only). |
| Reserved-vocabulary spellings | `NAMING.md`, `NAMING`, `naming.md`, and any `docs/` entry whose name starts with `naming` (case-insensitive). Heading-shape sniffing inside a glossary is *not* attempted — it would guess at a convention that has no standard. |

## Acceptance-criteria satisfaction

- **Naming docs → non-blank, cites the file** — a fixture with `NAMING.md`
asserts `constraints/naming` partial, `TierConventions`, citing `NAMING.md`.
- **ARCHITECTURE.md + layout → non-blank, cites both** — a fixture with
`ARCHITECTURE.md` and an `internal/<pkg>/` tree asserts `internals` non-blank
citing the doc and the layout entries.
- **Neither → honest blanks** — a bare fixture asserts both sections blank with
populated `Searched` and a non-empty `Question`.
- **Read-only** — the byte-for-byte tree-invariance test from spc-10 covers the
whole probe, both new adapters included.
- **No duplicate `glossary` adapter** — a glossary-only fixture asserts
`glossary` is still grounded by `convGlossarySource`, that
`constraints/naming` is partial with the fallback qualifier and lower
confidence, and that `conventionSources()` contains exactly one adapter whose
`Section()` is `glossary`.
- **Both tiers present → one deterministic result** — the unchanged
`beats`/`tierRank` reduction; the native adapters keep winning where a record
exists, and the report names the winning tier.
Loading