Skip to content

fix: the capture ledger says what it skipped, stays inside the checkout, and defers a record past one cut - #723

Merged
REPPL merged 38 commits into
mainfrom
fix/capture-cluster
Sep 26, 2026
Merged

REPPL merged 38 commits into
mainfrom
fix/capture-cluster

Conversation

@REPPL

@REPPL REPPL commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator

With this change, the capture ledger says what it skipped, what it has not committed and which ledger it wrote to, keeps every write inside the checkout, and abcd capture defer carries a major or critical record past one release cut.

  • capture status and list count every record they skip and name the layer that refused it. An unreadable record bucket is reported as unreadable, not read as empty.
  • A derived slug always satisfies its own validator.
  • Hidden runes (bidi overrides, zero-width characters) are encoded when a record is written. That covers the capture body, found_at and found_during, resolve and wontfix notes, grounds, the defer reason, and an intent's title, press text and body. Line structure is kept. A derived wontfix grounds line is validated like a typed one.
  • Every ledger write, removal, placeholder and the allocator lock go through os.Root on the ledger's base. A symlinked lock, a symlinked open/ or a record swapped for a symlink is refused, and nothing is written outside the checkout. Reading and disposition records are written the same way. The ledger-root marker walk is deleted: discovery goes through git and refuses a marker git will not answer for.
  • capture promote no longer panics on an orphan draft that has no grounds. Its remedy is the whole command, in a delimiter the text cannot close. A lost promote race names the duplicate draft.
  • A quoted impact reaches the same verdict as the bare word in capture and in the record gate.
  • capture and its status board say when a record is not yet committed. Every ledger verb names the checkout and branch whose ledger it addressed, on stderr, and in a ledger member in --json. A capture with no --found-at says it names no location in this checkout.
  • abcd capture defer <iss-N> --after <vX.Y.Z> --reason <text> writes deferred_after and deferral_reason, and the release cut honours them. It refuses a minor or nitpick record, a record that is not open, an empty reason and an --after that is no release tag. The verb takes its help sentence from the surface manifest.
  • A closed-set refusal (--severity, --category and the rest) names the flag and its accepted set.
  • The restamp gate parses the origin rather than searching for it, and refuses an origin outside the vocabulary.

Review: Fable 5.1, one round, SHIP. Its four follow-ups are captured and open: iss-2609251823551349, iss-2609251823555125, iss-2609251823559111, iss-2609251823560369. The branch was then merged with main at 85a10e1. Two files conflicted and were resolved hunk by hunk.

Reading-corpus change. The windows are recalibrated at the merged tip (68deae0): widening 1,040,000 to 1,060,000, detection 1,050,000 to 1,060,000, and entailment's measured fields only.

Resolves: iss-258
Resolves: iss-260
Resolves: iss-2608261133218490
Resolves: iss-2608290810037524
Resolves: iss-2608300941548519
Resolves: iss-2608301206073609
Resolves: iss-2608301244450106
Resolves: iss-2609012037143368
Resolves: iss-2609020154474224
Resolves: iss-2609090947359464
Resolves: iss-2609100508570527
Resolves: iss-2609120452071388
Resolves: iss-2609181223260994
Resolves: iss-2609202053570475
Resolves: iss-2609231156260287
Resolves: iss-2609251208394294
Refs: iss-2609120511058115
Refs: iss-2609251151293535
Refs: iss-2609251235119402
Refs: iss-2609251455354719
Refs: iss-2609251823551349
Refs: iss-2609251823555125
Refs: iss-2609251823559111
Refs: iss-2609251823560369
Assisted-by: Claude:claude-opus-5-5

…ng layer

The bare status board excluded a record the reader refused from all three
totals and printed a bare error beside it, so a reader could not tell
whether the writer or the validator was the side that was wrong, and the
header under-reported the ledger. Each SkipRecord now carries the reader
layer that refused it (filename, read, frontmatter, schema, invariant),
StatusResult carries skipped_count, and the header counts skipped records
beside the totals. The list and mentions renders name the layer too.

The other halves of the record were already fixed at the base: the slug
truncation trims its separator (recordid.Slug) and the --severity,
--category and --source help names each vocabulary (enumHelp).

Refs: iss-2609120452071388
Assisted-by: Claude:claude-opus-5-5
…ator

The property half of iss-2609120452071388's acceptance: 20000 seeded
adversarial inputs (separator runs, punctuation, non-ASCII, lengths either
side of the 60-character budget) each derive a slug the reader's SlugRe
accepts and that normalisation leaves unchanged. The trim that makes it
hold already stands in recordid.Slug; the property had no test.

Refs: iss-2609120452071388
Assisted-by: Claude:claude-opus-5-5
…er-named

The slug and help halves were fixed before this lane; the skip count and
layer diagnostic land in 8c09f03. The autonomous-hunt vocabulary question
is captured separately as iss-2609251151293535 for a product ruling.

Resolves: iss-2609120452071388
Refs: iss-2609251151293535
Assisted-by: Claude:claude-opus-5-5
A bidi override, zero-width rune, C1 control or DEL typed into a capture
body, a resolution or wontfix note, a grounds entry or an intent headline
reached the committed record verbatim. termsafe gains
EncodeHiddenRunesBlock, the multi-line form of EncodeHiddenRunes that
keeps the line feed, the tab and a CRLF pair, and every ledger and
intent-draft free-text write now passes through one of the two after
redaction. grounds.New encodes the text it validates, so both grounds
writers are covered at one site.

The grounds a wontfix derives from its reason now go through
grounds.NewDerived, which applies ValidateText's control-character check
(held once as validateControl) and the encoding, leaving only the
substance floor off. A control character in a wontfix reason is refused
at the grounds boundary before any write, not by the serialiser under the
ledger lock.

Refs: iss-2608301206073609, iss-2608301244450106
Assisted-by: Claude:claude-opus-5-5
…write

Resolves: iss-2608301206073609
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2608301244450106
Assisted-by: Claude:claude-opus-5-5
The ledger's writes were absolute-path operations, each guarded by its own
Lstat or O_NOFOLLOW check, which left the window between a check and the
write it licensed to a local racer: an ancestor swapped for a symlink in
that window carried a directory or a record out of the checkout.

Every ledger write now resolves inside an os.Root opened on the checkout
(ledgerroot.go): the directory walk creates through root.Mkdir, the
placeholder is an exclusive no-follow create in the root, the commit, the
transition, the link, promote and migrate stamps write through
WriteFileAtomicPreserveModeInRoot, the removals go through root.Remove,
and the allocator lock opens through the new fsutil.WithFileLockIn. That
lock open uses the plain-open / exclusive-create sequence AppendLineIn
already uses, because a non-exclusive openat(O_CREAT) races to ENOENT on
darwin. The per-segment Lstat walk still refuses a committed symlink by
name. A test seam (ledgerRaceHook) drives the swap at the mkdir and write
windows.

reading.go's own directory provisioning keeps its path-operand call:
safeMkdirLeaf stays path-based (now resolved inside a root on the parent)
so that file is untouched.

Refs: iss-2609012037143368
Assisted-by: Claude:claude-opus-5-5
The two ledger writes left in reading.go (a reading run's records and a
disposition) go through the same os.Root helper as every other ledger
write. Three lines; the file's symlink guard is untouched.

Refs: iss-2609012037143368
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609012037143368
Assisted-by: Claude:claude-opus-5-5
…answer

discoverRepoRoot fell back, where git would not answer, to walking upward
and accepting any directory whose .git entry merely existed, with neither
the shape check nor the ownership gate the rules-root resolver grew. It now
delegates to gitutil.CheckoutRoot, the isolated rev-parse the front doors
already resolve through, and returns no root when git names none. Nothing
needed the walk: every front door resolves through LedgerRoot, which
refuses the same states.

Refs: iss-2609090947359464
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609090947359464
Assisted-by: Claude:claude-opus-5-5
Confirmed while fixing the remedy's delimiter: a promotion given no
grounds whose stamp fails after the mint dereferences the absent grounds.

Refs: iss-2609251208394294
Assisted-by: Claude:claude-opus-5-5
The post-mint stamp-failure report attached the link remedy
unconditionally and composed it with three defects:

- a promotion that lost the race to a concurrent promotion of the same
  issue was told to link its draft, which is refused as already promoted;
  the already-promoted refusal now wraps ErrAlreadyPromoted, and that
  failure names the winner and says to delete the duplicate draft;
- the remedy was delimited with single backticks while carrying free-prose
  grounds, so a backtick in the grounds closed it early; it is now a
  CommonMark code span one backtick longer than any run inside it;
- a promotion given no grounds (optional on the issue route) dereferenced
  the nil grounds composing the remedy and panicked; the remedy now names
  --grounds only when grounds were given.

One helper, orphanDraftError, serves both the issue and the reading route.

Refs: iss-258, iss-2609020154474224, iss-2609251208394294
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-258

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609020154474224

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609251208394294

Assisted-by: Claude:claude-opus-5-5
record-lint's issue_impact_valid and the release derivation read impact
as the raw scalar, so a quoted legal value ("fix") and a quoted empty one
("") are blocked there; the capture parser unquoted them first and the
reader accepted both. The parser now keeps a quoted impact's raw token
(rawScalarKeys), so validateStrict reaches the gate's verdict. A
differential test runs one table of spellings through both readings.

Refs: iss-2608261133218490
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2608261133218490

Assisted-by: Claude:claude-opus-5-5
…the board

A ledger record held only as an untracked file is in no state to anyone
but the checkout holding it: no branch cut from the default branch sees
it, and no gate reading the committed tree reads it. capture wrote such a
record and reported plain success, and the status board listed it as an
equal member of its folder.

One git status read over the ledger (uncommittedLedgerPaths) now drives
CaptureResult.uncommitted, Issue.uncommitted on status and list rows, and
StatusResult.uncommitted_count. The write prints an "uncommitted:" line;
the board marks the row and counts the records. Where git cannot answer,
nothing is marked.

Refs: iss-2609100508570527
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609100508570527

Assisted-by: Claude:claude-opus-5-5
The release cut's finding guard reads deferred_after and deferral_reason,
and no verb wrote them: a deferral was a hand edit of frontmatter that
bypassed every validator, done by script in a managed repository across
three cuts.

capture defer <iss-N> --after <vX.Y.Z> --reason <text> writes the pair and
appends a dated "## Deferral <date>" body section; the record stays in
open/. It refuses, with nothing written, everything the cut's reader would
not honour: a tag that is not the checkout's newest release tag, an empty
reason, a record that is not open, and a grade other than major or
critical. The grade is judged before the tag, so a refusal about the record
names the checkout's own. Re-deferring replaces the pair and appends a new
section.

Wired from the CLI and commands/capture.md; commands/launch.md's waiver
route names the verb; the capture and launch brief chapters, the principle,
the generated reference, the surface snapshot and the appendix move with
it.

Refs: iss-2609181223260994
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609181223260994

Assisted-by: Claude:claude-opus-5-5
Runs changelog.GuardFindings over the committed record capture defer wrote, and asserts the finding is waived with its reason rather than blocking.

Refs: iss-2609181223260994

Assisted-by: Claude:claude-opus-5-5
…ressed

The ledger is per worktree and said so nowhere: a record filed in one
worktree read "not found" from another with no hint which ledger had been
searched. Every capture verb now names it — one stderr line
"abcd capture: ledger of <checkout> on branch <branch>" in the plain
render, written before the verb runs so a refusal carries it too, and a
`ledger` member {checkout, branch} appended to the --json envelope
(renderLedger). The record dispatcher does the same for an issue id. The
checkout is home-relative where it can be; the branch comes from
symbolic-ref, so an unborn branch is named too.

The intent audit's half is captured separately as iss-2609251235119402:
its envelope belongs to the intent surface.

Refs: iss-2609202053570475, iss-2609251235119402
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609202053570475

Assisted-by: Claude:claude-opus-5-5
A capture without --found-at is legitimate and stays so, but it was
written silently, and it is the shape all nine misfiled installer records
of iss-2609120511058115 had: the path guard refuses a path-shaped
found_at absent from the checkout and has nothing to judge when there is
none. The verb now says, on stderr, that the record names no location in
this checkout, and --json carries no_location; the exit code and the
record written are unchanged, and a capture naming a location says
nothing of the kind.

Refs: iss-2609231156260287, iss-2609120511058115
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609231156260287

Assisted-by: Claude:claude-opus-5-5
findRecordFile, the existence probe behind resolve --intent/--spec and
promote --intent, treated every bucket read failure as an empty bucket.
An unreadable bucket was answered "not found in the store", and a
symlinked bucket was followed out of the store, so a resolve could stamp
provenance naming an intent that lives outside the checkout. An absent
bucket stays soft; a symlinked or non-directory bucket is refused as
path-unsafe, and any other read failure is returned as itself.

Refs: iss-260
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-260

Assisted-by: Claude:claude-opus-5-5
An unrecognised --severity, --category or --source was refused as
"malformed frontmatter: invalid source ..." — frontmatter the caller never
wrote, from a flag the message did not name. capture now judges the three
request members first, before anything is written, and returns a
FieldValueError naming the member, the value and the accepted set; the CLI
renders it as "--source "ci-signal" is not accepted; accepted values: ..."
with exit 2.

Refs: iss-2608290810037524
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2608290810037524

Assisted-by: Claude:claude-opus-5-5
restampField refused a restamp on a record with no origin but accepted one
beside any origin at all, so a record carrying an out-of-vocabulary origin
was restamped into a pair no command writes. It now asks
provenance.ParseOrigin and refuses naming the value. The converse — a
valid origin with no production_mode, completed into the pair by a
restamp — is kept and pinned by a test, and stated in spc-56 and the
command page as a deliberate write inside the lint's declared residual.

Refs: iss-2608300941548519
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2608300941548519

Assisted-by: Claude:claude-opus-5-5
Refs: iss-2609251823551349
Refs: iss-2609251823555125
Refs: iss-2609251823559111
Refs: iss-2609251823560369
Assisted-by: Claude:claude-opus-5-5
Brings v0.11.0, the verb consolidation, the help sentences, and the guard
and site fixes onto the capture lane. Two files conflicted, resolved hunk
by hunk: commands/capture.md takes main's sentence description and block
and keeps the lane's defer in the argument hint; cli.go keeps main's
relink-error line ahead of the lane's ledger-naming render for resolve
and wontfix.

Assisted-by: Claude:claude-opus-5-5
The help-sentence manifest reached this branch with the merge of main, and
its gate refused `abcd capture defer`, the one visible verb the lane added:
TestEveryVisibleVerbCarriesItsSentenceEverywhere and
TestTheFrameworkCommandsAreTheOnlyExemption both named it. The verb now
takes its one sentence from internal/core/surface/sentences.go in the house
shape and drops its cobra Short; the generated CLI reference and the
surface snapshot are regenerated from it.

Assisted-by: Claude:claude-opus-5-5
Merging main (85a10e1) into the capture lane, whose fixes grow
internal/core/capture and its tests, grew the tree-bounded reading objects.
Each window is re-measured by dry run over a clean clone of the merged tip
68deae0 against the rule the entries state, the smallest ten-thousand
boundary leaving at least one per cent headroom. Widening measures
1,040,187, past its 1,040,000 declaration, so it moves to 1,060,000 (1.90
per cent); entailment measures 344,124, 1.71 per cent under 350,000, so its
declaration stays and only its measured fields move; detection measures
1,049,223, 0.07 per cent under its 1,050,000 declaration, so it moves to
1,060,000 (1.03 per cent). Comparative is bounded by the widening run it
is handed, not by the tree, and is exempt by name.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 26, 2026 10:49
@REPPL
REPPL added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit a119979 Sep 26, 2026
13 checks passed
@REPPL
REPPL deleted the fix/capture-cluster branch September 26, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant