Skip to content

The print already named its marks, and nothing had asked - #85

Merged
omars-lab merged 2 commits into
mainfrom
mark-a-vocabulary
Aug 7, 2026
Merged

The print already named its marks, and nothing had asked#85
omars-lab merged 2 commits into
mainfrom
mark-a-vocabulary

Conversation

@omars-lab

Copy link
Copy Markdown
Owner

packages/core/src/skins.ts has said since Loop 6a that a tajweed colour cannot land on part of a word, because the print's glyphs are "anonymous outlined <path>s". That is true of assets/pages/**, which is what the app draws — and false of the ligature corpus, which names every mark it draws in a data-diacritic attribute. The same comment already named that corpus as its own gate.

This PR is the measurement that settles it, and ships nothing to apps/web/public/assets.

What it adds

file what
packages/core/src/diacritics.ts (+ test) the twenty-six names, in frequency order, and the id a shard would use
packages/etl/scripts/lib/diacritics.mjs readDiacritics(svg, apply) — a named, exact box per mark, grouped by its word
packages/etl/scripts/probe-diacritics.mjs pnpm probe:diacritics — the corpus-wide run
docs/design/sub-word-marks.md the design of record, plus four open-question rows

What was measured

pnpm probe:diacritics, all 604 cached pages:

measure result
distinct data-diacritic values 26 of 26 in DIACRITICS; none unknown
marks extracted 326,515 on 86,964 of 91,451 words
marks outside their own word's box 0 (slack 0.2 — rounding, not registration)
words with no box in the committed shard 0
what the shards would weigh 7.35 MB raw / 2.28 MB gz across 604 files

The 4,487 words carrying no mark are the 4,486 pause marks gate:words already counts, plus exactly one — 20:1's opening, which this print writes with nothing above or below it. An off-by-one would have scattered those empties.

Three decisions worth reviewing

The order of DIACRITICS is a wire format. A shard says 0, not "fatha", because a page carries ~540 marks and the names are the larger half of the bytes. Appending is safe; reordering silently re-labels the whole corpus and moves no geometry, so nothing that measures rectangles would notice. Six ids are pinned in a test for that reason, and it is filed as its own open row (④).

Containment is the invariant, and it is checked against the committed shards. A word's shipped box is union(pathBBox) over every path in its group, marks included — so containment holds by construction, and an escape can only be an alignment error, a mark filed under the wrong word. That is the failure class behind gate:edges. Verifying against boxes computed in the same pass would let two sides of one computation share their mistakes and agree about them.

Nothing here fits anything. apply is handed in; applierFromPin rebuilds it from the four numbers word-boxes.pin.json already records. A second fit could disagree with the first by a tenth of a unit, which is indistinguishable from a mark on the wrong letter.

Why it ships nothing

2.28 MB of assets no caller fetches is the same waste gate:assets names for a non-vendored edition — the download is paid for and unreachable. So the order is mark-A (this) → mark-B, draw the marks in the encoding inspector and see whether a tajweed span lands on one → mark-C, only then pay the bytes. That is also the answer given when asked where the marks should appear first: the inspector, then the app.

skins.ts's stale comment is left in place and filed as confirmed (③) rather than edited: it is load-bearing prose about why a shipped feature has the granularity it has, and rewriting it before the replacement exists would leave the file claiming a capability the app does not have.

Verification

  • pnpm --filter @hifth/core test — 470 passing, 6 new
  • make ci green under the lock (33s); gate:issues OK, 87 indexed across 13 registers; gate:map OK, 154 pointers
  • docs/map.json hand-edited, never generated

🤖 Generated with Claude Code

https://claude.ai/code/session_01EuhvbUKjGesE3uMhjCzBGt

omars-lab and others added 2 commits August 7, 2026 08:20
`skins.ts` has said since Loop 6a that a tajweed colour cannot land on part of
a word, because the print's glyphs are "anonymous outlined `<path>`s". That is
true of `assets/pages/**`, which is what the app draws. It is false of the
ligature corpus, which names every mark it draws in a `data-diacritic`
attribute — and the same comment already named that corpus as its own gate.

This is the measurement, and nothing else.

  - `packages/core/src/diacritics.ts` — the twenty-six names, in frequency
    order, with the counts beside them. Twenty-six is measured over all 604
    pages, not chosen. A shard would say `0`, not `"fatha"`, because a page
    carries ~540 marks and the names are the larger half of the bytes — which
    makes the array's *order* load-bearing in a way nothing else in core is.
    Appending is safe; reordering silently re-labels the whole corpus and moves
    no geometry, so nothing that measures rectangles would notice. Six ids are
    pinned in a test for that reason. Dots are excluded on purpose: i'jam is
    part of the letter's identity, not a mark a reader is told to look at.

  - `packages/etl/scripts/lib/diacritics.mjs` — a named, exact box per mark,
    grouped by the word it sits on. It fits nothing: `apply` is handed in and
    `applierFromPin` rebuilds it from the four numbers `word-boxes.pin.json`
    already records, so a mark cannot land on a second transform that disagrees
    with its own word's by a tenth of a unit. It throws on a name core has
    never heard of rather than inventing an id.

  - `packages/etl/scripts/probe-diacritics.mjs` — the corpus-wide run. It ships
    nothing: 2.28 MB of assets no caller fetches is the same waste
    `gate:assets` names for a non-vendored edition.

The invariant that makes any of this verifiable offline is that a word's
shipped box is the union over *every* path in its group, marks included. So
containment holds by construction, and an escape can only be an alignment
error — a mark filed under the wrong word, the failure class behind
`gate:edges`. Checked against the *committed* shards rather than boxes computed
in the same pass, because two sides of one computation share their mistakes.

  26 of 26 names drawn · 326,515 marks · 0 outside their word · 0 unmatched
  7.35 MB raw / 2.28 MB gz · smallest mark 1.8 units on our frame

The 4,487 words carrying no mark are the 4,486 pause marks `gate:words`
already counts, plus one: 20:1's opening, which this print writes with nothing
above or below it. An off-by-one would have scattered those empties.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuhvbUKjGesE3uMhjCzBGt
`docs/design/sub-word-marks.md` — the design of record for the named-mark
layer, and the place §② keeps two different things called a mark apart: the
word shards' `marks` are *pause* marks whose integers are word indices, and
these are drawn inside a word. That collision is why the asset kind would be
`diacritics`, and why `DIACRITICS` deliberately cannot name a waqf sign.

Four rows under its open-question section, indexed in `docs/issues.json`:

  ① does a tajweed span land on a mark a reader can be shown · open
     Two measurements now exist over the same words and nothing has checked
     whether they meet. Only the encoding inspector can: the correspondence is
     between a codepoint in a reconstructed text and an outline on a page.
     Deriving it from the fact that both numbers exist is the circularity
     `word-indexing.md` ⑪ ⑤ names about its own oracle.

  ② are the shards worth 2.28 MB, and against what ceiling · open
     Waiting on a caller, not on a number. `gate:assets` fails outright on a
     kind it has never heard of, and ⑪ ① is the standing warning that shipping
     an answer costs multiples of shipping the question.

  ③ `skins.ts` still says the print's glyphs are anonymous · confirmed
     Left as a defect rather than edited in place: it is load-bearing prose
     about why a shipped feature has the granularity it has, and rewriting it
     before the replacement exists would claim a capability the app lacks.

  ④ a mark id is only meaningful against an array's order · open
     Deferred deliberately — the risk does not exist until something is
     shipped that an id can be stale in.

`docs/map.json` gains four pointers on `word-geometry`, hand-edited. The doc's
pointer names a body line rather than a heading, because `code-pointers.mjs`
reads a markdown `#` as a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuhvbUKjGesE3uMhjCzBGt
@omars-lab
omars-lab merged commit 5e82cdb into main Aug 7, 2026
6 checks passed
@omars-lab
omars-lab deleted the mark-a-vocabulary branch August 7, 2026 13:25
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.

1 participant