v3.5.9 - the defects that were filed and never read #236

Merged
john merged 35 commits from cadence/v3.5.9 into main 2026-08-23 02:38:29 +00:00
Owner

Ten defects sat in the capture archive unread from 2026-08-05 until the 2026-08-22 triage reproduced every one against the current tree. Filed as #231 and #232, and the theme is one sentence: the release seam and the frontmatter reader both return a clean answer over a case they did not actually handle.

Two phases, five requirement ids, all five traced to a verified phase. /cad-audit PASS on both arms, fourteen of fourteen acceptance criteria covered.

Phase 1 (REL-01, REL-02, REL-03) is the release/changelog seam, the seam v3.5.8 rewrote. Fence-aware heading scans so a '## ' inside a code block stops ending a section, a heading-only release section reporting empty, the trailing link-reference block bounded by what its keys name, a version-less manifest halting the close instead of passing as a benign skip, changelog.state on every emitting envelope, and an unparseable --version refusing by naming the raw argument.

Phase 2 (FRM-01, FRM-02) is the frontmatter reader under plan-overlap. The four value-level grammar codes are scoped to the two list keys the seams read, so a backtick in goal: stops bailing the risk floor on a plan's whole declared file list, while the five structural codes still cross keys on purpose. And a markdown-decorated files: path now reports markdown-decorated-path instead of parsing clean, so two plans that collide on one file route sequential rather than into separate worktrees.

The goal-backward verify pass on phase 2 found the decoration rule covered only the three shapes AC3 named, so an italic path still cleared. Widened to a matched-wrap table and retested; all 597 frontmatter files: entries under .planning were measured first and none opens or closes on a wrap byte.

Closes #231
Closes #232

Ten defects sat in the capture archive unread from 2026-08-05 until the 2026-08-22 triage reproduced every one against the current tree. Filed as #231 and #232, and the theme is one sentence: the release seam and the frontmatter reader both return a clean answer over a case they did not actually handle. Two phases, five requirement ids, all five traced to a verified phase. /cad-audit PASS on both arms, fourteen of fourteen acceptance criteria covered. Phase 1 (REL-01, REL-02, REL-03) is the release/changelog seam, the seam v3.5.8 rewrote. Fence-aware heading scans so a '## ' inside a code block stops ending a section, a heading-only release section reporting empty, the trailing link-reference block bounded by what its keys name, a version-less manifest halting the close instead of passing as a benign skip, changelog.state on every emitting envelope, and an unparseable --version refusing by naming the raw argument. Phase 2 (FRM-01, FRM-02) is the frontmatter reader under plan-overlap. The four value-level grammar codes are scoped to the two list keys the seams read, so a backtick in goal: stops bailing the risk floor on a plan's whole declared file list, while the five structural codes still cross keys on purpose. And a markdown-decorated files: path now reports markdown-decorated-path instead of parsing clean, so two plans that collide on one file route sequential rather than into separate worktrees. The goal-backward verify pass on phase 2 found the decoration rule covered only the three shapes AC3 named, so an italic path still cleared. Widened to a matched-wrap table and retested; all 597 frontmatter files: entries under .planning were measured first and none opens or closes on a wrap byte. Closes #231 Closes #232
john added 35 commits 2026-08-23 02:38:21 +00:00
The tracker holds #231 and #232: ten defects reproduced against the current
tree, filed 2026-08-05 to 2026-08-08, archived unread in CAPTURE.md, and
re-verified on 2026-08-22 by the archive triage. Six are in the release/changelog
seam v3.5.8 rewrote; four are the frontmatter reader under plan-overlap.

Ids seeded up front - REL-01, REL-02, REL-03, FRM-01, FRM-02 - so each is either
traced to a phase or visibly unpicked in /cad-audit.

Corrects the v3.5.8 record: the merge and the v3.5.8 tag both landed, so the
Active section no longer calls them outstanding. #233 came out of the same triage
and is left unscoped.
Phase 1 takes the six release/changelog defects (#231), phase 2 the two
frontmatter defects under plan-overlap (#232). Split on subsystem rather than
severity: different files, different callers, no shared test surface.

Phase 2 carries the note that its own repair is the check that decides whether
parallel dispatch is safe, so it must be planned as if plan-overlap cannot be
trusted until this phase verifies it.
A PRIMARY .claude-plugin/plugin.json whose verdict is skip/no-version-field
returned {"ok":true,"action":"skip"} at exit 0, so milestone.md read the
close as successful and the release shipped with a manifest nobody bumped.
The seam re-classifies that one verdict as a refusal - ok:false,
action:"refuse", reason:"no-version-field", exit 1, nothing written - with
its own detail sentence rather than the verdict's benign "leave it
untouched".

The pure core is unchanged and SIBLING manifests keep skipping: this
project's own .claude-plugin/marketplace.json carries no version by design,
and a verdict-level refusal would halt its close every cycle. A new test
copies both real manifests into a fixture and pins that.
The three outcomes differed only by key PRESENCE: section_empty was absent
when CHANGELOG.md was absent and false/true when it had been read, and both
absent and false read falsy. milestone.md's halt could not tell a project
with no changelog from a clean one, so a close over a project keeping no
CHANGELOG.md reported as if the notes were fine.

Every changelog object the seam emits now carries changelog.state, in
readChangelog's own vocabulary - absent | unreadable | ok - plus
not-examined for the refusals that return before the changelog gate is
entered. section_empty, changed and promoted are untouched: section_empty is
still what the empty-section halt reads. The two envelopes that deliberately
carry no manifest/siblings/changelog key at all (no-plugin-manifest,
bad-date) are untouched too.
`bump --version v` returned {"ok":false,"reason":"no-target-version",
"target":""}: the seam stripped the leading v, was left with an empty
string, discarded the raw argument and then reported that no version was
given. That is false - a version WAS given - and it names nothing the
operator can repair.

The raw trimmed value is kept when normalization is left with nothing
usable, so decideManifestBump reaches its own unparseable-version, whose
sentence already quotes the offending value. No verdict code is minted at
the seam. normalizeTargetVersion is unchanged: planning.mjs reads it for
v-stripping inside the audit's version_drift signal. An ABSENT --version
still refuses as no-target-version and a blank one still refuses at its
declared arg-contract row as missing-flag-value; a test pins all five
spellings.
release-bump.mjs's header named four of the seven codes decideManifestBump
can return: unparseable-version, downgrade and not-an-upgrade were missing,
and the sentence claims the verdict codes are emitted verbatim as reason, so
a caller reading that list could not branch on three of them. The header now
names all seven and states the one code this seam re-classifies.

The guard derives its set from EXECUTABLE source - the code: literals inside
decideManifestBump - and asserts each token appears in both the seam's
leading header comment block and that function's JSDoc. Comparing the two
prose lists to each other would pass while both were stale together, which
is how this survived two cycles. The extraction is asserted non-vacuous
against the seven codes shipping today, and each failure names the missing
token and the document that lacks it.
A `## ` line inside a fenced code block was read as a real heading by
every `^## ` scan in the module - sectionEnd's next-heading walk,
prependChangelogEntry's anchor scans, and promoteUnreleased's own
Unreleased/release-heading locators. prependChangelogEntry is the one
that actually corrupted a document: it wrote the dated heading INSIDE
the fence, and the promotion that followed stranded most of the body.

Add a module-private fence-state scanner (mirrors lib/planning-files.mjs's
shape, not imported since this file carries no node:fs) fed every line in
order, and gate every heading scan in the module on it.
releaseSectionEmpty treated any non-blank line as content, so a
`## [Unreleased]` body scaffolded down to `### Added` / `### Fixed`
with no bullets read as non-empty and the close's empty-section halt
never fired on it. Blank lines and ###/#### subheadings are now the
only shapes that count as empty; any other non-blank line, including
a bare prose paragraph, is still content - matching how every released
section in this repo's own CHANGELOG.md opens with prose before any
bullet.
sectionEnd stopped its backward scan at the first blank line, which
the committed "trailing link references stay put" test already pins
against. Rebuild the rule on D-05 instead: walking up from the last
non-blank line, the trailing reference block is the run of `[key]: url`
definitions whose keys name an existing `## [key]` heading; the first
definition whose key names no such heading, and everything above it,
is body content that promotes with its section.

Accepted cost, stated in the comment where the rule lives: a file-final
definition naming no heading pulls the whole run at EOF into the body,
even a ref line above it whose own key does name one - the rule's
stated edge, not a regression to repair with a second exclusion.
The JSDoc sentence claiming "the seam owns its own disjoint set" named
4 of release-bump.mjs's 9 header codes, missing
unreadable-sibling-manifest, unreadable-changelog, partial-bump,
bad-date and missing-flag-value. Add the five so the claim is true
rather than asserted - the same drift class D-11 names on the other
side (release-bump.mjs's own header naming 4 of 7 core verdict codes,
phase 2's job). The seven verdict codes immediately below stay
byte-identical: plan 2 derives that set from this module's executable
code literals.
Step 2's refusal halt enumerates reason codes by name, so the seam-level
no-version-field refusal would have been reported without stopping the
close. It is in the list now. The changelog halt named only
changelog.section_empty: true, which an absent CHANGELOG.md could never
set - a project with no changelog closed as if the notes were fine - so the
bullet names changelog.state: "absent" beside it.

milestone.md sat at exactly its byte ceiling, so weight-budgets.json is
re-pinned to the new count in this same commit or self-verify's budget check
fails on this phase's own work. DOCS-CLAIMS.md's MILESTONE-06 and
MILESTONE-08 rows are updated by hand for the new prose, and MILESTONE-07's
range is corrected for the line this edit moved it by.
parsePlanFiles' frontmatter arm now reports markdown-decorated-path on a
declaration wearing bold (**src/a.rs**), the link form ([a](a)), or a matched
interior backtick pair - two or more backticks at indices strictly inside the
value. The declaration is REPORTED, not repaired: its bytes go into the files
list unchanged (D-04/D-19), so overlaps keeps meaning "these two declarations
intersect" and never "intersect after repair", and the diagnostic is what moves
the gate because execute.md routes any non-empty frontmatter_issues sequential.

The interior COUNT is the whole of the backtick arm (D-05), which is what keeps
it additive to resolveValue's unchanged boundary rule rather than a second
opinion on the same bytes: a wrap has zero interior backticks and a
wrap-plus-punctuation exactly one, so neither double-reports, and a real path
carrying one interior backtick stays diagnostic-free - the UAT-21 over-fire
guard, untouched.

Frontmatter arm only (D-06): the - **Files:** task arm already strips backticks
and contributes both its normalized and its raw form, so a decorated task path
already matches a sibling's plain one.

The code's row lands in references/plan-frontmatter.md in this same commit, with
that file's weight-budgets.json pin moved 17312 -> 18700 (D-11).
Two plans in one phase declaring the same file, PLAN-1 in bold and PLAN-2
plain. overlaps stays empty - the two spellings genuinely do not intersect and
D-04 keeps overlaps meaning "these two declarations intersect", never
"intersect after repair" - and frontmatter_issues names PLAN-1, line 6,
markdown-decorated-path, with the declaring line's text.

That is the collision this phase exists for: workflows/execute.md routes any
non-empty frontmatter_issues to the sequential path, so the pair is no longer
cleared into two worktrees. Reverting the parsePlanFiles change reddens this
test alone.
readFrontmatterList returned the whole pass's issues whatever key was asked
for, so a backtick or a stray quote in the bytes of goal: arrived at
plan-overlap's frontmatter_issues and at phase-plans' declared-files bail as
though the plan's FILE LIST were unreadable. The four value-level codes -
unterminated-quote, trailing-value-content, residual-quote,
backtick-wrapped-value - are now raised only where the key that owns the value
is requirements: or files: (D-01, D-07's enumeration of the family).

The gate is scopeToListKeys, applied at the four push sites inside
parseFrontmatter where the owning key is already in scope. No field is added to
Issue and readFrontmatterList filters nothing, so all five readers of that
envelope keep the shape they have. Deliberately not "only the key the caller
asked for": a requirements: defect still reaches a files: read, because both
lists belong to one declaration.

The five structural codes and the two bracket-level ones are untouched and keep
reaching every read (D-02). One silence the gate would otherwise have opened is
closed with it: the item arm returns early on a scanValue failure, before the
no-block-key diagnosis, so with no key open the line reported nothing at all -
it now raises item-without-key there, which is what D-02 asserts about it.

The three JSDoc claims this makes false are corrected in the same commit, the
split is stated in references/plan-frontmatter.md's Diagnostic codes preamble,
and that file's weight-budgets.json pin moves 18700 -> 20460 (D-11).
Nothing tied lib/planning-files.mjs's frontmatter grammar to
references/plan-frontmatter.md's Diagnostic codes table, so a code could ship
with no documented meaning and no stated remedy. The set is now derived from
EXECUTABLE source - every kebab-case quoted literal between scanValue's
declaration and the end of parsePlanFiles, comments stripped first - and every
member must have a row.

Bounded by symbol, never by line number: the same module defines five codes for
other grammars with their own references, and an unbounded scan would demand
rows for them here. Non-vacuity is asserted before the loop against the twelve
codes shipping before this phase plus markdown-decorated-path.

A whole-literal net rather than one regex per push form, because the forms are
not a closed set - a code: property, a codes: array literal, a codes.push
argument, a ternary's array branch and a new Set([...]) - and the three-form
draft of this test silently missed trailing-inline-content on exactly that. The
wider net's cost is a false positive on a non-code literal in that region,
whose remedy is a row or a rename: the safe direction, unlike a miss.
`milestone-prune.test.mjs`'s live-REQUIREMENTS corpus asserted that every
moved bullet wraps, so the suite went red the moment a requirement was
written on one line - a state of the repository, not a result, and the same
mistake the empty-roadmap case above was already fixed for. The wrapped
assert now tallies instead, and says out loud when nothing wraps.

Relaxing it surfaced what it had been masking: the row lookup matched on
`startsWith('| REL-02 ')` against the whole Shipped table, and ids are reused
across milestones, so it compared this run's archive against a `REL-02` that
shipped in v1.1.0-rc.2. Scoped to the rows the run added, which is the
scoping the pipe-count check twelve lines below already uses.

The `## Active` bullets are rewrapped to 80 columns so the corpus still
exercises the multi-line span, and to match the prose around them.

2673 tests, 0 failures.
CAPTURE.md is gitignored in this repo, so the three open items are in the
live queue only.
`isDecoratedPath` tested exactly bold, the link form and a matched interior
backtick pair, so `*src/shared.rs*` was returned as a clean file entry and
`plan-overlap` cleared it into a separate worktree against a plain sibling
declaring the same file. That is the phase's own failure mode surviving in a
spelling the acceptance criteria did not enumerate, caught by the goal-backward
verify pass rather than by any of the seven criteria, all of which held.

The wrap set is now a table of boundary pairs: `*`, `_`, `<>` and a bare `[]`,
with the link form still tested separately because it opens `[` and closes `)`.
Single-byte entries carry bold by repetition, so a wrap is recognised by the
bytes at its two ends and never by counting how many the author repeated.
Backticks stay out of the set on purpose, since the boundary pair already
belongs to `backtick-wrapped-value` and listing it here would double-report.

A wrap is matched or it is nothing, because `_`, `[` and `*` are all legal path
bytes: `_private/a.rs`, `src/a_`, `src/__init__.py` and `[src/a.rs` stay
diagnostic-free, pinned as rows beside UAT-21's backtick guard. The residual
over-fire is a path that legitimately opens and closes on the same emphasis
byte, `__main__`, which reports and is still returned byte-exact. That direction
is the safe one for a parallel-safety gate: a phantom diagnostic costs a
sequential dispatch, where a missed shape costs two plans writing one file. All
597 frontmatter `files:` entries under `.planning` were measured first and none
opens or closes on any wrap byte.

Report, never repair, is unchanged (D-04): every flagged declaration comes back
in `files` with its bytes untouched.
Both fires were blocking and both passed, but the records were never staged, so
the v3.5.9 prune would have deleted the only copy of what gpt-5.6-terra raised
on either PLAN and how each finding was ruled.
docs: open v3.6.0, seed its five ids, record the v3.5.9 close
All checks were successful
test / node-test (git, 22) (pull_request) Successful in 54s
test / node-test (git, 24) (pull_request) Successful in 30s
test / node-test (other, 22) (pull_request) Successful in 1m13s
test / node-test (other, 24) (pull_request) Successful in 1m7s
test / node-test (planning, 22) (pull_request) Successful in 1m33s
test / node-test (planning, 24) (pull_request) Successful in 1m40s
test / node-test (prose, 22) (pull_request) Successful in 34s
test / node-test (prose, 24) (pull_request) Successful in 35s
test / node-test (review, 22) (pull_request) Successful in 18s
test / node-test (review, 24) (pull_request) Successful in 19s
test / node-test (routing, 22) (pull_request) Successful in 38s
test / node-test (routing, 24) (pull_request) Successful in 39s
test / self-verify (pull_request) Successful in 14s
test / typecheck (pull_request) Successful in 16s
d7005173db
john merged commit b86fc25cca into main 2026-08-23 02:38:29 +00:00
Sign in to join this conversation.
No description provided.