Skip to content

Print the Bogen fainter, and let one word jump a frozen plan - #555

Merged
MarkusNeusinger merged 2 commits into
mainfrom
eigenhand-feine-lineatur-und-anheftung
Sep 6, 2026
Merged

Print the Bogen fainter, and let one word jump a frozen plan#555
MarkusNeusinger merged 2 commits into
mainfrom
eigenhand-feine-lineatur-und-anheftung

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Two owner wishes for the own-hand capture sheets (2026-09-06), both about what the FIRST printed Bogen has to be. No sheet has been printed yet, so neither change costs paper.

1 · A fainter lineature

Owner: "as long as it is faintly recognisable in the print, it is strong enough." Every ruling of the capture theme (core/eigenhand/geometry.py::CAPTURE_STYLES) moves down one step — the baseline takes the width the auxiliary lines had, and the auxiliary lines go thinner again:

Role before now
baseline 0.22 mm 0.12 mm
waist 0.15 mm 0.10 mm
ascender/descender (dashed) 0.12 mm 0.08 mm
slant grid (dashed) 0.10 mm 0.06 mm
word box + verdict box 0.12 mm 0.12 mm (unchanged)

Why thinner is the safe direction: the lineature only has to guide the hand for the seconds a row takes, and everything it does afterwards is damage. Faint cyan still prints — 0.06 mm is 1.4 dots at 600 dpi, so the device draws its thinnest line rather than nothing — while every millimetre of cyan that is NOT on the paper is one the ingest cannot mistake for ink, cannot leave in the blue channel and cannot smear into a stroke the fit measures. The box role keeps 0.12 on purpose: the same role draws the verdict box in the right margin, which the writer has to find and tick (a forgotten tick discards the row).

The order baseline > waist > ascender/descender > slant survives, so the row still reads at a glance. Positions, Passmarken and the printed ruler check are byte-identical — only stroke widths moved, which is why every layout test and every cfg stamp stayed put and only the golden PDF hash is re-pinned (declared re-baseline, with the reason next to it).

The owner's print task text stays valid as written: colour printer, A4, 100 %, ruler on the Passmarken at 190.0 / 277.0 mm. data/samples/own-hand/README.md gains one rule saying the lineature is deliberately this faint — a print in which it is barely visible is the right one, not a printer fault.

2 · „Kurrentschrift" on the first sheet — the Anheftung

The word is the site's hero and the author wants a written specimen early. It was not in the Wortvorrat, and the strip plan is frozen at 180 strips across three waves, so it could not be put INTO a strip: append-never (Prüfstein 4) forbids exactly that.

The mechanism is the smallest one that fits the doctrine instead of bending it:

  • tools/eigenhand/corpus.py gains a pin curation layer with PINNED_FIRST — words that earn their place by what they are, not by their coverage. So the word is a normal pool word: it enters the Soll universe and is counted in Bestand and coverage like any other.
  • python -m tools.eigenhand.pool pin appends ONE strip of its own per pinned word and registers it in the plan's new pins block. verify_immutable runs on this path too; the diff to streifen.json is purely additive.
  • core/eigenhand/plan.py::ordered_strips puts pins at the head of plan order. The pin is expressed in plan order rather than in the queue so that every reader agrees on what comes first — print queue, coverage progression and Bestand walk the one order. A pin leaves the front the ordinary way: once the strip is belegt it is out of the queue.

Result: S0181 = „Kurrentschrift", row 1 of the next Bogen (B0001, seven rows, then S0001 … S0006 as before). Spelling: the plain word, which core/shaping.py already shapes as Kurrentſchrift — the default rules give the long ſ at the start of the second morpheme -schrift, so no fugen form is needed and the printed label carries no hint marks. pins is additive, so the plan format stays 2; a second pool pin run is a no-op ("already planned, not pinned again").

Three decisions taken here and written down in the proposal: the plan stays hand-agnostic (one plan serves all three scripts — a hand-scoped pin would need hand-scoped plans), a word already planned is never pinned again, and a pinned strip is not filled up with quota words (that would need the local-only Übergangsraum weights; the box generator hands the free row width to the one word, so „Kurrentschrift" gets the full 180 mm).

Verification

  • /verify-core: full suite green (2474 passed, 18 skipped), ruff check + ruff format --check clean.
  • Direct invocation instead of the synthetic smoke, because the diff is the sheet: one Bogen composed and rendered to PDF before/after, rasterised at 300 dpi, plus a 4× crop of the same row in both versions. The row reads clearly and prints faintly.
  • tools.docs_budget check, tools.docs_register check, tools.changelog check green.
  • No bench numbers: this core/ change touches page geometry and plan order, not extraction, composition or glyph rendering.

New tests: the capture lineature stays strictly fainter than the app's druck theme, keeps its hierarchy and stays printable; a pin appends and leads without touching a frozen strip; an already-planned word is not pinned again; an uncurated word is refused; the committed plan pins what the curation pins; the pinned strip is row 1 of the first sheet, and a written pin leaves the front of the queue.

New term Anheftung is in docs/reference/glossar.md (§5 + Schnellindex). It is not in kurzglossar.md: that file's own Nachzieh rule (docs/dokument-status.md) is the two-of-three threshold, and the term reaches one source so far.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3

Two owner wishes for the own-hand capture sheets, both about what the
first printed Bogen has to be.

The lineature drops one step per role: the baseline takes the width the
auxiliary lines had (0.22 -> 0.12 mm), waist 0.15 -> 0.10,
ascender/descender 0.12 -> 0.08, the slant grid 0.10 -> 0.06. It only has
to guide the hand for the seconds a row takes, and everything it does
after that is damage — so thinner is the safer direction: faint cyan
still prints (0.06 mm is 1.4 dots at 600 dpi), while every millimetre of
it that is NOT on the paper is one the ingest cannot mistake for ink or
smear into a measured stroke. The `box` role keeps 0.12 because the same
role draws the verdict box, which the writer must find and tick.
Positions, Passmarken and the printed ruler check are untouched; the
golden PDF hash is re-pinned as a declared re-baseline.

"Kurrentschrift" was not in the Wortvorrat and the plan was frozen at 180
strips, so the word could not be pushed into a strip. `pool pin` appends
one strip of its own for every word in `corpus.PINNED_FIRST` and
registers it in the plan's new `pins` block; `plan.ordered_strips` puts
that block at the head of plan order, so the print queue, the coverage
progression and the Bestand all lead with it. Append-never holds — the
guard runs on this path too, and the pin leaves the front the ordinary
way, by being written. S0181 carries "Kurrentschrift" (shaped
`Kurrentſchrift`: the default rules give the long s at the start of the
second morpheme, so no fugen form and a plain label), and it is row 1 of
the next Bogen.

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 21:57

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

The repeatable pin path can combine multiple pinned words into one strip, violating the documented one-strip-per-pin contract.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Refines Eigenhand capture sheets and adds pinned words to the frozen strip plan.

Changes:

  • Makes capture rulings fainter while preserving layout geometry.
  • Adds append-only pinned strips, led by “Kurrentschrift”.
  • Updates tests, documentation, plan data, and changelog.
File summaries
File Description
tools/eigenhand/pool.py Adds pinned-strip generation and CLI support.
tools/eigenhand/corpus.py Curates the first pinned word.
core/eigenhand/plan.py Places pinned strips first.
core/eigenhand/geometry.py Reduces capture ruling widths.
core/eigenhand/streifen.json Appends pinned strip S0181.
tests/test_eigenhand_stack.py Tests print-queue pin behavior.
tests/test_eigenhand_pdf.py Re-baselines PDF output.
tests/test_eigenhand_geometry.py Tests capture ruling properties.
tests/test_eigenhand_coverage.py Tests pin creation and immutability.
docs/proposals/eigenhand-erfassung.md Documents both design changes.
docs/reference/werkzeuge.md Documents the pin command.
docs/reference/glossar.md Defines “Anheftung”.
data/samples/own-hand/README.md Updates printing instructions.
changelog.d/eigenhand-feine-lineatur-und-anheftung.md Records user-visible changes.
Review details

Suppressed comments (1)

tests/test_eigenhand_geometry.py:77

  • The numeric explanation is reversed: one 600 dpi dot is about 0.042 mm, so 0.05 mm is slightly more than one dot, not under one. Correcting this keeps the test's physical threshold understandable.
        # A line the printer cannot resolve is guidance that never reaches the
        # paper: 0.05 mm is under one dot at 600 dpi (0.042 mm).
  • Files reviewed: 14/14 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/test_eigenhand_geometry.py Outdated
Comment thread tools/eigenhand/pool.py Outdated
Both from the Copilot review, both real.

`pin_words` ran the fresh words through the wave packer, so two short
pins would have shared a row — while the docstring, the proposal, the
glossary and the README all promise each pinned word a strip of its own
and the full row width. Pins are few and deliberate, so the promise is
the better half: one strip per fresh word, with the reason next to it,
and a test with two short pins that the packer would have merged.

The hierarchy test compared against a reverse-sorted copy, which accepts
two roles at the same width — that is exactly the state the assertion
exists to forbid. It now compares adjacent widths with `>`. The printable
floor's comment had its arithmetic backwards (0.05 mm is about one dot at
600 dpi, not under one); it says what the floor means instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
@MarkusNeusinger

Copy link
Copy Markdown
Owner Author

Both review findings are fixed in 743f475.

  • tools/eigenhand/pool.py: every fresh pinned word now gets a strip of its own instead of going through the wave packer. The docstring, the proposal, the glossary and the README all promised exactly that — one strip, full row width — and pins are few and deliberate, so keeping the promise is better than saving a row. A test with two short pins ("das", "lesen") now covers the case the packer would have merged; the committed plan is unchanged, since it holds a single pin.
  • tests/test_eigenhand_geometry.py: the hierarchy assertion compares adjacent widths with > instead of against a reverse-sorted copy, so two roles at the same width fail as they should. The printable floor's comment also had its arithmetic backwards (0.05 mm is about one dot at 600 dpi, not under one) and now says what the floor means.

@MarkusNeusinger
MarkusNeusinger merged commit c0cc942 into main Sep 6, 2026
8 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the eigenhand-feine-lineatur-und-anheftung branch September 6, 2026 22:16
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