diff --git a/.abcd/development/intents/drafts/itd-2609152113364815-testimony-maps-a-confirmed-terminal-session-finding-to-the-c.md b/.abcd/development/intents/drafts/itd-2609152113364815-testimony-maps-a-confirmed-terminal-session-finding-to-the-c.md new file mode 100644 index 0000000..dcd6639 --- /dev/null +++ b/.abcd/development/intents/drafts/itd-2609152113364815-testimony-maps-a-confirmed-terminal-session-finding-to-the-c.md @@ -0,0 +1,45 @@ +--- +id: itd-2609152113364815 +slug: testimony-maps-a-confirmed-terminal-session-finding-to-the-c +spec_id: null +kind: null +suggested_kind: null +reclassification_history: [] +builds_on: [itd-3, itd-11] +severity: minor +impact: additive +origin: researcher-authored +production_mode: hand-written +--- + +# Testimony maps a confirmed terminal-session finding to the command that produced it + +## Press Release + +> _Seeded from a quoted-text intent capture. Expand into the full press-release narrative before planning._ + +## Why This Matters + +Split from itd-3 (codebase mapping) during its planning interview on 2026-09-15. Web findings carry a selector or route anchor; a terminal finding carries neither, so the mapping step for terminal sessions is a different problem and is recorded on its own rather than deferred silently inside itd-3. + +## Mechanism + +> _Prompted (the claim-recording gradient): why the authors expect this to work, as a falsifiable "we expect X because Y" — not the outcome restated. Replace this line with the claim, or with the exact token `None stated.` alone on its line to record the claim as considered and declined._ + +## Scope Conditions + +> _Required (the claim-recording gradient): the population, platform, scale, or assumptions this claim holds under, one per top-level bullet — `abcd intent plan` stamps each with a persistent identity. Replace this line with those bullets, or with the exact token `None stated.` alone on its line._ + +## Acceptance Criteria + +> _Required (the itd-1 discipline): add at least one Given-When-Then bullet describing the verifiable bar for "shipped" before this draft can be planned._ + +## Open Questions + +- A terminal finding has no `ui` anchor field: `terminal_output` timeline records carry only `t`, `kind`, and `text`. The only anchor is the finding's quote and its cited evidence ids, so what the request hands the host, and what ingest can verify, both need deciding. +- Timeline text keeps its escape sequences verbatim and is stripped only at `report`'s render boundary. Anything that searches the text for a command string must strip control bytes first, and nothing does so today. +- `terminal.cast` is archival: nothing downstream reads it. Anchoring must work from the timeline, not the cast. + +## Audit Notes + +_Empty. Populated by intent-auditor when intent moves to shipped/._ diff --git a/.abcd/development/intents/drafts/itd-3-codebase-mapping.md b/.abcd/development/intents/drafts/itd-3-codebase-mapping.md deleted file mode 100644 index 3ca4c03..0000000 --- a/.abcd/development/intents/drafts/itd-3-codebase-mapping.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: itd-3 -slug: codebase-mapping -spec_id: null -severity: major ---- - -# Findings Land in the Code, Not Just the Report - -## Press Release - -> **Testimony maps confirmed findings back to the codebase.** An agentic mapping step resolves each finding's UI anchor — a `data-testid` attribute, a CSS selector, a route, or the command text visible in a terminal cast — to source locations, attaching `code_refs` with a confidence level. Where the evidence supports it, the step drafts an issue: title, reproduction steps reconstructed from the event window, the participant's quote as user evidence, and the suspected file. A tester's spoken "I expected this button to save immediately" arrives in the tracker pointing at the component that owns the button. -> -> "The gap was always between 'users struggled here' and 'this file, this handler'," said Alice, the maintainer. "Now a finding comes to me with the selector, the route, the suspect file, and the person's own words. I start fixing instead of reconstructing." - -## Why This Matters - -Session findings that stop at a report require a second act of translation before anyone can act on them, and that translation is where evidence goes stale. Anchoring interactive elements with stable `data-testid` attributes at build time makes selectors near-deterministic grep targets, so the translation can be automated — turning a qualitative observation into a workable, evidenced issue while the session is still fresh. This is the deliberately last and most novel step of the pipeline, kept separable so it can iterate without destabilising capture or analysis. - -## What's In Scope - -- The `data-testid` build-time convention for our own web apps, documented as the anchoring contract. -- The agentic mapping step: selectors and test IDs grepped directly; routes resolved through the router table; CLI findings anchored on command/flag/output text from the cast stream. -- `code_refs` written back onto findings with `high | medium | low` confidence. -- Drafted issues (title, repro from the event window, quote as evidence, suspected file) for findings that warrant them. - -## What's Out of Scope - -- Automatic filing of issues into a tracker without human review. -- Native-app accessibility-identifier instrumentation beyond using identifiers already present. -- Mapping for third-party apps — Mode B has no codebase access by definition (itd-4). -- Seeding regression tests from confirmed findings; noted as an open question, not built here. - -## Acceptance Criteria - -- **Given** a confirmed finding with a `data-testid` selector anchor, **when** the mapping step runs against the app's repository, **then** the finding gains at least one `code_refs` entry with a file path and confidence level. -- **Given** a finding from a CLI session, **when** the mapping step runs, **then** the anchor is resolved from the command or output text in the cast stream to the owning source location. -- **Given** a mapped finding, **when** an issue draft is produced, **then** it contains a title, reproduction steps from the event window, the participant quote, and the suspected file. diff --git a/.abcd/development/intents/planned/itd-3-codebase-mapping.md b/.abcd/development/intents/planned/itd-3-codebase-mapping.md new file mode 100644 index 0000000..730292a --- /dev/null +++ b/.abcd/development/intents/planned/itd-3-codebase-mapping.md @@ -0,0 +1,72 @@ +--- +id: itd-3 +slug: codebase-mapping +spec_id: spc-2609152218094570 +kind: standalone +suggested_kind: null +reclassification_history: [] +builds_on: [itd-2, itd-9] +severity: major +--- + +# Findings Land in the Code, Not Just the Report + +## Press Release + +> **Testimony maps confirmed findings to the code that owns them.** Once a human has confirmed a finding, the mapping step hands its selector, route, and event window, together with the path to the application's repository, to the operator's chosen model. The model answers with the source locations it believes own that anchor. Testimony checks each answer against the repository, the file exists and the line is in range, and records it as a proposed reference. From a mapped finding, Testimony renders an issue draft: a title, reproduction steps from the event window, the participant's own words as evidence, and the suspected file. A person accepts or rejects each reference, and the decision is kept beside it. Nothing is filed anywhere, and nothing is written into the application's repository. +> +> "The gap was always between 'users struggled here' and 'this file, this handler'," said Alice, the maintainer. "Now a finding comes to me with the selector, the route, the suspect file, and the person's own words. I start fixing instead of reconstructing." + +## Why This Matters + +Session findings that stop at a report require a second act of translation before anyone can act on them, and that translation is where evidence goes stale. Anchoring interactive elements with stable `data-testid` attributes at build time makes selectors near-deterministic grep targets, so the translation can be delegated to a model and checked by the CLI, turning a qualitative observation into a workable, evidenced issue while the session is still fresh. + +The step sits downstream of verification, as regression-test drafting (itd-9) does: only a finding a human has confirmed is mapped, every reference is born a proposal, and the human decision on it is retained beside the machine record. The mapping is the last step of the pipeline, kept separable so it can iterate without destabilising capture or analysis. + +## What's In Scope + +- Emitting a mapping request over each confirmed finding that carries a selector or route anchor: the anchor, the participant's quote, the event window, and the path to the application's repository. +- Ingesting the answer into a new record family, `refs.jsonl`, beside `findings.jsonl` and `tests.jsonl`: each reference names a confirmed finding and a repo-relative path that exists under the repository, with any line within the file's length, and is born `proposed`. +- A human accept / reject pass over each reference, appended non-destructively as verdicts and test decisions are, so the reference stays linked to its finding and session. +- Rendering an issue draft from a mapped finding: a title, reproduction steps from the event window, the participant's quote, and the suspected file. +- Reading the application's repository only to verify that a returned path exists and a line is in range. + +## What's Out of Scope + +- Automatic filing of issues into a tracker without human review. +- Anchoring a finding from a terminal session on the command or output text in the timeline; a terminal finding carries no selector or route, and the timeline text keeps its escape sequences, so that is its own draft (itd-2609152113364815). +- A confidence level on a reference. Ingest cannot validate a model-asserted word, and the human decision is the only quality signal. +- Grepping, router-table resolution, or any other reasoning about the application's source inside the CLI; resolution is the host's job. +- Writing anything into the application's repository. +- Native-app accessibility-identifier instrumentation beyond using identifiers already present. +- Mapping for third-party apps; Mode B has no codebase access by definition (itd-4). +- The `data-testid` anchoring convention itself, which the how-to for instrumenting your own app documents. + +## Scope Conditions + +- The session is a web session and at least one confirmed finding carries a selector or route anchor. A session with none is refused loudly, not mapped. +- The application's repository is available locally and passed by path. Testimony reads it only to verify that a returned path exists and a line is in range, and never writes into it. +- The application anchors interactive elements with stable `data-testid` attributes. Without them, resolution rests on selectors that may not survive a rebuild. +- The host that answers the mapping request is the operator's chosen model. The CLI emits the request, never calls a model, holds no keys, and adds no network dependency. +- `timeline.jsonl` is present, so each mapped finding's event window resolves for the issue draft. + +## Mechanism + +We expect a `data-testid` selector or a route, handed to a model with the repository path, to resolve to the owning component because stable test ids are near-deterministic grep targets in the application's own source. It would be shown wrong by a high reject rate on references, or by references that land on the test suite instead of the component. + +## Acceptance Criteria + +- **Given** a session with a confirmed finding that carries a selector or route anchor, and a path to the application's repository, **when** the mapping request is emitted, **then** it carries that finding's anchor, quote, event window, and the repository path, and no finding that is unverified, rejected, or duplicate. +- **Given** a mapping answer from the host, **when** it is ingested, **then** each reference that names a confirmed finding and a repo-relative path that exists under the repository, with any line within the file's length, is written to `refs.jsonl` with status `proposed`, and an answer in which any reference fails a check is refused as a whole with every error reported. +- **Given** a mapped finding, **when** an issue draft is produced, **then** it contains a title, reproduction steps from the event window, the participant quote, and the suspected file. +- **Given** a proposed reference, **when** a human accepts or rejects it, **then** the decision is appended to `refs.jsonl` without rewriting the reference line, and the reference stays linked to its finding and session. +- **Given** a session with no confirmed finding that carries a selector or route, **when** the mapping step runs, **then** it refuses with the finding count by status and writes nothing. + +## Open Questions + +- Does a route anchor resolve as reliably as a test id? A route lives in a router table the model must read and interpret, where a test id is a literal string to search for. +- Should an accepted reference feed the regression-test draft (itd-9), so a test arrives with the file it exercises already named? + +## Grounds + +- pursued: itd-9 proved the emit, ingest, review, render shape on drafts, and mapping is the last pipeline step that turns a finding into something a developer opens in an editor; it would be shown wrong if reviewers reject most references, or if the issue drafts are not what a maintainer actually files diff --git a/.abcd/development/research/notes/2026-09-15-decomposition-calibration.md b/.abcd/development/research/notes/2026-09-15-decomposition-calibration.md new file mode 100644 index 0000000..cdf1ef9 --- /dev/null +++ b/.abcd/development/research/notes/2026-09-15-decomposition-calibration.md @@ -0,0 +1,35 @@ +# Decomposition calibration note + +Hand-run itd-84 decompositions, graded against the maintainer's confirmation. +One entry per proposal. The corpus this note builds gates the automated rung. + +## 2026-09-15: itd-3 codebase mapping (planning interview) + +Initial routing, proposed before the interview: + +| Part | Type | Home | +|---|---|---| +| Web-anchor mapping: emit over confirmed findings with selector, route, window, repository path; ingest validates each reference into a new record family; human accept or reject | intent | itd-3 | +| Issue draft rendered from a mapped finding | intent | itd-3, or split off | +| Terminal-anchor mapping over timeline text | intent | new draft, or a recorded deferral in itd-3 | +| `data-testid` anchoring convention for the app under test | docs | a how-to page, not a CLI capability | +| Third record family through the shared append primitive and review verb | ADR | refines adr-1 | + +Typed links proposed: `builds_on` itd-2 and itd-9; `refines` adr-1; unblocked +by itd-11. No reversal flagged. + +Verdict proposed: SPLIT. + +Confirmed routing: + +| Part | Type | Home | Survived? | +|---|---|---|---| +| Web-anchor mapping | intent | itd-3 | yes | +| Issue draft | intent | itd-3 (kept in; the maintainer chose the draft to render from any mapped finding, not only an accepted reference) | yes, with a scope change | +| Terminal-anchor mapping | intent | new draft itd-2609152113364815, filed in the same session | yes; the maintainer chose the spin-off over the deferral | +| `data-testid` convention | docs | already documented in `docs/how-to/instrument-your-own-app.md`; no new work | routing survived, but the part was already home | +| Third record family | ADR | refines adr-1; recorded in the spec rather than a new ADR, since adr-1 already names the pattern | yes | + +Verdict adopted: SPLIT. The initial routing survived confirmation in full; the +two corrections were within a part (the issue draft's trigger) and a +discovery that one part needed no work (the convention doc already exists). diff --git a/.abcd/development/specs/open/spc-2609152218094570-codebase-mapping.md b/.abcd/development/specs/open/spc-2609152218094570-codebase-mapping.md new file mode 100644 index 0000000..9819867 --- /dev/null +++ b/.abcd/development/specs/open/spc-2609152218094570-codebase-mapping.md @@ -0,0 +1,380 @@ +--- +id: spc-2609152218094570 +slug: codebase-mapping +intent: itd-3 +origin: researcher-authored +production_mode: hand-written +--- +# codebase-mapping + +## Summary + +`testimony map` adds the mapping step that turns a **confirmed** web-session +finding into proposed references to the application's source, and +`testimony review -kind refs` adds the human accept / reject pass over each +reference. The resolution stays **host-delegated**, exactly as for `analyze` and +`draft-tests`: the CLI never calls a model, holds no keys, and adds no network +dependency. `map -session DIR -repo DIR` *emits* one self-contained mapping +request: a versioned rubric, the session context, the repository path, and, for +each confirmed finding that carries a selector or route anchor, that finding's +record plus its event window from `timeline.jsonl`. Any agent host or human +resolves the anchors against the repository and saves the JSON answer. +`map -session DIR -repo DIR -ingest FILE` is the **validation boundary**: every +reference must name a finding whose current status is `confirmed` and whose +`ui` carries a selector or route, a repo-relative path that exists as a regular +file under `-repo`, and, when a line is given, a line within that file's +length. Ingest writes `refs.jsonl`, a third record family beside +`findings.jsonl` and `tests.jsonl`, with every reference forced to +`status: proposed`. `review -kind refs` walks the proposed references (or takes +one decision non-interactively) and records `accepted | rejected` as appended, +non-destructive records. `map -session DIR -render` renders an issue draft per +mapped finding to stdout: a title, reproduction steps from the event window, +the participant's quote, and the suspected files. + +Nothing is filed anywhere, and the CLI never writes into the application's +repository: `-repo` is opened read-only, for existence and line-count checks. + +A session with no confirmed finding carrying a selector or route is **staged +loudly**: `map` refuses, prints the finding count by status, exits non-zero, +and writes nothing. + +Every test in this slice is fixture-based and hermetic; stdlib only. The +package is `internal/coderefs`. + +## Design + +### Decisions carried in from the planning interview + +- **Web anchors only.** A finding is mappable iff its effective status is + `confirmed`, its `mode` is not `B`, and its `ui` carries a non-empty + `selector` or `route`. Terminal findings carry no `ui` and are the subject of + their own draft (itd-2609152113364815); this slice does not read + `terminal_output` text. +- **A separate `refs.jsonl`.** The finding schema is closed and `analyze + -ingest` refuses to rewrite a verdict-bearing `findings.jsonl`, so nothing is + written onto a finding. The reference is its own record family through the + ADR 0001 primitives (`session.CommitRecords` for the machine record, + `session.AppendRecord` for the human decision) and its own `review -kind`. +- **No confidence field.** A model-asserted `high | medium | low` cannot be + validated at the boundary. The reference carries what ingest verified and the + human decision is the only quality signal. An unknown field, `confidence` + included, is refused by `DisallowUnknownFields`. +- **The issue draft renders from a mapped finding**, not only from an accepted + reference. The render lists every reference for the finding with its current + status, so the reader sees which paths a person has already vouched for; + review does not gate the render. +- **Resolution is the host's job.** The CLI hands over the anchor and the + repository path; it never greps, never reads a router table, and never + interprets the source. Its only reads of the repository are the existence + and line-count checks at ingest. + +### Eligibility + +`map` calls `analyze.Load(dir)` and `analyze.EffectiveStatus(findings, +verdicts)`, the single helper `review`, `report`, and `draft-tests` already use, +so a finding confirmed then later rejected is not eligible and one rejected then +later confirmed is. `duplicate` and `unverified` are never eligible. On top of +the `draft-tests` rule, a finding whose `ui` is absent, or whose `selector` and +`route` are both empty, is not eligible: there is no anchor to hand over. + +The refusal names both counts, so an operator can tell a session with no +confirmed finding from one whose confirmed findings carry no anchor: + +``` +map: no mappable finding in 5 findings: 1 confirmed (0 with a selector or route), 3 unverified, 0 duplicate, 1 rejected +``` + +### CLI surface + +``` +testimony map -session DIR -repo DIR [-window 10] [-out FILE] emit the mapping request (stdout default) +testimony map -session DIR -repo DIR -ingest FILE validate answer JSON → refs.jsonl (FILE may be "-" for stdin) +testimony map -session DIR -render [-out FILE] render an issue draft per mapped finding (stdout default) + +testimony review -session DIR -kind refs interactive accept/reject walk +testimony review -session DIR -kind refs -ref R-NNN -decision accepted|rejected +``` + +`map` runs in exactly one mode, following `draft-tests`' rule: emit (neither +`-ingest` nor `-render`), ingest (`-ingest`), or render (`-render`). `-ingest` +combines with neither `-out` nor `-render`; `-window` belongs to emit alone and +is refused with `-ingest` or `-render` rather than silently ignored. `-repo` is +required for emit and ingest and refused with `-render`, which reads only what is +on disk in the session. `-repo` must name an existing directory; a path that is +not a directory, or an explicitly empty value, is refused at exit 2. Every flag +follows the existing gauntlet: `-session` required or inferred from the current +directory; empty explicit values refused; a non-finite `-window` refused; no +positional arguments. + +| Flag | Default | Meaning | +|---|---|---| +| `-session` | *(required, or inferred)* | session directory | +| `-repo` | *(required for emit and ingest)* | the application's repository root; read only | +| `-window` | `10` | emit mode: the event-window half-width in seconds, as `draft-tests` | +| `-out` | *(stdout)* | emit/render mode: write to `FILE` instead of stdout | +| `-ingest` | *(off)* | ingest mode: validate the answer JSON at `FILE` (or `-`) into `refs.jsonl` | +| `-render` | *(off)* | render mode: write the issue drafts as Markdown | + +`review` gains `refs` as a third value of `-kind` and two flags: + +| Flag | Default | Meaning | +|---|---|---| +| `-kind` | `findings` | `findings`, `tests`, or `refs` | +| `-ref` | *(interactive)* | non-interactive: the reference to decide (`R-NNN`), `-kind refs` only | +| `-decision` | *(interactive)* | non-interactive: `accepted` or `rejected`; `edited` is refused with `-kind refs` | + +Flags belonging to another record family (`-finding`, `-verdict`, `-test`, +`-edit`) are refused with `-kind refs` at exit 2, checked in `internal/cli` and +again in `review.Run`, as the `tests` kind already is. `-kind findings` and +`-kind tests` are byte-for-byte unchanged. + +Reads by mode: emit reads `manifest.json`, `timeline.jsonl`, and +`findings.jsonl`; ingest reads `manifest.json`, `findings.jsonl`, and the +repository (existence and line counts only); render reads `manifest.json`, +`timeline.jsonl`, `findings.jsonl`, and `refs.jsonl`. Emit hints to run `merge` +first when the timeline is missing; every mode hints to run `analyze -ingest` +first when there is no `findings.jsonl`; render and `review -kind refs` hint to +run `map -ingest` first when there is no `refs.jsonl`. + +### The event window + +Reused, not reimplemented: `drafttests.Window(entries, f, window)` is the one +helper that turns a finding's cited evidence into a time-ordered slice of the +timeline, and the issue draft's reproduction steps need exactly the window the +test draft needs. The default half-width is 10 seconds for the same reason +`draft-tests` chose it: a repro needs the lead-up and the aftermath. + +### `map` — emitting the request (host-delegated) + +`EmitRequest(dir, repo string, window float64) (string, error)` builds one +self-contained prompt, mirroring `drafttests.EmitRequest`: + +1. **Rubric header**: `Testimony code-mapping rubric: testimony-coderefs/v1`, + with `RubricVersion` a package constant. +2. **Stance**: every reference is a proposal, born `proposed`; a human accepts + or rejects it. Only the confirmed findings below are eligible. Resolve each + anchor by reading the repository at the path given; never invent a path, + never guess a line, and omit a finding rather than answer it with a path + that does not exist. Never alter the quote. +3. **Instructions**: zero or more references per finding, in finding-id order; + each names the finding, a repo-relative path (forward slashes, no leading + `/`, no `..` segment), an optional 1-based line, and one `role` from the + closed set `owner | handler | route | test` (the file that renders the + element, the code that handles its interaction, the router entry for the + route, or a test that exercises it). The role is the model's description of + *why* the path is relevant, which the human reads; it is validated as a + member of the set, never for truth. +4. **Rubric body**: field definitions, the hard constraints restated as the + rules ingest enforces, and the `status` explanation `draft-tests` already + carries (the record is shown verbatim, so its `status` reads `unverified`; + the confirming verdict is named in the header). +5. **Repository**: the absolute path passed as `-repo`, as the host will open + it, plus a note that the CLI verifies existence and line counts at ingest. + This is the one place an absolute local path appears in an emitted request; + it is the operator's own machine's path, written to a file the operator + chooses, and it is never written into the session directory. +6. **Session context**: manifest `app`, `participant`, and ordered `tasks`. +7. **Eligible findings**: per finding, in id order: a prose header naming its + id, `type`, `severity`, clock, the selector and route, and the date of the + confirming verdict; the finding's own JSON line in a ```jsonl fence; its + event window as a ```jsonl fence. +8. **Required output shape and worked example**: + + > Answer with a single JSON document: `{"rubric":"testimony-coderefs/v1","refs":[ … ]}`. A bare top-level array is also accepted. Output JSON only. + + ```json + {"rubric":"testimony-coderefs/v1","refs":[ + {"id":"R-001","finding":"F-001","session":"sample-session", + "path":"src/settings/ProfileForm.tsx","line":48,"role":"owner", + "status":"proposed"}, + {"id":"R-002","finding":"F-001","session":"sample-session", + "path":"src/settings/saveProfile.ts","line":12,"role":"handler", + "status":"proposed"} + ]} + ``` + +**Escaping** follows the established rule: prose and list-item values go +through `session.SafeInline`; fenced values through `session.SafeText`. The +repository path goes through `SafeInline` too. + +Nothing in the session directory is mutated by emit. + +### Reference record — the `Ref` type + +One reference per line of `refs.jsonl`. Reference lines carry no `kind` field; +the schema is closed (`DisallowUnknownFields`). + +| Field | Type | Required | Validation at ingest | +|---|---|---|---| +| `id` | string | yes | matches `^R-\d{3}$`; unique within the answer and the file | +| `finding` | string | yes | names a finding in `findings.jsonl` whose effective status is `confirmed`, whose `mode` is not `B`, and whose `ui` carries a selector or route | +| `session` | string | yes | equals the manifest's `session` | +| `path` | string | yes | repo-relative: non-empty after `SafeText`+trim, at most 512 bytes, forward slashes, no leading `/`, no `.` or `..` segment, no NUL; after `filepath.Join(repo, path)` and `filepath.Rel` back, still inside `repo`; names an existing **regular file** (symlinks are not followed: `os.Lstat`) | +| `line` | int | no | when present, `1 <= line <= lines(file)`, where `lines` counts `\n`-terminated lines plus an unterminated last line, reading at most `session.MaxJSONLBytes`; a file larger than that bound is refused for a line check rather than counted | +| `role` | string | yes | one of `owner \| handler \| route \| test` | +| `status` | string | no on input, always present on disk | ignored on input and forced to `"proposed"` | + +The field is `finding`, following the convention the verdict and decision +records set: the field is named after the kind of record it points at. The +decision record below names its referent `ref`. + +**Why the path check is a containment check and not a string check.** A model +answer is untrusted input naming a filesystem path the CLI will open. The +`..`-segment rule catches the obvious escape; the `Rel`-back-inside-`repo` rule +catches the ones a string rule misses (a path through a symlinked directory, +an absolute path on a platform where `Join` does not neutralise it). `Lstat` +rather than `Stat` means a symlink inside the repository pointing outside it is +refused as "not a regular file" rather than followed. The CLI opens the file +only when `line` is present, only to count lines, and only up to the shared +byte bound. + +**Why there is no `quote` on the reference.** The test draft carries +`rationale_quote` because a test is a document a person reads on its own. A +reference is a pointer; its finding carries the quote, and the render joins +them. Copying the quote onto every reference would be a second place for the +evidence to drift. + +### `map -ingest FILE` — the validation boundary + +1. Load `manifest.json` and `findings.jsonl`; compute effective status; build + the eligible set `id → finding`. If empty, refuse loudly before reading a + byte of the answer. +2. Read `FILE` (or stdin) bounded by `session.MaxAnswerBytes`. Accept an object + with a `refs` array or a bare array. `rubric`, when present, must be known. +3. Decode each reference with `DisallowUnknownFields` through a `rawRef` whose + `Line` is `*int`, so an absent line is distinguishable from `0`. +4. Run every rule in the table. Validation is **transactional and exhaustive**: + all errors across all references are collected, each naming the reference by + id or answer position, the field, and the offending value; if any exists, + nothing is written and the exit is non-zero. +5. Force `status: "proposed"`; pre-flight the serialised size against + `session.MaxJSONLLine` per line and `session.MaxJSONLBytes` in total. +6. Commit through `session.CommitRecords` with a guard that refuses to + overwrite a `refs.jsonl` already holding any `kind:"decision"` line, exactly + as `commitDrafts` protects `tests.jsonl`. +7. Print `validated N references → (all proposed)`. + +An empty `refs` array is refused rather than written, so a bad answer cannot +erase a prior `refs.jsonl`. A model that found nothing for a finding omits it; +an operator who wants that recorded has the refusal message and the request. + +### The human pass — `review -kind refs` + +**Decision record**, appended to `refs.jsonl` through `session.AppendRecord`, +never an in-place rewrite: + +```json +{"kind":"decision","ref":"R-001","decision":"accepted","at":"2026-09-15"} +{"kind":"decision","ref":"R-002","decision":"rejected","at":"2026-09-15"} +``` + +| Field | Type | Validation | +|---|---|---| +| `kind` | string | literal `"decision"` | +| `ref` | string | an existing reference id in the file | +| `decision` | string | `accepted \| rejected`; there is no `edited`, because a wrong path is rejected and a corrected one is ingested, never patched | +| `at` | string | ISO date `YYYY-MM-DD` | + +The interactive walk shows, per proposed reference, the finding's quote and +anchor, the path, line, and role, and the first lines around `line` from the +repository when `-repo` is given (read-only, bounded), so the reviewer can +judge without leaving the terminal. Without `-repo` the walk shows the record +alone. + +`EffectiveStatus(refs, decisions)`: every reference starts `proposed`; decisions +apply in file order and the last one wins; a decision naming an unknown +reference or carrying a value outside the enum is ignored for display. + +### `map -render` — the issue draft + +One Markdown block per mapped finding, in finding-id order, to stdout or +`-out`: + +```markdown +## F-001: Saving gives no confirmation + +**Severity** 3 · **Anchor** `[data-testid=save-btn]` on `/settings/profile` + +> "I clicked save and nothing happened" +> Alice, 00:22 + +### Steps to reproduce +1. Open /settings/profile. +2. Click the display-name field ([data-testid=display-name]). +3. Type "Alice". +4. Click Save ([data-testid=save-btn]). + +### Suspected files +- `src/settings/ProfileForm.tsx:48` (owner) — accepted 2026-09-15 +- `src/settings/saveProfile.ts:12` (handler) — proposed + +Session `sample-session` · finding F-001 · references R-001, R-002 +``` + +The title is derived by the CLI, not the model: the finding's `type` and the +first clause of its quote, capped at 80 characters, because a title is +presentation and the render must work from the records alone. The steps are +derived from the window the way `draft-tests`' rubric instructs the model to, +but here by the CLI: one orientation line from the first event's route, then +one imperative line per event in the window at or before the finding's `t`, +naming the selector where the event carries one. Steps the CLI derives are +mechanical and may read flatly; that is the trade for a render that needs no +second model round-trip and cannot invent a step. Every reference for the +finding is listed with its current status, so a draft rendered before review is +visibly unreviewed. + +Nothing is written into the application's repository, and the render does not +open it. + +### Docs and record + +- `docs/reference/cli.md`: `map` and the `review -kind refs` additions. +- `docs/reference/session-directory.md`: `refs.jsonl`. +- `docs/how-to/map-findings-to-code.md`: the four-step flow. +- `examples/sample-session`: a `refs.jsonl` with one accepted and one proposed + reference, and a `request.md` fixture; the sample references point into a + small fixture tree under `internal/coderefs/testdata/repo/` rather than into + Testimony's own source, so the smoke run in CI does not depend on the layout + of this repository. +- `CHANGELOG.md` `[Unreleased]`: one Added entry. +- The change that lands this closes the spec: `abcd spec close + spc-2609152218094570 --impact additive`. + +## How the acceptance criteria are satisfied + +1. **The emitted request carries the anchor, quote, window, and repository + path, and no ineligible finding.** `EmitRequest` renders only the eligible + set computed from `EffectiveStatus` plus the anchor rule; each finding's + header names selector and route, its JSON line carries the quote, its window + follows, and the repository section carries `-repo`. Tested over the sample + session with F-001 confirmed and anchored, F-002 unverified, F-003 rejected, + F-005 duplicate, and a confirmed finding with no `ui`: exactly F-001 appears. +2. **Ingest verifies finding, path, and line, and refuses the whole answer on + any failure.** The `Ref` table is enforced field by field; the path check is + the containment-plus-`Lstat` rule; the line check counts the file. Tests + cover: an unconfirmed finding, an anchorless finding, a `..` path, a symlink + out of the repository, a directory, a missing file, a line past the end, an + unknown field (`confidence`), and a second good reference in the same answer + that is not written because the first failed. +3. **An issue draft from a mapped finding carries title, steps, quote, and + suspected file.** `Render` derives the title, steps from `Window`, the + quote from the finding, and lists every reference. Tested against a golden + file from the sample session, and against a session whose `refs.jsonl` holds + a proposed-only reference to show the render does not wait for review. +4. **A decision is appended without rewriting the reference line, and the link + survives.** `AppendDecision` goes through `session.AppendRecord`; the + round-trip test asserts the reference line is byte-identical after two + decisions; there is no edit path, so `finding`, `session`, and `path` are + unreachable by any later write. +5. **A session with no mappable finding is refused with the tally and writes + nothing.** The eligibility check runs first on every write path; the test + strips the sample's verdicts and asserts exit 1, the by-status line with the + anchored count, and no `refs.jsonl`. + +## Open questions carried on the intent + +- Route anchors: the first slice hands the route over and lets the human + decide; the accept rate on `role: route` references against `role: owner` + ones is the measurement, and it is readable from `refs.jsonl` alone. +- Feeding accepted references into `draft-tests`: not built here. When it is, + the reference id is the link, and the render above already shows the join. diff --git a/.abcd/work/DECISIONS.md b/.abcd/work/DECISIONS.md index 1aaaded..0ae6761 100644 --- a/.abcd/work/DECISIONS.md +++ b/.abcd/work/DECISIONS.md @@ -1708,3 +1708,15 @@ Architecture-shaping decisions graduate to an ADR under an exchange unit. The quality-floor question the draft raised is deferred to the retained verdicts, which now become a per-backend comparison, rather than built as an unmeasured number the CLI could not enforce anyway. +- 2026-09-15 — itd-3 (codebase mapping) planned as a SPLIT: web anchors only + (a confirmed finding whose `ui` carries a selector or route), with terminal + anchoring spun off as its own draft (itd-2609152113364815) rather than + deferred silently. References land in a separate `refs.jsonl` record family + through the ADR 0001 primitives, never on the finding, because the finding + schema is closed and `analyze -ingest` protects a verdict-bearing file. No + confidence field: ingest cannot validate a model-asserted word, so the human + accept / reject decision is the only quality signal. Resolution is the host's + job; the CLI reads the application repository only for existence and + line-count checks and never writes into it. The issue draft renders from any + mapped finding, listing every reference with its current status, so review + does not gate the render. Spec: spc-2609152218094570.