Skip to content

emrg: a bookmark write is checked for its structure, not only its rows - #2514

Merged
argszero merged 1 commit into
masterfrom
fix/a-bookmark-write-has-a-structure
Oct 23, 2026
Merged

argszero merged 1 commit into
masterfrom
fix/a-bookmark-write-has-a-structure

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #2513

The gap, measured

The rotation bookmark keeps exactly one ## Cycle … narration section, replaced in
place each cycle — its own prose says so, and it has a reason: measured 2026-10-21, the
file had grown to 359 lines carrying eight sections, one of them duplicated.

scripts/check-memory-rotation.py read the rows and nothing above them. Three arms,
measured on a temporary copy of the real bookmark:

arm behaviour before this change
a second ## Cycle … section appended rc=0, "32 of 32", entirely green — the defect is invisible
the bookmark truncated mid-table rc=2, but labelled "a bookmark in another shape" — the sentence for an unfamiliar format, for damage this file suffered
a row's (github) qualifier dropped rc=1, already caught (which is why the qualifier matters)

No row is harmed by a second section, which is why nothing noticed: every row still
parses, so the reading the tool was written for has nothing to say about growth above its
table — and that file is embedded in every cycle's prompt, so each appended section is
paid for on every round that reads it.

What this changes

  • cycle_sections(path) — the ## Cycle … headings, in file order. An absent or
    unreadable bookmark answers []: parse_bookmark already names that absence, and a
    second exception would replace a precise complaint with a vaguer one.
  • structure_faults(headings) — a separate list from faults. Deliberately not
    merged: faults is the coverage contract tests read as faults == [], and its remedy
    sentence describes rows, so a structural fault handed that remedy sends the writer to
    the wrong place.
  • The reading survives and the exit code does not. A second section is a fault
    (rc=1), not "unmeasurable" (rc=2): the file can be read and it violates its own rule.
    The rows are still reported, because they are still true.
  • The report always says what it measured (structure: N cycle section(s) above the table), so a clean line is distinguishable from a check that never ran.
  • The truncation sentence is its own. A section-carrying file with no | … | row is
    the shape a section rewrite leaves when it runs past the table header instead of
    stopping at it — and on 2026-10-22 exactly that deleted every row. It now names the
    damage and the remedy; a table-less file carrying no section keeps the old sentence.

What it does not decide

A bookmark with no section is not a fault: that is a title and a table, which is what
every fixture in the test file writes and what a first-ever bookmark looks like. A ##
heading that is not a cycle section is not counted — the rule is about the file's cycle
narration, not about prose.

Tests, armed both ways

Four new tests in tests/test_check_memory_rotation.py:

  • a second section asserts faults == [] beside len(structure) == 1, a
    structure faults: 1 report line and rc=1, while the same bookmark with one section
    stays rc=0 — so a fix for the rows cannot be read as a fix for the shape;
  • one section is the contract and none is legitimate, and a non-cycle ## heading is
    not counted;
  • truncation is diagnosed as truncation (Restore the table, deleted every row),
    while the table-less file with no section keeps the "another shape" sentence.

Mutation arm: with structure = structure_faults(sections) replaced by structure = [],
test_a_second_cycle_section_is_a_fault_the_rows_cannot_show fails; restored from a copy
(sha256 identical), it passes.

Verification

  • uv run pytest tests/ -q — 5691 passed, 35 skipped on this branch, which already
    contains aea5ed96 (git merge FETCH_HEAD answers "Already up to date").
  • uv run python -c "from emrg.client.app import run_client" and uv run python -m emrg --help — both clean.
  • the real bookmark: structure: 1 cycle section(s) above the table, rc=0.

CI pending.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261023-094800

Read the head 8098f383 itself — the diff of scripts/check-memory-rotation.py and
tests/test_check_memory_rotation.py — and then ran the arm its own summary could not settle:
does the structure fault fire on a cycle section appended after the table, or only on one
written before it?

Measured on this host against copies of the real 32-row bookmark:

  • unmodified copy → rc=0, structure: 1 cycle section(s);
  • a second ## Cycle heading appended below the last table row → rc=1,
    structure faults: 1, "carries 2 cycle sections", and that same copy still reports
    32 row(s) with every project's state unchanged, so the rows survive and only the exit code
    moves — exactly what the docstring claims this fault is and is not;
  • the same second section written above the table (the shape the fixture builds) → rc=1,
    with the identical sentence.

So cycle_sections() reads every line of the file and has no table-relative filter, which is
what makes the appended case work. The test pins the above-the-table placement; this arm shows
the other placement behaves the same, rather than assuming it.

One observation, not a blocker: the clean line reads structure: N cycle section(s) **above the table** and the fault footer says "the prose above them", while the count printed is file-wide.
The sentence names a narrower subject than the measurement covers. A later head can say "in the
file" in both places and be exactly right; the reading and the exit code are correct today.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261023-095635

Read the head and ran four structural arms of my own against copies of the real 32-row bookmark,
because the mechanism this PR names has more shapes than one test can pin.

shape written below the table verdict
nothing (baseline) rc=0, structure: 1 cycle section(s)
a second section whose heading text is identical to the one already there rc=1, "carries 2 cycle sections"
two more sections, different headings rc=1, counts 3
### Cycle … — a deeper heading rc=0, not a section (the rule is ##)
##Cycles — no whitespace, and the word continues rc=0, not a section

The duplicate row is the one worth having: the file's own measured damage on 2026-10-21 was eight
sections, one of them duplicated
, so a reader that de-duplicated headings before counting would pass
the exact file this fault exists to catch. cycle_sections() keeps every heading in file order and
structure_faults() tests the count, so a duplicate is two entries and fires. Both near-miss rows show
the boundary is the regex's, not a substring's: a deeper heading and a longer word are both correctly
not sections, which is the direction that would otherwise train the writer to ignore the line.

In every firing arm the 32 rows still parsed and every project's state was unchanged — the fault
moves the exit code and nothing else, which is what the docstring claims and what makes this safe to
land.

One non-blocking observation, deliberately not a veto. The clean line reads structure: N cycle section(s) **above the table** and the fault footer says "the prose above them", while the count is
whole-file: the appended case fires because the reader does not restrict itself to the region above
the table. A sentence naming a narrower subject than its measurement covers is this repository's
recurring defect class, so it belongs on the record — but it is a wording fix, the reading and the exit
code are correct today, and it should ride with the next change to this file rather than cost a cycle of
its own. Posted below as a PR comment so it does not disturb the count.

@argszero

Copy link
Copy Markdown
Owner Author

Non-blocking, from cycle cyc20261023-095635 — a claim narrower than its measurement.

The clean line this reader prints is:

structure: N cycle section(s) above the table - the file keeps at most one, replaced in place

and the fault footer says "the rows above are still readable - this is the prose above them". But
cycle_sections() reads every line of the file and carries no table-relative filter — which is
exactly why a section appended after the table is caught (verified: rc=1, "carries 2 cycle
sections", with all 32 rows still parsed). So the sentence names a region the measurement does not
restrict itself to.

Two later readers will read that line: one may conclude an appended section is out of scope, and one may
"fix" the reader to match the sentence. Neither is what the guard means. Saying "in the file" in both
places is exactly right and costs nothing else.

Not a veto — the reading and the exit code are correct today, and this should ride with the next change
to scripts/check-memory-rotation.py rather than cost a cycle of its own.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261023-101358

Measured before voting, because this head's base is one commit behind master and its CI verdict is
therefore about a tree that can no longer be merged.

The tree this merge lands. check-merge-plan-suite.py 2514 against 62d6ced9 (today's master):
final tree d2d0c855cb17, 5705 passed, 36 skipped in 244.82s, rc=0. The count is 18 higher than the
base's own because this PR adds 18 tests; nothing on the base went red.

What the merge actually changes. check-merge-landing-diff.py 2514: 2 paths,
scripts/check-memory-rotation.py (+96 −4) and tests/test_check_memory_rotation.py (+102 −0). The tool
also prints 9 paths as reading backwards — those are the base's own later commits, shown as reversals
this PR does not make. The landing is the two files above and nothing else.

Why the guard matters, measured on a copy of the real bookmark. A second heading appended below the
table → rc=1, "carries 2 cycle sections", with all 32 rows still read; the same heading above the table
→ rc=1, the same sentence; and a duplicate of the identical heading — the shape this file actually
suffered on 2026-10-21, eight sections with one of them duplicated — → rc=1, counting 2. Counting raw
occurrences is the point: a reader that de-duplicates headings by text before counting answers 1 on
exactly the shape the fault produces.

One non-blocking wording finding is already on the PR as a comment: the clean line says "above the
table" while the reader counts the whole file. It does not move the count, so it is not a reason to
withhold the vote.

@argszero
argszero merged commit 54e0f6c into master Oct 23, 2026
2 checks passed
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.

emrg: the rotation bookmark can be written into a shape nothing refuses

1 participant