|
A rule named "justeat orders" in the field labelled Name was written to
rules.json correctly and then dropped by every reader, because load()
required ^[a-z0-9][a-z0-9-]*$ and the save path validated nothing. The
rule stayed in the file, invisible in the dialog, never applied by the
post-new hook, and the next save from the dialog would have deleted it
outright.
The asymmetry was the defect, not the pattern. TagRules::validate() is
now the single predicate: the dialog refuses to save against it, and
load() uses it to repair rather than drop, so a rule that fails is
visible and fixable instead of silently discarded.
- The typed name is sanitised into an id when the field is committed,
so the field shows what will reach the file. uniqueId() suffixes a
collision, since sanitising is many-to-one and can manufacture the
duplicate that load() then drops.
- An already-legal id is never rewritten, including one like "a---b"
that sanitising would otherwise collapse. Rewriting valid ids would
churn a file mailctl also reads.
- A bad id loads repaired, with the warning kept: what is on disk is
not what the hook runs until the file is saved back.
Deliberately not mirrored into mailrules.py. The hook tags real mail
unattended every ten minutes, where silently renaming an id is worse
than dropping the rule; the file converges as soon as the dialog saves.
No format change, so no version bump and no two-repo commitment.
The load warning was not missing: it had been showing "1 rule could not
be read and was skipped" on every open, in the same font and colour as
the intro prose two lines above it, and read as more explanation. It is
now a red banner beside Save, with an icon and a dismiss button, and it
says the rules need attention rather than that they were skipped, which
is no longer true. Dismissal is per-appearance only; a persistent one
would re-hide the problem that went unnoticed for a session.
Both new dialog tests were confirmed to fail with the sanitiser
reverted, and the banner's styling, position and dismissal each fail
under mutation. 20 of 20 suites green, 34 tests in test_tagrules.
Closes item 83.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Item 73. The backlog kept every item's full Observed/Cause/Approach section
forever, including the sixty-eight that are closed, and had reached 5056 lines:
past the point where it could be read in one pass, and past the point where a
tool could open it at all.
The closed sections move to 2026-08-03-post-0.1.0-usability-closed.md, taking
the backlog to 570 lines. The status table stays where it was and remains the
index of all 80 items, so a closed item keeps its row, its date and its outcome
beside the open ones; only its evidence moved. Nothing was renumbered and
nothing was deleted, which the item required: the numbering is cited from commit
messages, from CLAUDE.md and from the specs, and both files share one sequence,
so item 42 is `## 42.` in whichever file holds it.
The split was done by script and verified by set difference rather than by
reading: every non-blank line of the original appears in one of the two files,
zero missing, and the only lines not in the original are the new file's header.
All 80 numbers resolve, every open item has its section in the backlog, every
closed one in the archive, with no duplicates and no orphans.
Two things the item's own approach did not anticipate. Three cross-references
said "see below" and their targets had just moved, so rows 60 and 75 and the
header's note on item 20's parked branch now say where the entry went. And the
cause was never the fifty done sections, it was that nothing moved a section on
the day its item closed; doing this once buys a few months and then item 73
returns. The rule in "Adding to this document" now requires the move on the
closing commit, and CLAUDE.md tells a future session that grepping the backlog
for a closed item's evidence will find the table row and nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|