Development record
Reported from a downstream repository using abcd, where `abcd capture` (bare,
Reported from a downstream repository using abcd, where abcd capture (bare,
the read-only status render) printed three "skipped … malformed frontmatter"
lines for records abcd itself had written. The status board's counts then
silently exclude records that exist, and nothing tells the reader whether the
writer or the validator is the side that is wrong.
Two distinct causes, and the first is the serious one.
1. The slug generator produces a slug its own validator rejects
Reported record: iss-190, refused with
slug "guard-notebook-coverage-reads-notebooks-yml-as-one-flat-" is not kebab-case
The slug was truncated to its length budget mid-word, leaving a trailing hyphen.
SlugRe (internal/core/issueschema/issueschema.go:150) is
^[a-z0-9]+(-[a-z0-9]+)*$, which refuses a trailing hyphen — correctly. The
defect is that the generator can emit one: the truncation does not trim the
separator it cuts against, so writer and validator disagree about what a legal
slug is.
This is the sharpest shape of a record defect: the tool wrote a record its own
gate refuses to read. It is the same class as resolveShipImpact's stated
purpose — that abcd must not "produce, out of its own verbs alone, a record its
own record-lint refuses" (iss-126) — arriving on a different path.
The failure is also silent in the direction that matters. The record is skipped rather than reported as a fault, so a ledger quietly under-counts and the only signal is a line the reader may not connect to the count.
2. source: autonomous-hunt is not in the accepted set
Reported records: iss-299, iss-300, iss-301, refused with invalid source "autonomous-hunt". They were written by an autonomous bug-hunt loop, so a value
one abcd workflow writes is not one the schema accepts.
Sources (issueschema.go:140-144) is plan-review, impl-review,
manual-test, review-followup, agent-finding, agent-observation,
user-observation, drift-detection, memory-curation.
Corroborated independently in this repository on the same day. Capturing
findings during the v0.8.0 release, --source release-followup and
--category reliability were both refused, each on the first attempt, each
plausible-sounding and neither in the list. The vocabulary is closed, which is
right, but it is discoverable only by being refused: capture --help renders
--source string surfacing channel (default user-observation) and names no
member, and the same is true of --category.
So the refusal is correct and the surface around it is not. Two sessions independently guessed values that do not exist, which is evidence about the help text rather than about the guessers.
The peer session supplied the controlled comparison that turns this from a
complaint into a diagnosis. In the same --help output, --severity names its
four members inline and --source and --category name none. Their report:
severity was right first time because it was named; the other two were right
only by accident. Two sessions, three flags, and the one flag that documents its
vocabulary is the one nobody got wrong.
That isolates the cause. It is not that the vocabularies are closed, and not that
the values are unguessable — it is that two of the three do not say what they
accept, while the third, in the same help text, does. Whatever is decided about
autonomous-hunt, naming the members would have prevented both of the day's
wrong guesses independently of it.
What is owed
- Trim the separator when truncating a slug, so the generator cannot emit
what
SlugRerefuses. Then sweep: any other writer that truncates into a bounded filename has the same exposure. - Decide
autonomous-hunt. Either the bug-hunt loop uses an existing member (agent-findingis the obvious fit) or the value joinsSources. This is a vocabulary decision, not a bug fix, and it belongs to the maintainer. - Name the members in
--helpfor--sourceand--category, the way--severityalready does ("severity: nitpick | minor | major | critical"). Two independent wrong guesses in one day is the measurement. - Say which layer is wrong when a record is skipped. A record that fails validation should report that it does, distinguishably from a record that is absent, rather than being dropped from a count with a line above it.
Acceptance
- Given any text a caller captures, when the slug is derived and
truncated, then the result satisfies
SlugRe— no trailing or doubled separator — and a property test asserts this over adversarial inputs. - Given a record whose frontmatter the schema refuses, when the status board renders, then the count and the diagnostic agree about what was excluded and why.
- Given
capture --help, when a caller reads it, then the legal values for--sourceand--categoryare named inline, as--severityalready names its four.
Grounds
- pursued: a reader shown a skipped record can now tell whether the writer or the schema is wrong; a skipped record whose layer is empty, or a board whose header omits a skip it lists, would show it wrong
Frontmatter
| id | iss-2609120452071388 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-2609120452071388-the-slug-generator-can-emit-a-trailing.md
open on GitHub ↗ · commit history ↗
mentions8
- iss-126intent Reconcile ships planned intents into shipped/ without stamping impact, so a no-impact seed passes plan and reconcile then trips the intent_impact_valid blocker on a record produced entirely by the tool's own verbs; a plan-or-ship verb must be able to stamp impact (sibling of the iss-117 class, found by its review)
- iss-190scanner adjacency recovery is capped by maxAdjacencyProbeWindow, so a recovered token longer than the window is truncated — and the truncation can break the recovery CHAIN, hiding a third abutting token entirely. probeAt (scanAllPatterns, internal/adapter/scanner/scanner.go) runs its anchored probe against line[at:at+maxAdjacencyProbeWindow] (512 bytes), so a recovered open-ended match longer than that is recorded with an artificial end at the window edge. Two manifestations. (a) Known and accepted since iss-185: the recovered token's far tail past the window stays raw in Redact's output; the token's prefix is still masked, so this is a partial-redaction trade, not a total miss. (b) NEW, found by two independent adversarial reviews of iss-188's fix: because the artificial end does not align with the token's real end, the next iteration's probeAt(m.end) and stolenJunctions both start from a byte offset that is inside the token rather than at its junction, so the chain that would otherwise unwind a run of three or more abutting tokens breaks — the third token becomes entirely invisible, not merely truncated. Repro: line := "ghp_" + strings.Repeat("a",36) + "ghp_" + strings.Repeat("m",600) + "ghp_" + strings.Repeat("z",36) yields exactly 2 token:github_pat findings where an unbounded search would find 3; the third token's 36 raw bytes survive Redact and the fail-closed residual re-scan reports the output clean. A sharper variant of (a) affects jwt_shaped specifically: its regex requires two literal '.' separators, so a JWT-shaped token whose header and payload together exceed the window has NEITHER separator inside the probe's slice and the anchored probe finds no match at all — the token is missed entirely rather than truncated, e.g. a fixed-length match immediately followed by a JWT whose first two segments total more than ~502 bytes. This is NOT a defect introduced by iss-188's backward search; it is the same pre-existing maxAdjacencyProbeWindow trade-off from iss-185, now reachable through a second code path because the backward search feeds more candidates into probeAt. Fixing it properly needs a different mechanism (a growing or adaptive window, or re-probing chained from a recovered match's real end) that must not reintroduce the unbounded per-match cost the window exists to prevent — the exact risk iss-188's own ledger entry warned against. Deliberately not folded into iss-188's fix pass to keep that diff scoped to the confirmed junction-search defect.
- iss-299brief/README.md's 05-internals directory-tree comment enumerates ten chapters including a nonexistent 'in-session dispatch' (that is planned intent itd-2, not a chapter); the directory holds nine files and its own index has nine rows
- iss-300The persona glossary (brief/glossary/core/persona.md) and brief/01-product/05-personas.md say the persona picker 'chooses at random', contradicting personas.json and itd-79 whose rule is 'selection is by role, never by name'; the glossary's worked examples also assign off-role names (Carol as product lead, Alice as developer)
- iss-301external-review.yml computes its approver tally with 'gh api .../reviews --paginate --jq' where --jq runs per API page (not across the full set; --slurp is incompatible with --jq), and no per_page is set so pages are 30 — so the 'latest state per reviewer wins' reduction the comment promises fails once reviews span a page: a stale page-1 APPROVED survives a later page-2 CHANGES_REQUESTED, and a straddling double-approval is counted twice. Fail-open on a required security gate. Not fixed autonomously: required-check workflow that cannot be validated without running Actions
- iss-2609120505141653`origin: researcher-authored` states that a person invoked the verb. It is
- iss-2609120511058115Nine findings about abcd's own installer are sitting in a teaching-materials
- iss-2609251151293535The source vocabulary has no member for a finding an autonomous bug-hunt loop files, and a downstream loop wrote source autonomous-hunt, which the reader refuses and skips. Decomposed out of iss-2609120452071388 when its code halves were fixed, because this half is a closed-vocabulary ruling for the product thinker, not an implementer's fix: either such a loop uses an existing member (agent-finding is the nearest) or autonomous-hunt joins issueschema.Sources, with record-lint and the help text following from the one list. Until it is ruled, a record carrying it is skipped with the schema layer named on the board.