Skip to content

fix(claude-memory): count heading-shaped lines a pseudo-frontmatter block would swallow - #1962

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/memory-stats-heading-grammar
Aug 5, 2026
Merged

fix(claude-memory): count heading-shaped lines a pseudo-frontmatter block would swallow#1962
kyle-sexton merged 2 commits into
mainfrom
fix/memory-stats-heading-grammar

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1933 (work-items row 127, campaign ledger #1941). #1933 shipped the M1 index-size measurement fixes for memory-dir-stats.sh; this closes the last under-count residual that surfaced during its gate review, after the PR had already merged.

No linked issue — the residual was found by #1933's own gate review rather than filed as an issue, and the campaign ledger it belongs to stays open past this PR.

The bug

memory-dir-stats.sh admitted # lines to its frontmatter grammar. A MEMORY.md that opens with a --- thematic break, carries heading lines, and reaches any later --- therefore had the whole span stripped as frontmatter:

---
# heading one
# heading two
---
body

Raw 5 lines / 41 bytes, reported 1 line / 5 bytes.

M1 is a [FAIL]-severity size gate, and a low count always passes it, so the shape disarmed the gate outright rather than merely mismeasuring it. This is the same failure species #1933 exists to eliminate — silent under-count on the gate — reached through the frontmatter grammar instead of the comment strip.

A # line is a comment to YAML but a heading to markdown, and headings are loaded content. The grammar now accepts only blank lines and key: mapping entries.

Evidence

Measured against origin/main's script and this branch's, same fixtures:

fixture 0.5.6 0.5.7
5-line heading pseudo-block 1 line / 5 bytes 5 / 41 (raw)
20 headings + later --- (23 lines) 1 line / 5 bytes 23 / 123 (raw)
genuine key:-only frontmatter 1 / 5 1 / 5 (still strips)
genuine frontmatter with a # note 1 / 5 6 / 44 (raw)

The bound is capped at fmcap (20) lines, so the worst case was ~22 content lines — 11% of the 200-line budget — never a full disarm of the line limb, but enough to make a five-line index report one.

The cost, stated plainly

A real YAML comment inside frontmatter now ends the block, and ending it strips nothing at all: the opening ---, every entry held so far, and the rest of the block through its close all count. That is an over-count, the direction M1's readings already guess toward — it can make the gate fire early, never fail to fire. It takes a hand-edited index to reach, since Claude Code only stamps a modified scalar into frontmatter a file already has. Comments join an existing class rather than opening a new one: frontmatter this grammar cannot parse already ended the block before this change, and a block sequence under tags: still does.

criteria.md M1 reading 1 records both halves.

Verification

  • Full suite 60/60, exit 0 — run in this worktree off current main.
  • Fresh-context verifier, rationale withheld, PASS on all six criteria (suite; heading fixture reports raw; key:-only frontmatter still strips; comment/fence behavior unregressed; no # alternation left in the executed awk; version + changelog hygiene). It derived every expectation from the awk before looking at output, and added its own mutation check — running origin/main's pre-fix script side by side to confirm the fix is load-bearing and the numbers above reproduce.

Versions

claude-memory 0.5.6 → 0.5.7 (0.5.5 → 0.5.6 landed via #1947); criteria.md 1.5.1 → 1.5.2; CHANGELOG entry under [0.5.7].

Related

kyle-sexton and others added 2 commits August 5, 2026 17:28
…lock would swallow

A markdown heading is a comment to YAML but loaded content to markdown, so admitting it
to the frontmatter grammar let a MEMORY.md opening with a thematic break, carrying up to
twenty heading lines, and reaching any later break strip the whole span: a 5-line index
reported one loaded line, disarming the FAIL-severity M1 size gate that a low count
always passes. The grammar now accepts only blank lines and key mapping entries, so that
shape counts every line. The cost is that a real YAML comment inside frontmatter ends the
block, and ending it strips nothing at all, so the delimiters and every entry held so far
count too. That is an over-count in the direction M1 already guesses toward, and a rare
one, since Claude Code writes only the modified scalar and never authors comments.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…belongs to

The entry read as if a hand-authored YAML comment were a new over-count shape. Any
frontmatter the grammar cannot parse already ended the block before this change, and a
block sequence under tags still does, so comments join that class rather than opening one.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 58s —— View job


Security review complete

  • Gather context (diff, changed files)
  • Review shell script changes for injection / logic issues
  • Review test and doc changes
  • Post findings

Scope

Reviewed all 5 files changed in this PR (git diff origin/main...HEAD):

  • plugins/claude-memory/skills/audit/scripts/memory-dir-stats.sh — the awk grammar fix
  • plugins/claude-memory/skills/audit/scripts/memory-dir-stats.test.sh — new test cases
  • plugins/claude-memory/skills/audit/reference/criteria.md — docs
  • plugins/claude-memory/CHANGELOG.md — docs
  • plugins/claude-memory/.claude-plugin/plugin.json — version bump only

No GitHub Actions workflow files are touched by this PR.

Findings

None. This is a narrow, local, read-only bugfix:

  • The change is confined to memory-dir-stats.sh's strip_unloaded() awk grammar (removing the #-comment alternation from the frontmatter-block match, permalink) plus the accompanying comment/doc/test updates. No new inputs, code paths, or trust boundaries are introduced.
  • No injection surface: the awk program's patterns are static string literals: matching is done with index()/substr() against $0, never with a dynamically-built regex or eval-like construct from file content. MEMORY.md content can't influence what the script executes, only what it counts.
  • No shell injection: index="$memory_dir/MEMORY.md" is a fixed, quoted path derived from the (unchanged, out-of-scope) sibling resolve-memory-dir.sh; no PR-changed line interpolates file content into a shell command.
  • No secrets/credentials/tokens touched; no permission or .claude-plugin/plugin.json capability changes (version bump only); no userConfig/path-traversal surface added.
  • The test file only adds printf-generated local fixtures under the test harness's own temp dir and asserts against the script's stdout — no new attack surface.
  • Worst-case behavioral impact of the bug being fixed (and of the new over-count edge case it documents) is a wrong integer feeding an internal [FAIL]-severity audit gate — not a security boundary.

No CRITICAL, IMPORTANT, or SUGGESTION items to report.

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 30s —— View job


Code review complete

  • Gather context (diff, changed files, CLAUDE.md)
  • Review memory-dir-stats.sh grammar change
  • Review test coverage
  • Review docs/changelog/version hygiene
  • Post findings

Scope note: this is the code-review lane; security concerns are left to the dedicated security-review lane per REVIEW.md's split. This PR doesn't touch a plugin manifest, marketplace schema, hook contract, or documented harness behavior — only a script's internal parsing logic — so the CLAUDE.md fresh-docs mandate doesn't gate this change.

Correctness

Traced the awk state machine in memory-dir-stats.sh by hand against all four new/existing fixtures (couldn't execute the suite myself — bash <script> invocations require an approval this session didn't grant — so this is a manual trace, not a run):

  • Heading pseudo-block (---/# heading one/# heading two/---/body): # heading one fails the new blank OR key: test → fm drops to 0, the held pending (---\n) flushes, and the line falls through to the catch-all print in the same awk cycle. Every subsequent line prints normally. Output equals the raw input byte-for-byte — matches the PR's claimed 5/41 raw reading.
  • Genuine frontmatter with a # note: type: index is held, # note breaks the block (flush + fall-through), modified: x and the closing --- are no longer inside fm==1 so they print via the catch-all too. Whole file counts raw — matches the claimed over-count behavior and its criteria.md M1 reading 1 documentation.
  • Pre-existing "unclosed frontmatter" fixtures (lines 111–114 of the test file, not touched by this diff) are invariant under the grammar change: since those blocks never reach a closing ---, they get flushed as content on the first non-matching line either way (old grammar: on the first non-#/non-key line further down; new grammar: on the # line itself) — same total line count either path. No regression risk there.
  • Confirmed via grep that the removed $0 ~ /^[[:space:]]*#/ alternative isn't duplicated anywhere else in the script — the "no # alternation left in the executed awk" claim holds.

The core fix (dropping # from the frontmatter-admission grammar) is correctly targeted and the reasoning in the script's header comment, criteria.md M1 reading 1, and the CHANGELOG entry are mutually consistent — all three describe the same two-sided tradeoff (heading pseudo-blocks now counted correctly; a genuine YAML comment inside real frontmatter now over-counts, which the doc explicitly accepts as the direction M1 already guesses toward).

Test coverage

The new cases in memory-dir-stats.test.sh cover the three load-bearing shapes: heading-only pseudo-block (lines/bytes both), comment-ends-real-frontmatter (lines only), and key-only-still-strips as a control. One minor asymmetry: the "comment in frontmatter ends the block and counts" case (line 181) only asserts --memory-lines, not --memory-bytes, unlike the heading case immediately above it which asserts both. Not a correctness gap — the byte and line paths share the same strip_unloaded pipeline, so a lines-only assertion isn't hiding a real risk — just a small stylistic inconsistency with the adjacent case.

Hygiene

  • Version bumps consistent across plugin.json (0.5.6 → 0.5.7), criteria.md (1.5.1 → 1.5.2, Last updated bumped to 2026-08-05), and the CHANGELOG entry.
  • marketplace.json doesn't pin per-plugin versions, so no update needed there.
  • CHANGELOG and criteria.md M1 reading 1 both explicitly document the over-count tradeoff rather than glossing over it — good for a [FAIL]-severity gate where future maintainers need to know which direction the readings bias.

No blocking findings. This is a narrowly-scoped, well-justified fix with the reasoning traceable across code comment, test, doc, and changelog.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count.

@kyle-sexton
kyle-sexton merged commit 01e1dca into main Aug 5, 2026
33 of 34 checks passed
@kyle-sexton
kyle-sexton deleted the fix/memory-stats-heading-grammar branch August 5, 2026 22:31
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