Skip to content

Record the sweep that leaves no word specimen incomplete - #557

Merged
MarkusNeusinger merged 2 commits into
mainfrom
sidecar-incomplete-sweep
Sep 6, 2026
Merged

Record the sweep that leaves no word specimen incomplete#557
MarkusNeusinger merged 2 commits into
mainfrom
sidecar-incomplete-sweep

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

The words.json sidecar defines incomplete for a word specimen whose OWN
ink 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:

  • Ink on the border line of each rect, excludes painted white first.
  • Connected components on the whole plate, so a stroke that leaves a box
    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.
  • Diacritic-sized specks in the band above a rect, for the failure the
    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_PX standard except where foreign ink reaches in.

The eleven rects an ink component does cross:

Specimen What crosses Verdict
Gewehr, Zügel, streiten, a22-dank the comma after the word foreign — same case regieren, a22-Krüger, a22-dank-2, a22-hinaus already carry as an exclude
laden, a22-laden the descender of the line above foreign
a22-fern, a22-ein the previous word's last letter / run-out foreign
a22-laden, a22-ihren, a22-doch, a22-roten-2 the word's own lead-in hairline, ≤ 19 px intact — the letterforms are whole and the renderer generates the Anstrich anyway

A 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 on sep01 and the word bench
re-baselined (qualitaetsmetrik.md §15), because the flag is the second
remedy, not the first: where only the RECT cuts the ink, the rect is fixed
(tools/wordbench/repair_boxes.py), and incomplete is left for ink that
ends 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

  • No rect changed. The corners are frozen wordbench fixtures; widening one
    is a declared re-baseline, and nothing here needs one.
  • No exclude added for the four commas that reach into their crop,
    although regieren's comma has exactly such a box. Adding one changes
    ref_mask and is therefore a re-baseline too — the author's call, noted
    here as a finding rather than taken.
  • No changelog fragment: data-only PR, exempt per changelog.d/README.md;
    the provenance lives in SOURCE.md, whose Maße/SHA256 for words.json
    are updated in the same commit.

Verification

  • Diff is the sidecar's top-level note line plus the SOURCE.md block — no
    entry touched, so no rect, exclude or lineature moved and the frozen
    fixtures stay byte-identical.
  • uv run --extra test pytest — 2465 passed, 18 skipped.
  • ruff check + ruff format --check — clean.
  • npm run test — 296 passed (the incomplete filter path is pinned by
    traceStatus.test.ts and is untouched).
  • License battery for the scoped diff: all 15 recorded SHA256 hashes match
    their bytes, words.json included; required SOURCE.md fields intact; no
    new files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3

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
Copilot AI balanced review requested due to automatic review settings September 6, 2026 22:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Comment thread data/sources/suetterlin-1922/words.json Outdated
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
MarkusNeusinger merged commit 6793923 into main Sep 6, 2026
8 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the sidecar-incomplete-sweep branch September 6, 2026 22:14
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>
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.

2 participants