Record the sweep that leaves no word specimen incomplete - #557
Merged
Conversation
The words.json sidecar defines `incomplete` for a specimen whose OWN ink the rect clips, and not one of the 202 entries carries it. This checks whether that is right, and writes the answer down where the flag is defined. A boundary sweep over every rect — connected components on the whole plate, so a stroke leaving a box is one object seen from both sides, rather than a per-crop threshold that fringes on the abb22 scan — finds no specimen missing a letterform or a diacritic. Where ink still crosses a rect it is the comma after the word (Gewehr, Zügel, streiten, a22-dank), the neighbouring word or the descender of the line above (laden, a22-laden, a22-fern, a22-ein), or the word's own lead-in hairline, which the renderer generates anyway (a22-laden, a22-ihren, a22-doch, a22-roten-2). A blind second pass over the rendered crops agreed on all sixteen. That is the expected result rather than a surprise: the seven specimens that really were clipped — regieren, zum, einer, das, und, Wer, zwei — were REPAIRED on sep01 and the word bench re-baselined (qualitaetsmetrik.md §15), because the flag is the second remedy, not the first. The sidecar note now carries both halves, so the next edge scan that lists the same rects does not read as work waiting to be done. Data only: the note prose, no entry touched. No rect, exclude or lineature moved, so the frozen wordbench fixtures are byte-identical. 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
Clarify the incomplete rule for clipped lead-ins and correct the provenance claim about seven clipped crops.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Documents the audit confirming all 202 word specimens remain traceable.
Changes:
- Records boundary-sweep results and the repair-before-flag policy.
- Updates integrity metadata and audit provenance.
File summaries
| File | Description |
|---|---|
data/sources/suetterlin-1922/words.json |
Adds completeness-audit findings and policy notes. |
data/sources/suetterlin-1922/SOURCE.md |
Updates checksum, size, and audit provenance. |
Review details
Suppressed comments (1)
data/sources/suetterlin-1922/SOURCE.md:74
- The cited §15 says seven crops were below the 3 px standard, but only four had negative clearance; the other three still had 1–2 px of air (
qualitaetsmetrik.md:3646-3659). Calling all seven “wirklich angeschnitten” makes this provenance note contradict its source.
Buchstabe oder ein Diakritikum fehlt. Die sieben wirklich
angeschnittenen Proben sind seit der Rechteck-Reparatur
`sep01` repariert statt markiert
(`qualitaetsmetrik.md` §15) — die Marke ist das zweite
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review finding: the note defined `incomplete` as any clipping of the specimen's OWN ink and then, two sentences later, cleared four specimens whose own lead-in ink is clipped. A later audit could not have decided those rows from the rule alone. The rule now names the boundary it always meant: LETTERFORM ink. A clipped lead-in or run-out hairline does not qualify, because those connecting strokes are generated from the entry/exit tangents rather than traced (architektur.md §3) — the flag is for ink no one can put back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
MarkusNeusinger
added a commit
that referenced
this pull request
Sep 6, 2026
…556) One pre-registered measurement round on the owner's design input of 2026-09-06: "this loop must stay open" is not a threshold for the whole hand, it is a property of ONE loop of ONE letter, read off the plate — a size class, and for the small ones a state (open · varying · Punktkringel, closed by construction). **This is not an arm.** No candidate, no switch, no adoption, no `core/` byte, no DB write, no root re-export, no ruler touched. The frozen `sep05` roots (`eaa195aa7c84` / `0fbde2d72b64`) were used as they stand and reproduce their headline digit for digit (**0.108444 · 0.148236**) with `--expect-root` and BLAS pinned. What ships is a frozen catalogue, a report-only sensor, and the decomposition of the "26 closing words" #551 left standing. ## Part 1 — the catalogue `tools/tracebench/kringel_catalogue.json`, built by `tools/tracebench/kringelcat.py` from one frozen root. **46 loops over 27 glyphs**, each with the plate's own aperture, the composed one, a size class and a state. | | klein | mittel | groß | Σ | |---|---|---|---|---| | **offen** | 18 | 15 | 8 | **41** | | **wechselnd** | 3 | 1 | – | **4** | | **Punktkringel** | 1 | – | – | **1** | | Σ | **22** | **16** | **8** | **46** | Both rules were written down before the first catalogue number and are not moved by it: - **Size in pen widths.** The plate's pen has full width `W = 2·0.0968 = 0.1936 xh` and erodes a centerline loop by exactly `W`, so `klein` is `D0 < 2W`, `mittel` `2W ≤ D0 < 4W`, `groß` `D0 ≥ 4W`. The cut describes the instrument, not the sample. Afterwards, honestly: the `2W` cut does **not** fall in a gap — 18 values sit between 0.3049 and 0.4787 — while `4W` does (0.7212 → 0.8057). Both stay where they were. - **State from the plate.** The share of occurrences in which the plate shows a hole at all: `offen ≥ 0.8`, `punkt ≤ 0.2`, otherwise `wechselnd`. Blind to how WIDE the hole is — a Punktkringel is defined by the plate never opening it. **Acceptance against #551:** six of the nine narrow glyphs read digit for digit (`o` `sz` `g` `r` `v` `G`), `a` +0.0016, `p` +0.0086, `k` +0.0396 (the last two because the slot ruler attributes one occurrence more each). `w_pen` **0.0968** and **202** plate counters are #551's numbers too. **Two corrections after the first pass, named rather than hidden.** The pre-registered assignment was containment (the plate hole's centre inside the composed loop's region) — it claims only **121 of 202** counters, because a narrowed loop no longer contains the hole, i.e. it fails exactly at the object of measurement. Replaced by the **slot ruler** (the counter belongs to the slot whose own strokes span its x, then one-to-one by centre distance inside the letter): **183 of 202**. The third ruler carried alongside (overlapping inscribed circles) sits at 144. And a **splinter floor of 0.05 xh** was added when a 0.015 xh loop of the `w` claimed a 0.59 xh counter — without it a COLLAPSED loop is booked as a narrow one instead of a missing one. ## Part 2 — the sensor, report-only `tools/tracebench/kringel.py` adds `kringel <lost>/<offen>` to every word line and `kringel_lost` / `kringel_wechselnd_zu` to the block. Only an `offen` loop the delivered pen runs shut is a loss; `wechselnd` closures stand apart because the plate closes those itself, and `punkt` loops are exempt. Report-only, proved rather than asserted: | run | before | after | |---|---|---| | `wordbench.run --set all` | 0.108444 · 0.148236 | **0.108444 · 0.148236**, report identical but for `runtime_s` | | `tracebench --split all --candidate authored` | dtw 0.000000 · aiou 0.7308 · gate PASS | **68 lines identical**, line for line | | `tracebench --split dev --candidate chain` | dtw 0.049757 · p90 0.091389 | **56 lines identical** | The price is one extra composition pass (as for the Duktus-Soll): the 29-word identity run goes 114.8 → 175.3 s. ## Part 3 — the 26, re-read On #551's own path (its eight loop keys, and only where the plate shows the hole in that very occurrence) this apparatus finds **27 words**, one more because the slot ruler attributes an extra `a` and an extra `k`. Of those 27: - **24 carry a real `offen` loss**, - **3 only a `wechselnd` one** — `Sprünge` · `Zügel` · `regieren`, all through the `g` bowl — and are therefore not defects, - **0 are Punktkringel.** **The honest number that replaces 26 is 24.** But the full catalogue corrects the other way: over ALL loops, **34 of the 63 word specimens** lose an `offen` loop at half width 0.097 (44 occurrences), and beside them stand **19 topology losses** in 14 words — counters no composed loop accounts for at all. 36 of the 63 words carry one or the other. | loop | class | losses | deficit | rescue path | |---|---|---|---|---| | `a`#0 | klein | 15 | +0.1055 | **R3** — loop geometry from the evidence | | `t`#1 | mittel | 9 | +0.3395 | two-stroke model at fused loops; the `t` first needs a ductus loop range at all | | `o`#0 | klein | 5 | +0.1461 | **R3** | | `sz`#0 | klein | 5 | +0.1349 | **R3** | | `G`#0 | klein | 3 | +0.1367 | skeleton loops for capitals (no running-form row, H3) | | `r`#0 | klein | 3 | +0.1182 | running-form coverage — the row survives, the chart fallbacks don't (#551) | | `k`#1 | mittel | 2 | +0.2366 | skeleton loops for capitals | | `v`#0 | klein | 2 | +0.1027 | skeleton loops / author path | | topology: `w` `sz` `G` `m` `n` `P` `S` `e` `f` | – | 19 counters | – | medial-axis term (R4 indicator) + two-stroke model | Two findings worth the round on their own. **The `d` loop needs nothing** — our row draws it 0.017 xh WIDER than the plate, as do `s`#0 and `k`#2. And **the `t` is the blind spot**: `core.aggregate.loop_ranges` has no loop range for it (nor for `n` `m` `i` `u` `c`), the plate holds two counters there in 9 of 9 occurrences, and `t`#1 (0.0517 against 0.3912) is the catalogue's largest deficit — and the ONLY loss that survives at the delivered nib 0.0724, where #551 counted zero. ## Filing §14 entry „Kringel-Landmarke `sep06`" at the end of `messjournal.md` with its register row, a §7.9 rescue-path row in `tintenfolger.md`, the landmark sentence in `tintenfolger.md` §2.3 and in `menschliche-bewertung.md` §3.6b, glossary entries **Kringel-Landmarke** and **Topologie-Verlust (Kringel)** (plus the Schnellindex and the Kurzglossar), the tool in `werkzeuge.md`, and a changelog fragment. `mess-runde` was raised 21 057 → 23 179 with the arithmetic written into `tools/docs_budget/__init__.py` — one dense register row, 15 tokens short, re-measured plus the documented 10 %; the §14 entry itself was trimmed to 4 500 against its unchanged 4 503 ceiling instead. ## The review round Eleven Copilot threads, all acted on — the full point-by-point is in the [reply comment](#556 (comment)). Three of them changed something worth naming here: - **A deferred diacritic carries its slot's index but is flushed at the END of the word**, so a slot's naive index range ran past the last letter: the "connector after the slot" was a foreign item and the mark joined the stroke set the loops were measured on. Fixed at both call sites through one shared `body_items`. On this root it is a **clean null probe** — the rebuilt catalogue is byte-identical apart from its new `style` key, because a mark sits directly above its own letter. The reading was right by luck, not by construction, which is why the fix stays. - **The catalogue now belongs to one hand and says so.** Every class in it is counted in the width of this plate's pen, so applying it to a `--style kurrent` run would publish a fabricated expectation; the sensor compares the catalogue's style and root against the run's and omits the column on a mismatch. - **The committed catalogue is written down as a NAMED exception** to the open-core rule in `quellen-und-rechte.md` §5, beside the gitignore rule it qualifies: it is an expectation ruler, not a holding — no geometry, no per-occurrence rows, no word specimen, so no letter can be reconstructed from it. A test keeps that true, and the exception is written as covering this shape only. Plus three refusals where a typo would otherwise have produced a plausible column that means nothing (a non-finite or negative pen width, a malformed catalogue row escaping as `KeyError` past the "costs the column, never the run" contract, and the per-word boundary that covered only the composition), and three factual corrections: the changelog's totals, the entry's "largest relative loss" claim (`t`#1 keeps 13 %, `e`#0 19 %), and the Kurzglossar's term count, which had already drifted before this round. ## Verification `/verify-core` on the merged base: **2524 passed, 8 skipped**; `ruff check` and `ruff format --check` clean over 370 files. 26 new tests pin the two classification rules, the raster loop finder against circles of known diameter, the slot grouping including a deferred mark, the report contract (`punkt` exempt, `wechselnd` apart, an unknown glyph counted separately), and the four refusals — a missing catalogue, a malformed row, a word outside the vocabulary, and a catalogue from another hand or another root. `docs_budget check`, `docs_register check` and the changelog fragment gate pass. The catalogue rebuilds byte-identically from the same root, and **both report-only proofs were re-run on the MERGED base** after `origin/main` moved under the branch: words 0.108444 · pairs 0.148236 unchanged, authored identity run 68 lines identical. `origin/main` moved mid-review (#555, #557, #558) and the PR went `CONFLICTING`, which starts no CI at all rather than a red one. Merged in and resolved: #558 raised the same `mess-runde` budget on the same day for its own register pair, so both raise comments stay and the budget is **re-measured once on the merged file** (21 722 for all three rows) instead of being added up from two branches that each measured without the other; the two Schnellindex lines and the two appended journal entries keep both sides. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3 --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
words.jsonsidecar definesincompletefor a word specimen whose OWNink the crop rect clips — the flag that takes a never-traceable specimen out
of the admin's „Offen" list and out of the tracing tally's denominator (#470).
Not one of the 202 entries carries it. This PR checks whether that is right,
and writes the answer down where the flag is defined.
What was measured
A boundary sweep over every rect, three sensors, all read-only:
is one object seen from both sides. This is the sensor that matters: a
per-crop adaptive threshold fringes on the abb22 scan and reports dozens of
phantom edge touches.
edge scan cannot see: an i-dot left outside with a gap, touching nothing.
Result: no specimen is missing a letterform or a diacritic. No body ink
leaves any rect to the right, the top or the bottom at any threshold down to
0.78 grey; the only own ink outside a rect anywhere is a lead-in hairline,
at most 19 px. No orphan diacritic sits above any rect. Every rect keeps the
3 px
BOX_PAD_PXstandard except where foreign ink reaches in.The eleven rects an ink component does cross:
Gewehr,Zügel,streiten,a22-dankregieren,a22-Krüger,a22-dank-2,a22-hinausalready carry as anexcludeladen,a22-ladena22-fern,a22-eina22-laden,a22-ihren,a22-doch,a22-roten-2A blind second pass over the rendered crops, given no expected answer, agreed
on all sixteen specimens it was shown.
Why the answer is a negative, and expected
The seven specimens that really were clipped —
regieren,zum,einer,das,und,Wer,zwei— were repaired onsep01and the word benchre-baselined (
qualitaetsmetrik.md§15), because the flag is the secondremedy, not the first: where only the RECT cuts the ink, the rect is fixed
(
tools/wordbench/repair_boxes.py), andincompleteis left for ink thatends on the plate itself. Six of those seven are in the candidate list an
automatic edge scan still produces — the list predates the repair.
So the sidecar note now carries both halves: the repair-before-flag rule, and
the sweep with the entries it clears by name. The next edge scan that lists
the same rects then reads as a settled question rather than as work waiting.
What is NOT in this PR, and why
is a declared re-baseline, and nothing here needs one.
excludeadded for the four commas that reach into their crop,although
regieren's comma has exactly such a box. Adding one changesref_maskand is therefore a re-baseline too — the author's call, notedhere as a finding rather than taken.
changelog.d/README.md;the provenance lives in
SOURCE.md, whoseMaße/SHA256forwords.jsonare updated in the same commit.
Verification
noteline plus theSOURCE.mdblock — noentry touched, so no rect,
excludeor lineature moved and the frozenfixtures stay byte-identical.
uv run --extra test pytest— 2465 passed, 18 skipped.ruff check+ruff format --check— clean.npm run test— 296 passed (theincompletefilter path is pinned bytraceStatus.test.tsand is untouched).their bytes,
words.jsonincluded; requiredSOURCE.mdfields intact; nonew files.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3