Follow the ink around a fused loop, and the counter stays open - #562
Conversation
R3 of the counter diagnosis, armed with the per-loop targets #556 froze. Where two pen strokes fuse around a small counter the ink is the union of two capsules of the KNOWN pen half width, and the medial axis of that fused lump is not the pen path — so the new tools/pairlab/zweizuege.py deconvolves the pen instead of thinning the blob: a point of the plate's counter is inked exactly when some pen sample lies within w_pen of it, so no pen sample may sit closer than w_pen to a counter the plate holds open, and the correction pushes the samples that do just far enough out along the counter's own distance field. Measured on the frozen sep05 roots with BLAS pinned, one knob against an otherwise identical base. The mechanism reaches its geometric target completely — 106 of 106 corrected loops open at the plate pen, 85 of them within the pre-registered 0.02 xh of the plate's expectation — and every distance reading improves (dev-19 dtw median -0.000701 at 14:3, p=0.0127; aiou 0.7583 -> 0.7601; the 63-word Soll distance 80 -> 78). It fails the structure gate on two words and the continuity gate on 1588 new kink events, and the cause is measured rather than guessed: the blend lengths are stated in x-heights while the trace is sampled in points, so at 0.0265 xh per sample they span 2.7 and 1.4 samples and the fade fades over three points. The pre-registered repair R3b (blend one rung up the sensor's own ladder) confirms the prediction in both directions and is also not adopted, which measures the formulation out: precision and smoothness hang on the same length here. The switch is off by default, no chain solve changes, no core/ byte moves, and no Laufform row is read or written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
There was a problem hiding this comment.
🟡 Changes recommended
Counter matching and closed-loop reporting are incorrect, and several recorded gate verdicts contradict the pre-registration.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a default-off two-stroke correction experiment for fused-loop counters and records the rejected R3/R3b measurements.
Changes:
- Loads frozen ink masks and applies loop-local path correction.
- Adds CLI controls and focused tests.
- Documents measurements, verdicts, glossary terms, and rescue paths.
File summaries
| File | Description |
|---|---|
tools/wordlab/cases.py |
Loads frozen reference masks. |
tools/pairlab/zweizuege.py |
Implements the correction and reporting. |
tools/pairlab/follow.py |
Integrates the optional correction. |
tools/docs_budget/__init__.py |
Raises the route reading budget. |
tests/test_zweizuege.py |
Tests geometry and refusals. |
docs/reference/werkzeuge.md |
Documents the tool. |
docs/reference/verfahren-kette.md |
Adds the route ledger entry. |
docs/reference/messjournal.md |
Records R3 and R3b results. |
docs/reference/kurzglossar.md |
Adds the short definition. |
docs/reference/glossar.md |
Adds detailed terminology. |
docs/proposals/tintenfolger.md |
Updates rescue paths and open arms. |
changelog.d/zwei-zuege-modell.md |
Adds the changelog fragment. |
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 8
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Four rounds landed on the same registers while this one was measuring, so the conflicts are all "both sides appended": the §14 register and its entries (LF15, J4 round 5 + adoption, J6), the Kette ledger (LF15's chart-seed row), the §7.9 rescue-path table, and the reading budgets. Every row of both sides is kept. The budgets are re-measured AFTER the merge rather than stacked, which is the precedent main itself set in the same file: - `mess-runde-route` needed 6947 on this branch against the pre-merge number; LF15's raise to 6890 already covers the merged path at 6636, so this branch's raise is dropped and the reasoning kept as a comment. - `mess-runde` came out 201 over after the merge and is brought back under by condensing this round's own rows — the two register rows and the two §7.11 rows — rather than by a raise. - `werkzeug-abschnitt` is raised once: the bench/lab inventory is where a new tool has to be named, and it stood at 4066 of 4073 on main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
|
Thanks — seven of the eight are fixed, and one I'm answering rather than changing.
|
…written Seven findings from the code review, and the two that matter both changed the result rather than the prose. The counter assignment was a flat 0.45 xh proximity search over every hole of the word — exactly what #553 measured going wrong, because it hands a loop the neighbour's hole and a word-global claim set then cascades the mistake. `catalogue_targets` now runs #556's Slot-Lineal verbatim: a hole belongs to the slot whose own BODY strokes span its x, and loops and holes are paired one-to-one inside that letter. The whole round is re-measured on the fixed rule (108 loops instead of 106; the "belongs to another loop" refusal drops from 9 to 1, which is what the old rule was mislabelling). And the round's own gates were read too generously. The pre-registration says the aperture has to land within 0.02 xh PER corrected loop, and that dtw must not be worse in median AND p90; 83 of 108 and a p90 of +0.000699 make both red. The entry, its register rows, the Kette ledger line and the §7.9 row now say four gates red — with every reading that improved still reported as a reading. Reading a gate more kindly after seeing the number is the one thing a pre-registration exists to prevent. The rest: the word-level early return that silenced the per-loop refusals when a plate shows no hole at all is gone (its two tests no longer need a decoy counter, and a new test pins the neighbour case); the fade length in the follower's help text said one nib where the constant is half a nib; the changelog described the fade as a property rather than as the attempt the round measured as too short; and the two new open arms are now in tintenfolger.md's Stand block. `ZweiZuegeOptions` keeps its name — `Laufform`, `Kringel` and `Eigenhand` are the repo's settled convention for a project Fachbegriff, and the code, the glossary and the ledger row have to be findable under one word. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
R3 of the counter diagnosis, armed with the per-loop targets #556 froze, and the
owner's decision of 2026-09-06 ("2 ja"): after the catalogue, the two-stroke
model is the next lever. His principle carries the whole arm — follow the ink
correctly and the counters stay open.
The mechanism, in three sentences
capsules of the known pen half width (0.0968 xh, the plate's own), and the
medial axis of that fused lump is not the pen path — the H0 finding of Measure where the counters get lost, from plate ink to stored row #551,
which every consumer of the skeleton inherits before any fit runs.
within
w_penof it, so the model reads that backwards as a hard statementabout the path: no pen sample may sit closer than
w_pento a counter theplate holds open.
distance field, fades the displacement to zero C¹ at both ends of the
corrected run, and reverts any push that pulls the loop's own
self-crossing open.
Scope is the catalogue's and nothing else: state
offen, size classkleinormittel(the classes where the instrument decides), and only where the plateshows a hole in that very occurrence. A loop the trace does not draw is a
topology loss and goes to R4 untouched; a lump the plate closed is measured
(
stroke_separation) and refused, because with no hole nothing says which sideof it each pass ran on.
Where it sits, and why that is the smallest honest point
After the last follower round, on the assembled pen path, behind
--zwei-zuege(default off). No chain solve changes, nocore/byte moves(so the golden parity fixture is byte-identical by construction), and it reads
the plate plus one frozen pen constant — never a Laufform row, so it cannot
close the harvest fixed point #553 warns about.
What the measurement says
Frozen roots
eaa195aa7c84…/0fbde2d72b64…, BLAS pinned, one knob againstan otherwise identical base that reproduces the campaign's
sep05numbersdigit for digit.
Verdict: not adopted. Gates (b) and (d) are red; the switch stays off. And
the mechanism reaches its target completely:
cross_missing12 → 13,cross_spurious7 → 9 — the whole amount onwillandGaloppieren, the other 17 dev words move no countercore/untouched, 2540 tests passPer-loop, the three keys the round was aimed at:
a#0 0.2046 → 0.2751,o#0 0.1741 → 0.2535,sz#0 0.2231 → 0.3063, every occurrence open.Gate (d)'s cause is measured, not guessed. A trace carries a sample every
0.0265 xh, so the declared anti-raster window (0.0363) spans 1.37 samples and
the C¹ fade (0.0725) spans 2.74 — both are stated in pen widths and are, in
samples, shorter than the discretisation they were meant to smooth, so the
plate's pixel raster lands in the pen path.
R3b, pre-registered in the same round and run on an offline path validated
to reproduce the follower arm exactly (largest coordinate difference
0.000e+00), moves that one length up to the sensor's own reading window. The
prediction holds in both directions: kinks 1588 → 465, p90 +16.74° →
+4.81°,
cross_spuriousback to 7 andwillclean — but gate (a) 85/106 →62/110 and the k0 Soll 78 → 82. Also not adopted, and together the two arms
measure the formulation out: precision and smoothness hang on the same length,
so the constraint belongs inside the follower solve rather than behind it.
That is now evidence rather than the assumption R4 wrote down.
Not in this PR, on purpose
follower path by the standing Route-A guard rail, and LF14's fixed-point
finding blocks the write anyway. The "24 closing words" is a property of the
rows, so this arm cannot move it; the path there is filed as an open arm.
tneeds, measured:loop_rangesscans self-crossings per penstroke, and the
tcloses its counter between its two strokes — chart row120 anchors,
stroke_starts [0, 83], loops per stroke[[], []], bothstrokes together one hole
D0 = 0.2475spanned by anchors 66..109, acrossthe stroke boundary. So the follow-up is not "add a range" but "let the finder
see cross-stroke crossings, and decide what a range spanning a pen lift means
for
align_loops". Filed in §7.11, not built here.Paperwork
§14 entry (pre-registration, measurement, R3b) with its register row, the Kette
ledger row, the §7.9 rescue-path row with four named conversions, two §7.11 open
arms, glossary + Kurzglossar entries for Zwei-Züge-Modell,
werkzeuge.md,a changelog fragment, and 16 new tests including the synthetic fused blob whose
target is arithmetic rather than a measurement.
mess-runde-routewas over budget by 138 tokens after the ledger row wascondensed once; raised deliberately with the reasoning in
tools/docs_budget/__init__.py.🤖 Generated with Claude Code
https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3