Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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.
24 changes: 24 additions & 0 deletions .abcd/work/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,27 @@ parallel-agent merge contention bites.
`/abcd:intent`'s interview script: `plan` is never run without the human's
explicit in-session confirmation). Escalation path if violations appear: an
`ac_confirmed_by:` field is lint-legal today and slots in as a fifth check.

- 2026-07-21 — The mapping table's per-tier status columns are a **ceiling**,
not merely a prediction. Every conventions adapter already honours its row
(`convGlossarySource` returns partial where the row says partial;
`convPlatformSource` returns grounded where it says grounded), so itd-95's
and itd-96's three new adapters cap at `StatusPartial` — the value all three
rows predict — and carry signal strength in `Confidence` instead. Rejected:
returning `StatusGrounded` for a dedicated `NAMING.md` or a prose-bearing
`ARCHITECTURE.md`, which would have made the rendered brief table wrong and
required editing `mapping.go`, the brief-to-lifeboat contract both intents
put out of scope. Every acceptance bar asks only for "non-blank", so the
ceiling satisfies them. Revisit only by amending the mapping row first.

- 2026-07-21 — A probe walk of a foreign tree must be bounded in **three**
dimensions, not one. itd-95 shipped `WalkFiles` with a regular-file cap; an
independent security review of the itd-96 branch showed both remaining
dimensions were exploitable — a tree of directories holding no regular file
never reaches a file cap, and `os.Root` re-resolves each directory from the
containment root one component at a time, making a directory chain quadratic
in its depth (depth-1500 did not finish in two minutes). Directories are now
counted against the same cap and descent is capped at `maxWalkDepth`. The
general rule: any new whole-tree traversal states which of {entries, depth,
aggregate bytes} bounds it, because a per-item cap and a count cap multiply
and their product is not a bound.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
schema_version: 1
id: "iss-113"
slug: "convopenquestionssource-s-truncation-evidence-names-only-the"
severity: "nitpick"
category: "tech-debt"
source: "impl-review"
found_during: "itd-96 P1 review"
found_at: "internal/core/lifeboat/sources_conventions.go"
resolution: "Fixed on the itd-95 branch alongside the walk-bounding change that caused it: both truncation strings in convOpenQuestionsSource now name the walk cap as '(N entries, M levels deep)' rather than as a file cap alone, so the evidence matches the three dimensions the walk actually bounds."
---

convOpenQuestionsSource's truncation evidence names only the "%d-file walk cap", but WalkFiles now also truncates on its directory cap and its depth cap, so a walk stopped by either reports a cause it did not hit. The three affected strings are the searched note, the blank's qualified question, and the non-blank scan note. convInternalsSource words the same note as "walk cap (N entries, M levels deep)"; the two should say the same thing. Found by an independent security review of the itd-96 diff.
8 changes: 7 additions & 1 deletion internal/core/lifeboat/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,13 @@ func hasConventions(c *SourceContext) bool {
candidates = append(candidates, convGlossaryDocNames...) // convGlossarySource
candidates = append(candidates, convPlatformFiles...) // convPlatformSource (Dockerfile, Makefile, go.mod, package.json)
candidates = append(candidates, convADRDirs...) // convADRsSource
for _, ml := range convManifestLocks { // convDependenciesSource
candidates = append(candidates, convNamingDocNames...) // convNamingSource
// convInternalsSource: an architecture document, an architecture tree, or the
// package layout on its own is grounding evidence for internals.
candidates = append(candidates, convArchitectureDocNames...)
candidates = append(candidates, convArchitectureDirs...)
candidates = append(candidates, convLayoutRoots...)
for _, ml := range convManifestLocks { // convDependenciesSource
candidates = append(candidates, ml.manifest)
}
return c.FindFirst(candidates...) != ""
Expand Down
19 changes: 13 additions & 6 deletions internal/core/lifeboat/probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,22 @@ func TestWalkFilesStopsAtTheDepthCap(t *testing.T) {
// full strength: a probe of a marker-bearing tree must leave every file's
// contents unchanged and add or remove nothing. The marker adapter reads every
// file in the tree, so byte-level proof — not a path/size fingerprint — is what
// makes "point it at an archived project, touch nothing" true.
// makes "point it at an archived project, touch nothing" true. The fixture
// carries every signal the whole-tree adapters consult — work markers, a naming
// document, an architecture document, and a package tree — so one fixture proves
// the property for all of them.
func TestProbeLeavesEveryFileByteIdentical(t *testing.T) {
dir := t.TempDir()
writeTree(t, dir, map[string]string{
"README.md": "# demo\n\nA demo project kept for the probe.\n",
"go.mod": "module example.com/demo\n\ngo 1.22\n",
"src/a.go": "package a\n\n// TODO: handle the retry case\n",
"src/b.go": "package a\n\n// FIXME: this leaks a connection\n",
"docs/notes.md": "Notes about the demo.\n",
"README.md": "# demo\n\nA demo project kept for the probe.\n",
"go.mod": "module example.com/demo\n\ngo 1.22\n",
"src/a.go": "package a\n\n// TODO: handle the retry case\n",
"src/b.go": "package a\n\n// FIXME: this leaks a connection\n",
"docs/notes.md": "Notes about the demo.\n",
"NAMING.md": "# Naming\n\nA voyage is never called a run.\n",
"ARCHITECTURE.md": "# Architecture\n\nA core, and a shell that formats it.\n",
"internal/core/core.go": "package core\n",
"internal/store/store.go": "package store\n",
})
before := fileHashes(t, dir)
if _, err := Probe(dir); err != nil {
Expand Down
Loading