The print already named its marks, and nothing had asked - #85
Merged
Conversation
`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
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.
packages/core/src/skins.tshas 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 ofassets/pages/**, which is what the app draws — and false of the ligature corpus, which names every mark it draws in adata-diacriticattribute. 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
packages/core/src/diacritics.ts(+ test)packages/etl/scripts/lib/diacritics.mjsreadDiacritics(svg, apply)— a named, exact box per mark, grouped by its wordpackages/etl/scripts/probe-diacritics.mjspnpm probe:diacritics— the corpus-wide rundocs/design/sub-word-marks.mdWhat was measured
pnpm probe:diacritics, all 604 cached pages:data-diacriticvaluesDIACRITICS; none unknownThe 4,487 words carrying no mark are the 4,486 pause marks
gate:wordsalready 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
DIACRITICSis a wire format. A shard says0, 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 behindgate: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.
applyis handed in;applierFromPinrebuilds it from the four numbersword-boxes.pin.jsonalready 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:assetsnames 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 asconfirmed(③) 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 newmake cigreen under the lock (33s);gate:issuesOK, 87 indexed across 13 registers;gate:mapOK, 154 pointersdocs/map.jsonhand-edited, never generated🤖 Generated with Claude Code
https://claude.ai/code/session_01EuhvbUKjGesE3uMhjCzBGt