Skip to content

Bound the marker's range, not just its syntax — and let a check do the sweep - #336

Merged
fdaviddpt merged 2 commits into
mainfrom
fix/326-332
Aug 8, 2026
Merged

Bound the marker's range, not just its syntax — and let a check do the sweep#336
fdaviddpt merged 2 commits into
mainfrom
fix/326-332

Conversation

@fdaviddpt

Copy link
Copy Markdown
Contributor

Closes #326
Closes #332

#326 — a digits-only marker ahead of now stuck the throttle on, permanently and mutely

case "$LAST_MOD" in ''|*[!0-9]*) LAST_MOD=0 ;; esac rejects the syntax error and does not bound the value. A marker holding a future epoch — clock stepped back by NTP, a VM snapshot restore, a container clock jump, or an overflowing integer — makes ELAPSED negative, so [ "$ELAPSED" -lt "$SAVE_COOLDOWN" ] is true and exit 0 fires. That exit 0 sits above the line that rewrites the marker, so the self-heal the whole design rests on is unreachable on exactly the path that needs it.

Every subsequent run skips. Nothing on stderr, nothing in hook-errors.log. The user discovers it by noticing that nothing has been saved for a week.

Four sites, each now rejecting a negative ELAPSED, resetting the marker at the point of rejection rather than below the exit, and saying so:

Site What had stopped
save-session.sh:174 no session saved again
save-session.sh:627 NDC compression never runs; now.md grows unbounded — and here the existing rewrite is inside if RUN_NDC, i.e. inside the branch the bug skips, so the immediate reset is load-bearing rather than belt-and-braces
50-git-backup.sh:180 git backup stopped — #258's original outage, reached through a value the guard accepts
post-tool-hook.sh:355 never forks a save again, compounding the first row

The misleading comment at save-session.sh:155-157 — which asserted the marker self-heals and one corruption costs one skipped cooldown — is corrected. It was still there after v0.17.0 corrected only the CHANGELOG.

Proceed rather than abort. Epoch seconds do not move for DST or a timezone change, so the "laptop suspended over a boundary" case does not arise; only a real clock step produces this. Proceeding costs one extra save, refusing costs the preservation.

The diagnostic goes to hook-errors.log, because /remember:doctor reports "Recent errors" from that file and it is what maintainers ask reporters to paste. Not emitted from post-tool-hook.sh: one line per tool call for as long as the clock is behind is noise, and save-session.sh emits exactly one when it heals. That site takes a pure range test — no rewrite, no diagnostic, zero added spawns or reads.

#332 — the hand-fixing was the defect, so a check does the sweep

The issue says seven sites remain. There are twelve, and the five it does not name were found by the check rather than by reading — which is the argument for having one:

scripts/doctor.sh:346            $_mf_bytes
scripts/lib-lock.sh:412          $_s
scripts/lib-lock.sh:421          $1
scripts/lib-lock.sh:430          $1
scripts/run-consolidation.sh:218 $staging_consumed

tests/test_arith_base_lint_332.py fails on any value guarded by a digits-only case that reaches $(( )) without 10#. All twelve sites fixed — radix after the case, never instead of it.

No shared helper, and the issue is right about why. Not for the sourcing-cost reason: all four #326 sites already source log.sh, so report_error adds a function definition and nothing else — no spawn, no read, invisible to test_post_tool_hook_spawns.py and test_case_divergence_298.py, both green. The real reason is that a helper cannot stop an author who has never heard of it. The check can.

The suite was using #326 as an off switch

tests/test_ndc_day_boundary.py suppressed NDC at three sites by writing 2**40 into tmp/last-ndc.ts — a marker ~34,000 years ahead — relying on the negative ELAPSED to engage the cooldown. It went red the moment the guard landed. Switched to _suppress_ndc(), the brake #159 established as supported.

A targeted-only run would have shipped that red. It is the reason the full suite ran.

Tests

TDD, red first: 10 targeted failures before implementation, plus the lint red listing all twelve sites. The 6 initially-passing tests are deliberate controls — a fresh marker still throttles at each gate, and the overflow parameters really do go negative against unfixed code.

Green: 16 + 6 targeted. Full suite: 1545 passed, 43 skipped in 892.37s. Windows unverified; the only new test that runs there is the pure-Python lint.

Out of scope

…et a check do the sweep

#326 — the four cooldown guards validate SYNTAX and not RANGE. A digits-only
marker ahead of now is accepted, makes ELAPSED negative, and a negative ELAPSED
is -lt any cooldown, so the gate takes its `exit 0` — which sits ABOVE the line
that rewrites the marker. The self-heal the last three releases rest on is
unreachable on exactly the path that needs it: no save, no compression, no git
backup, permanently and mutely, until the wall clock catches up.

Three states, not two. Out of range now PROCEEDS, resets the marker at the point
of rejection where the reset is reachable, and says so once in hook-errors.log
via a new report_error() in log.sh. A silent clamp would trade a mute stuck
throttle for a mute wrong value.

post-tool-hook.sh is deliberately asymmetrical: it declines the cooldown it
cannot substantiate and does nothing else — no rewrite (the marker is
save-session.sh's), no diagnostic (one per tool call), no added spawn or read
(#299/#330).

The source comment at save-session.sh:155 asserted the opposite and is corrected
here. That comment, not the CHANGELOG, is why the issue was reopened: PR #328
closed it by referencing it.

#332 — twelve remaining sites read a case-guarded value into $(( )) with no 10#,
not the seven the issue lists. The five extra were found by
tests/test_arith_base_lint_332.py, which sweeps scripts/ and hooks.d/ for the
shape mechanically and is the part that prevents a sixth issue. ShellCheck does
not flag it (koalaman/shellcheck#2679). No shared helper: a helper a new author
does not know about cannot stop them writing the sixteenth site.

tests/test_ndc_day_boundary.py was suppressing NDC by writing a marker 34,000
years in the future — #326's defect used as an off switch. Switched to
_suppress_ndc, the brake the product documents (#159).

Targeted RED verified before the fix at all four sites; full suite 1545 passed,
43 skipped. Windows unverified — CI is the authority.

Co-Authored-By: Max <noreply>
…ld have cried wolf

Findings from the /code-review pass on PR #336, fixed in the same run.

FIFTH SITE (#326). hooks.d/before_session_start/50-git-restore.sh reads
`started=` through the same digits-only case. A value ahead of now makes _age
negative, so _fetch_health answers "in-flight" — the one state meaning "wait,
something is already running" — about nothing, and _spawn_fetch takes its early
return so the only writers of the record never run. Same geometry as the four
sites #326 enumerates, and not on its list.

The harm is worse than a wrong label: the caller then prints "already up to
date with origin/main" off refs no fetch refreshed, which is exactly what
_fetch_health's own header forbids. Now answers "abandoned" and spawns a real
fetch that rewrites the record. _fetch_health stays pure — its stdout is the
verdict, read through $( ) — so the diagnostic goes in _spawn_fetch.

LINT COULD NOT BE TRUSTED AS A GATE (#332). `case "$1" in` captures the guarded
name as the bare string "1", and the matcher did not require the $ sigil, so
every `$(( x + 1 ))` in a file guarding a positional parameter was a finding.
lib-lock.sh guards two, so the next ordinary arithmetic there would have failed
CI for no reason. Numeric names now require the sigil.

ITS OWN FIXTURE WAS NOT THE SHAPE IT CLAIMED. `''` inside a single-quoted Python
literal closes it and opens another, so the planted guard had silently become
`case "$LAST" in |*[!0-9]*)`. A self-test whose fixture is not what it plants
still goes green, and green here is supposed to mean "looked, found none".
Pinned by a test.

STALE LINE-NUMBER CITATIONS. Six comments this PR added cited line numbers;
three were already wrong at commit time. All replaced with construct names. A
PR that exists because a comment outlived the truth should not add more.

Targeted RED verified before each fix — including catching that the first draft
of the restore tests passed vacuously, because a doubled backslash put a literal
\\n in the state file and the digits-only guard rejected it for the wrong
reason. Full suite 1551 passed, 43 skipped. Windows unverified.

Co-Authored-By: Max <noreply>
@fdaviddpt
fdaviddpt merged commit b5973e9 into main Aug 8, 2026
12 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

1 participant