diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md new file mode 100644 index 00000000..6781f132 --- /dev/null +++ b/.claude/memory/MEMORY.md @@ -0,0 +1,10 @@ +# Memory index + +- [Hifth app identity](hifth-app-identity.md) — Quran navigation app for huffaz, renamed from "Linker", loops plan in docs/PLAN.md +- [Artifacts in English by default](artifacts-english-by-default.md) — mocks/walkthroughs use English chrome, only scripture stays Arabic; watch published artifacts for feedback +- [Desktop edge-grab page turn](desktop-edge-grab-turn.md) — desktop spread turns by grabbing the outer fore-edge (hand cursor); mid-page drags never turn +- [Juz-jump leaf alignment](juz-jump-leaf-alignment.md) — a hop revealed the incoming leaf before centring it (1-frame ~9px flash); fixed by centerCurrent() in navigateTo; rebuild dist before e2e +- [Desktop zoom StrictMode orphan](desktop-zoom-strictmode-orphan.md) — "zoom broken on spread" was a dev-only StrictMode orphan host; teardown now removes hosts; prod was never affected +- [SPA hash nav is not a reload](spa-hash-nav-no-reload.md) — a #-only navigate keeps the old bundle on 5173; force location.reload() before calling a change "not rendered" +- [Playwright WebKit missing](playwright-webkit-missing.md) — iphone e2e project cannot launch locally; run the Chromium projects, ask before installing WebKit +- [Designs public on the site](designs-public-on-site.md) — tenet since 2026-09-01: docs/ pages are served from blog.bytesofpurpose.com/hifth/docs/ by the build; never publish a decision page to claude.ai as its address diff --git a/.claude/memory/artifacts-english-by-default.md b/.claude/memory/artifacts-english-by-default.md new file mode 100644 index 00000000..190e5fb8 --- /dev/null +++ b/.claude/memory/artifacts-english-by-default.md @@ -0,0 +1,25 @@ +--- +name: artifacts-english-by-default +description: Build design artifacts/mocks with English UI chrome by default; keep only scripture Arabic; watch them for comment feedback +metadata: + node_type: memory + type: feedback + originSessionId: e1a37078-db9f-4e9e-a2a4-0d297cd099ff + modified: 2026-08-29T19:28:22.343Z +--- + +When building a visual artifact / mock / walkthrough for review (e.g. the confusion-points +"Where You Slip" page), the **app chrome must default to English** — button labels, menu +items, warnings, list rows, legends. The only Arabic that stays is the **Qur'an text itself** +(ayah words, surah names as they appear on the mus'haf) — that is scripture, not chrome. + +Also: **publish these design artifacts in English mode and set a comment watch** so feedback +can be captured for future sessions. + +**Why:** the owner reviews and leaves feedback in English; Arabic-only chrome in the mocks +makes the experience unreadable to a reviewer and to anyone they show it to, and undercuts the +CLAUDE.md tenet that a decision page be followable by a stranger. + +**How to apply:** default new mock UI labels to English; render ayah text in Arabic; keep a +short glossary for domain words; after publishing, establish/keep a comment watch. See +[[hifth-app-identity]]. diff --git a/.claude/memory/designs-public-on-site.md b/.claude/memory/designs-public-on-site.md new file mode 100644 index 00000000..1ec5cf58 --- /dev/null +++ b/.claude/memory/designs-public-on-site.md @@ -0,0 +1,26 @@ +--- +name: designs-public-on-site +description: "Tenet since 2026-09-01 — every design page under docs/ is public on the app's own site at the same path; never publish decision pages to claude.ai as their address" +metadata: + node_type: memory + type: feedback + originSessionId: c8c77742-fa0c-48ea-9c25-4e720245832a + modified: 2026-09-02T02:13:50.680Z +--- + +The owner wants every design and decision page public-facing, for transparency, and made it a +tenet of the repo (CLAUDE.md "Every design is public"). The web build stages `docs/**/*.html` +onto the site at the same path, so `docs/design/x.html` is +`https://blog.bytesofpurpose.com/hifth/docs/design/x.html`, with a front door at `/docs/` linked +from the colophon. A decision's `artifact` is derived from its `page` and the gate refuses any +other address. + +**Why:** "i want all the designs to be public facing for transparency" / "this should be a tenant +for our repo". Earlier, nine pages went to claude.ai and the tree named five; a link on another +host dies with the host. + +**How to apply:** publishing a design = merging it. Do not publish pages to claude.ai as the +decision's address; a conversational copy is allowed but goes in `docs/artifacts.json` as a copy. +Pages reference vendored art as `../../apps/web/public/assets/…` and the staging step rewrites +it; any other dangling relative link fails the build. See [[hifth-app-identity]] and +[[artifacts-english-by-default]]. diff --git a/.claude/memory/desktop-edge-grab-turn.md b/.claude/memory/desktop-edge-grab-turn.md new file mode 100644 index 00000000..5631ded4 --- /dev/null +++ b/.claude/memory/desktop-edge-grab-turn.md @@ -0,0 +1,45 @@ +--- +name: desktop-edge-grab-turn +description: "On the desktop spread the page turns only by grabbing its outer fore-edge, not by dragging across the middle" +metadata: + node_type: memory + type: project + originSessionId: c8c77742-fa0c-48ea-9c25-4e720245832a + modified: 2026-09-01T17:31:44.144Z +--- + +On the desktop two-page spread, turning a page is an **edge grab**, not a swipe +across the page. The user was explicit: hovering the outer sides shows a hand +cursor, grabbing a fore-edge and dragging turns/flips the leaf, and **any drag +that does not start at a page edge must not turn the page** — the middle is left +free to pan and select. Grab zone shape: a strip down each leaf's outer edge that +**widens at the top/bottom corners** (pinched to a sliver at the vertical middle); +the centre fold/gutter is never grabbable. + +**Why:** a hafiz turns a physical mus'haf by its fore-edge; the old +drag-anywhere swipe-to-turn fought selecting and panning on a big screen. + +**How to apply:** the rails live on the book (one outer edge belongs to the +*facing* leaf, which the live stage feels no pointer on) and drive the live +stage's exposed turn verbs; left edge pulls forward, right edge pulls back +(drag-right = forward, matching the swipe convention). Desktop swipe-to-turn is +off (the live stage gets `dragToTurn={!desktop}`); wheel and arrow keys still +turn. The phone is untouched — it keeps swipe-to-turn and has no rails. See +[[hifth-app-identity]]. + +**The grab is a trigger, not a tracked band (as of the peel decision).** Reusing +the phone's finger-locked band on desktop drew a thin fore-edge strip creeping +1:1 over a spread whose two pages had not changed yet — the reader saw "a +vertical bar on the same page," not a turn. So the desktop edge verbs now draw +nothing while the hand moves; on release the commit rule decides, and a +committed grab is handed to the *ordinary* animated turn (the same flip a wheel +or arrow plays — grab, wheel and arrows all funnel through `onTurn → turnTo → +runTurn`). A faithful drag-to-**peel** that reveals the destination *opening* +under a lifting corner is the larger job tracked as the desktop page-turn +animation (#11): it must drive **both** leaves at once (the leaf that lifts and +the destination pages sit on opposite sides of the gutter, and which leaf lifts +depends on turn direction), respect "no 170KB SVG glyph moves" (reveal by +masking a corner off the current page over the already-mounted destination +beneath — never a transform on the page), and wants a live browser to tune. +Because every trigger shares `runTurn`, building the peel there gives scroll and +arrows the peel for free — keep that coupling. diff --git a/.claude/memory/desktop-zoom-strictmode-orphan.md b/.claude/memory/desktop-zoom-strictmode-orphan.md new file mode 100644 index 00000000..d1a1bb8c --- /dev/null +++ b/.claude/memory/desktop-zoom-strictmode-orphan.md @@ -0,0 +1,21 @@ +--- +name: desktop-zoom-strictmode-orphan +description: "Zoom broken on desktop spread" (one leaf grows, one stays) was a dev-only React StrictMode orphan host, not a prod bug +metadata: + type: project +--- + +The desktop-spread "zooming is broken / don't see full pages on zoom" report reproduced +**only in the dev server** (`pnpm dev`, port 5173), never in the production build (`vite +preview`, 4173). Cause: React StrictMode's dev double-invoke mounts → tears down → remounts +against the *same* layer. `PageStage`'s teardown effect cleared the page Map but did **not** +`host.remove()`, so the first mount's host lingered as an orphan `
` outside the Map. Zoom +transforms only the Map's host (`applyTransform` reads `pagesRef.get(currentPage)`), so the +orphan twin sat un-magnified under the one that grew — reading as "one leaf zooms, one doesn't." + +Fix (in `apps/web/src/components/PageStage.tsx`): teardown now removes each host from the +layer; `mountPage` re-reads the Map after its last await and yields to a rival that already +landed the page. Guarded by a StrictMode regression test in `PageStage.budget.test.tsx` +(confirmed it fails `-2/+1` on original code). Production was never affected — StrictMode is a +dev-only wrapper — but the dev server is what a developer previews in. See also the cold-link +blank-leaf reveal fix in the same file (showPage/navigateTo reveal unconditionally). diff --git a/.claude/memory/hifth-app-identity.md b/.claude/memory/hifth-app-identity.md new file mode 100644 index 00000000..d92ac8b2 --- /dev/null +++ b/.claude/memory/hifth-app-identity.md @@ -0,0 +1,21 @@ +--- +name: hifth-app-identity +description: "What the hifth project is — Quran navigation app for huffaz, renamed from design-phase \"Linker\", built in loops per docs/PLAN.md" +metadata: + node_type: memory + type: project + originSessionId: 0da21e77-a946-4228-877a-aee0949749f7 + modified: 2026-07-20T09:33:16.009Z +--- + +Hifth (حفظ) is a pure navigation web app for huffaz: tap/highlight an ayah on real +mushaf SVG pages → hop to similar verses (mutashabihat), shared roots, etc. Knowledge +graph = static adjacency JSON; no backend. Designed in a claude.ai conversation +(shared: https://claude.ai/share/9d43fe10-4c1a-4880-86eb-4adcadb3dfbb) under the +codename "Linker · رابط"; user renamed it **Hifth** on 2026-07-20. + +Key user decisions (2026-07-20): web app first with mobile support (PWA, touch-first); +implement iteratively in loops (each loop = vertical slice demoed on a phone); proper +component architecture is a requirement, not an afterthought. Spec of record: +`docs/reference/linker-spec.md`; plan: `docs/PLAN.md`; interactive mock and architecture +diagram also in `docs/reference/`. diff --git a/.claude/memory/juz-jump-leaf-alignment.md b/.claude/memory/juz-jump-leaf-alignment.md new file mode 100644 index 00000000..1c7602d0 --- /dev/null +++ b/.claude/memory/juz-jump-leaf-alignment.md @@ -0,0 +1,36 @@ +--- +name: juz-jump-leaf-alignment +description: "Why a juz jump could flash the two desktop leaves at different heights, and the one-line fix" +metadata: + node_type: memory + type: project + originSessionId: c8c77742-fa0c-48ea-9c25-4e720245832a + modified: 2026-09-01T17:53:16.966Z +--- + +On the desktop two-page spread, a jump to another juz (or any non-adjacent +relocation) could flash the two leaves at **different heights** for one frame — +the reported "sometimes things aren't aligned after a juz jump." + +**Root cause.** A juz jump is a *hop*, not a page turn: it runs the live stage's +`navigateTo`, which reveals the incoming page (`setCurrentPage` → `display:block`) +and only *frames* it afterwards, inside an `await`ed tween. A freshly mounted host +wears no transform, so for the one paint before the tween's first frame the +incoming page sat at its leaf's top-left — one centring offset (~9px) above the +facing leaf beside it. On a fast machine that is a single frame; on a real one the +incoming ~170KB SVG's parse stalls the correcting frame long enough to see and +screenshot. (The page-*turn* path, `crossFade`, never had this — it applies the +transform synchronously and forces a reflow *before* revealing: the "arrive +already wearing your transform" rule.) + +**Fix.** One line in `navigateTo`: call `centerCurrent()` right after +`setCurrentPage`, so the incoming leaf arrives centred before the tween can yield. +A zoom-1 hop then simply rests there; a closer hop (an ayah hop) tweens out from +fit, which is the correct motion anyway. + +**Guard.** `desktop.spec.ts` → "a juz jump keeps the two leaves level through +every frame" polls both leaves' top edges every frame across the settle and fails +if any frame caught them >1.5px apart (proven to bite without the fix). Note the +e2e gotcha: `vite preview` serves the pre-built `dist/`, so **rebuild (`pnpm +build`) after every source edit** or the test runs stale bytes. See +[[desktop-edge-grab-turn]], [[hifth-app-identity]]. diff --git a/.claude/memory/playwright-webkit-missing.md b/.claude/memory/playwright-webkit-missing.md new file mode 100644 index 00000000..d5cd3d77 --- /dev/null +++ b/.claude/memory/playwright-webkit-missing.md @@ -0,0 +1,20 @@ +--- +name: playwright-webkit-missing +description: "The e2e \"iphone\" project runs on WebKit, which is not installed on this laptop — every iphone test fails at browser launch, not in the app; run desktop/android/golden (Chromium) and ask before installing" +metadata: + node_type: memory + type: project + originSessionId: c8c77742-fa0c-48ea-9c25-4e720245832a + modified: 2026-09-02T00:04:00.172Z +--- + +On 2026-09-01 the full `npx playwright test` run failed every `[iphone]` test with +`browserType.launch: Executable doesn't exist at ~/Library/Caches/ms-playwright/webkit-2311/pw_run.sh`. +The `desktop`, `android` (Pixel 7) and `golden` projects are Chromium and run fine. + +**Why:** WebKit was never installed via `npx playwright install webkit` on this machine. Installing it +is a download from Playwright's CDN, which needs the user's explicit go-ahead first. + +**How to apply:** Verify a change with `--project desktop --project android --project golden`. Report +the iphone project as *not run*, never as failing. Offer the install as a one-line ask rather than +starting it. See [[spa-hash-nav-no-reload]] for the rest of the verification recipe. diff --git a/.claude/memory/spa-hash-nav-no-reload.md b/.claude/memory/spa-hash-nav-no-reload.md new file mode 100644 index 00000000..da208296 --- /dev/null +++ b/.claude/memory/spa-hash-nav-no-reload.md @@ -0,0 +1,26 @@ +--- +name: spa-hash-nav-no-reload +description: "Navigating the dev app to a hash-only URL does not reload the SPA — the DOM keeps the old bundle even after a Vite restart; force location.reload() before concluding a change \"did not render\"" +metadata: + node_type: memory + type: feedback + originSessionId: c8c77742-fa0c-48ea-9c25-4e720245832a + modified: 2026-09-01T23:47:36.435Z +--- + +When verifying a UI change on `localhost:5173`, navigating the tab to `http://localhost:5173/#/...` +(a hash-only change) does **not** reload the document. The React tree keeps running the old +module graph. Restarting Vite and clearing `node_modules/.vite` does not help either, because +the browser never re-fetches the entry. The served source can be verified new (fetch the module +and grep it) while the DOM is still old. + +**Why:** In this session the finished page-bar redesign (30 juz detents, page-icon handle) read as +"not rendering" for several turns — the DOM had zero detents while the served `PageSlider.tsx` +already contained them. One `location.reload()` showed everything. See also +[[desktop-zoom-strictmode-orphan]] for the sibling trap: a fault that shows only under the dev +server's StrictMode double-mount (5173) and not in the built app (4173). + +**How to apply:** After editing, force a real reload (JS `location.reload()` or navigate to a +different path and back) before reading the DOM. If something reproduces on 5173 and not 4173, +say it is dev-only. Rebuild `dist` before e2e and free port 4173 first. The `run-app` skill +carries the recipe. diff --git a/.claude/plans/shimmering-wandering-lobster.md b/.claude/plans/shimmering-wandering-lobster.md index 5c8cc469..30f55c36 100644 --- a/.claude/plans/shimmering-wandering-lobster.md +++ b/.claude/plans/shimmering-wandering-lobster.md @@ -1,155 +1,103 @@ -# The wheel navigates, buttons magnify - -## Context - -**The reported bug:** on the desktop spread, the two leaves get out of sync in zoom when -crossing between one-page and two-page modes. Reproduced in Chrome at 1440×900 against a -local build, it is **three** distinct defects, not one: - -1. **The facing leaf zooms on its own.** `ctrl`+wheel over it took it to `scale(1.549)` - while the live leaf sat at `0.8` and the book stayed open. The comment at - `App.tsx:1189` justifying why the facing stage gets no `onFitChange` — *"the facing leaf - … never receives a hop or a gesture that could change its own scale"* — is **false**: the - facing leaf mounts a complete `PageStage` with its own wheel listener and its own pinch - handler. That untrue comment is why this went unnoticed. -2. **The mode survives a breakpoint crossing; the zoom does not.** Zoom in at 1440 → resize - to 800 (the live stage remounts, view resets to `scale(1)`) → resize back to 1440 leaves - `data-solo="true"` with the host at `scale(1)`: a book closed onto one leaf at fit, with - no zoom to explain it. Recovery requires zooming in and back out. -3. **Zooming *out* counts as "at fit".** `atFit = z <= 1 + 1e-3` and `MIN_ZOOM = 0.8`, so at - `0.8` the book reopens with the live host at 266 px beside the facing leaf's 332 px. - -All three are symptoms of one thing: **`soloLeaf` is derived from zoom**, and zoom lives on -a gesture. The user's call is to cut the derivation rather than patch its three leaks — - -> i don't want zoom to be driven by scrolling … id rather a button to toggle between two -> page and 1 pages mode / settings driven zoom - -— which also composes with the earlier request to make ctrl+scrolling flip juz. - -**Outcome:** the wheel becomes navigation-only; magnification becomes an explicit control; -one-page/two-page becomes an explicit toggle with no derived state behind it. - -## The three answers - -| Fork | Answer | -|---|---| -| What does `ctrl`+wheel do? | **Nothing.** Swallowed (`preventDefault`, no action). Juz-flipping moves to **`Shift`+wheel** | -| Where does zoom live? | A **− / 100% / +** stepper in the desktop chrome. Session state, not persisted | -| Does zoom still auto-close the book? | **No.** The toggle is the sole source of truth for page mode | - -**Why `ctrl` cannot carry the juz jump**, even though it was asked for: a macOS trackpad -pinch *is* a `ctrl`+wheel — the OS synthesises the modifier, and the browser cannot tell it -from a real `ctrl`+scroll (`PageStage.tsx:1940-1943`). Binding juz to it means every -two-finger pinch on a laptop teleports the reader ~20 pages. `Shift`+wheel is unclaimed -here and costs nothing. - -**What this does not cost:** touch pinch is a separate path — `onPinch` via `@use-gesture` -with `pinchOnWheel: false` (`PageStage.tsx:1896`, `1924`). Phones and tablets are untouched -by every change below. - -## Work - -### A. `packages/core` — one lookup, no new arithmetic - -`juzPageIndex(pages)` → 30 entries, the first page of each juz, built on the existing -`juzOf` (the repo already forbids a second membership implementation — `packs.ts:107-109`). -Computed once and memoised at the App level, so a wheel flick is an array index rather than -a 604-page scan. `nextWheelTurn` (`gestures.ts:443`) is reused **unchanged** for the juz -axis with its own `WheelTurnState` ref — the state machine is already exactly right. - -### B. `PageStage.tsx` — the wheel loses zoom, the handle gains it - -- **Wheel handler** (`1930-2004`): the `ctrl`/`meta` branch stops zooming and returns having - only `preventDefault`ed. Swallowing rather than passing through to the browser's own page - zoom is deliberate — "I don't want zoom driven by scrolling" covers browser zoom too, and - letting it through would bounce the desktop breakpoint as CSS px change. One line to - reverse if that reads wrong in the hand. -- **New `Shift` branch → `onJuzTurn?: (step: 1 | -1) => void`.** Hazard to handle: several - browsers deliver `Shift`+wheel as **`deltaX`**, so this branch reads `deltaY || deltaX`. - §6's "only `deltaY` is bound" rule keeps its reason (the horizontal swipe is the browser's - back/forward) and gains this stated exception. Wired on **both** leaves, for the reason - §6 already gives `onTurn`: a wheel over the facing leaf that did nothing reads as a dead - half of the page. -- **Handle gains `setZoom(z): number`** — clamps to `MIN_ZOOM…MAX_ZOOM`, anchors at the - stage centre through the existing `zoomAbout` (`639-653`), returns what it applied. No - second copy of the anchor arithmetic; §7 ⑨'s fix stays the only one. -- **`onFitChange` and `atFitRef` are deleted.** The `ResizeObserver` added alongside them - **stays** — §8 ② records an independent reason (a window resize was one gesture behind - the truth). - -### C. `App.tsx` — explicit state, one write path - -- `soloLeaf` → `pageMode: "one" | "two"` (default `"two"`), `solo={desktop && pageMode === "one"}`. -- `zoom` state holds the **requested** level; the stepper calls `stageRef.current.setZoom` - and stores what it returns. No per-frame callback — §8 ② refused one for a good reason - (`view` is a ref precisely so a pan does not re-render a 170 KB SVG's parent). -- **Every landing resets `zoom` to 1** in the same place it calls `navigateTo`/`showPage`/ - `turnTo` — App is the sole caller of all three, so there is one place to keep in step. - Crossing the breakpoint resets it too, which is defect ② closed by construction. -- Remove `onFitChange` from the live stage. - -### D. `DesktopChrome.tsx` — two controls beside the language switch - -- A **page-mode `radiogroup`** («صفحة واحدة» / «صفحتان»), mirroring the existing `langRow` - at line 57 — same markup, same keyboard behaviour, no new pattern. -- A **zoom stepper**: `−` · readout · `+`, stepping by `1.2×` so it keeps the wheel's old - multiplicative feel (the same proportion at 0.8× as at 5×). -- **The stepper is disabled in two-page mode**, with the toggle beside it as the way out. - §8 ② rendered two magnified leaves and found they lose their edges and read as one - continuous column; and §3's finding is that a leaf is height-bound at ~398 px in a spread, - so zoom there buys nothing anyway. The toggle is the gateway to magnification. -- New strings in `messages/ar.json` + `en.json`, `.gen.ts` regenerated; `gate:i18n` already - enforces parity. - -### E. Tests and registers - -- **`e2e/desktop.spec.ts`** — the "`ctrl`+wheel zooms by a step" test (`709`) and the whole - *"the book closes above fit"* describe (`751-806`) assert behaviour that is being removed; - they are **rewritten, not deleted**: `ctrl`+wheel now turns nothing *and* zooms nothing, - the toggle closes and opens the book, the stepper zooms the live leaf only, `Shift`+wheel - lands on the next juz's first page. Plus a new row for defect ②: zoom at 1440 → 800 → - 1440, and assert the two leaves agree. -- **Unit** — `juzPageIndex` in core; `PageSpread.test.tsx` unchanged in substance (`solo` - was always a prop); `DesktopChrome.test.tsx` gains the two controls. -- **Docs** — `desktop.md` §8 ② is `fixed`, so it gets a superseding note rather than an - edit-in-place (its *outcome* survives; its *mechanism* is replaced); §6's wheel bullets - rewritten; §5 gains the two controls. `page-turning.md` §7 ③'s `ctrl`+wheel half is - superseded and says so. A row in `decisions/desktop-vs-mobile.md`, a new - `docs/decisions/` doc, `docs/issues.json` (+ `pnpm issues:doc`), `docs/use-cases.json` - (+ `make use-cases-doc`), and `docs/map.json` **hand-edited, never generated**. - -### Files - -| File | Change | -|---|---| -| `packages/core/src/packs.ts` (+ test) | `juzPageIndex` | -| `apps/web/src/components/PageStage.tsx` | wheel: −zoom, +`Shift`→juz; handle `setZoom`; −`onFitChange` | -| `apps/web/src/App.tsx` | `pageMode` + `zoom` state, landings reset zoom, juz wiring | -| `apps/web/src/components/DesktopChrome.tsx` (+ css, test) | mode radiogroup + zoom stepper | -| `apps/web/src/messages/{ar,en}.json` + `.gen.ts` | the new strings | -| `apps/web/e2e/desktop.spec.ts` | four rows rewritten, one added | -| `docs/design/desktop.md`, `docs/design/page-turning.md` | §8 ② superseded, §6 and §7 ③ rewritten | -| `docs/decisions/desktop-vs-mobile.md` + a new decision doc | the row and the record | -| `docs/issues.json`, `docs/use-cases.json`, `docs/map.json` | the registers | - -## Verification - -1. `pnpm issues:doc` && `make use-cases-doc`, then `git add -A` && `make ci` green. -2. `make e2e` — the rewritten desktop rows. -3. **Manual, Chrome at 1440×900**, re-running the three original reproductions and asserting - each is gone: `ctrl`+wheel over the facing leaf changes nothing; zoom → resize to 800 → - resize back leaves both leaves agreeing; the stepper at its floor no longer flips the - book. Then `Shift`+wheel lands on a juz boundary and announces it. -4. Commit code and docs separately. - -## Not doing - -- **The per-page "touch bar" strip.** Separate feature, and it has a hard blocker worth - stating: its colour-coded "self-reported mistakes" need a signal that does not exist — - `revision.ts` stores *looks* only, and its doc comment explicitly forbids quietly - absorbing other meanings into a `RevisionEvent`. It needs its own design pass. -- **Persisting zoom across reloads** — a preference surface is a new axis; the stepper is - session state until someone asks otherwise. -- **Any mobile change**, and **any shared `View` across the two leaves** (§8 ② rendered it - and rejected it). +# Both plans this file has held are finished + +This file has carried two plans. **Neither is outstanding.** It is kept as the record of +what they were and how each was checked, because the second one was audited on +2026-08-17 against the tree rather than against its own account of itself, and that +audit is the useful thing here. + +What is left in both cases is **reader work, not code** — sittings that have to be sat, a +verdict that has to be given on a device. Those live in `docs/validation/ledger.json` +and `docs/issues.json`, which are where somebody should look, not here. + +--- + +## Plan one — the repo's most-repeated claim was false + +**Landed** across `87b4241`, `5676128`, `a97fa98`, `a053bbd`, with two gaps closed later +in `2ec9f4d`. + +Twenty-two times across twenty files this project asserted some version of *there is no +Quran text here*, and two files held running scripture — one of which shipped in the +bundle. Both are gone. The claim is now scoped to what is vendored and shipped, and a +gate refuses the next one. + +| item | state | +| --- | --- | +| ① draw the comparison from the page artwork, delete the typed table | done — `verse-diff.ts` replaces `verse-text.ts`, panel redrawn | +| ② the second site, a pipeline test holding a phrase | done | +| ③ a gate that refuses the next one | done — wired into the composite, the Makefile and CI | +| ④ make the licence map see the whole assets folder | done | +| ⑤ say where the transliteration table came from | done | +| ⑥ the nine prose sites stating the rule unscoped | done | +| registers — issues, map, the licensing design doc | done; the second site is tracked on its own row | + +**Two things the plan promised and nobody had run, since checked and passing:** none of +the deleted table's 36 distinct token strings is byte-present in the built bundle +(strings recovered from git, never typed); and adding an undeclared tree under the +shipped assets folder now fails the licence map by name — tested by creating one, +confirming the failure, removing it. + +**Two gaps the plan did not know it had.** The comparison panel had no row on the code +map — not after the rewrite and not before it, the only trace being a note on the popover +saying it showed "the diff against the current ayah" and pointing at nothing. The map +gate missed it because it only checks pointers in *staged* files, so deleting a mapped +file and adding its replacement passes in silence. Both files have rows now, and the note +on the arithmetic carries *why* the table went, because that is the part a later reader +would otherwise undo: a small text table genuinely does look simpler, and it would bring +back all three defects at once. Separately, a skill heading still asserted the unscoped +claim three lines above quoting the scoped rule that replaced it. + +**One measurement worth not re-taking.** Six of fourteen shipped edge notes carry Arabic, +up to five words in one — more than the plan assumed — but the longest fully-vowelled run +across all of them is **0**. Specimens, not passages, so within the rule as the gate +measures it. + +--- + +## Plan two — the instrument that asks the question has to be trustworthy first + +**Recovered from `git show 83041f3:.claude/plans/shimmering-wandering-lobster.md` and +audited item by item on 2026-08-17. All of it landed.** It should not be restored as a +plan; this table is what was worth keeping. + +Sixteen sittings covering all 1,877 marks the machine could not place from ink were about +to be sat, and an audit of the page doing the asking found three faults that did not make +the sitting slower but made its answers **mean something other than what they say**. The +worst ran in the direction that looks like success: in dark mode the rectangles were drawn +in near-invisible colours on paper that is deliberately never re-themed, and a reader who +cannot see the box affirms it. + +| item | state in the tree | +| --- | --- | +| ① parse the page once per card, not once per pointer frame | done — `mount()`/`paint()` split, `non-scaling-stroke` | +| ② keep the rectangles legible on white paper in both themes | done — four never-re-themed tokens, plus a dash pattern so the distinction survives colour-blindness | +| ③a pin the buttons to the bottom | done — a sticky dock, `viewport-fit=cover`, safe-area padding | +| ③b get the destructive control out of the thumb corner | done | +| ④ make the affirm button look pressed | done | +| ⑤ get the lede off the fold without charging a tap for any answer | done — brief/full toggle keyed off a stored flag | +| ⑥ say which mark, properly | done — the containing word is kept, and the mark's name reaches all three branches | +| ⑦ 44px on the four undersized controls | done | +| ⑧ the scorer was medianing increments and printing zero | done — one row per mark, and the reader's hand and the against-what-ships figures printed as two numbers under two sentences | +| the hazard guard — the whole page is one template literal | done — two assertions that the emitted HTML contains no backtick and no interpolation | +| registers — map rows, the ledger, the issues rows | done | + +**The item the plan called *owed and still unrecorded* is recorded**, and says more than +the plan expected it to. The check stays `pending` on purpose: only 160 of 1,877 fallback +marks have been sat. It reads the two populations apart — sixty marks placed from their +own ink, every one affirmed, which bounds visible error at about 5% rather than at zero +and carries its own caveat that a gross error was structurally impossible on those cards; +against 160 fallback marks of which 158 carry a complaint. Two further instrument defects +were found *while reading that sitting* and fixed. + +**Checked, not assumed:** the ETL suite is 303 tests across 10 files, all passing. + +### What is actually left, and it is not code + +**1,717 of the 1,877 marks have not been sat.** That is the work. The instrument is +trustworthy now, which was the whole point of the plan — it was never the point that the +instrument existed. + +Two verification steps from that plan need a device and a person, and neither can be done +from here: opening a part on the phone over the tailnet in dark mode to confirm by eye +that both boxes are visible on white paper, the buttons do not move between cards, and the +largest card does not stutter under a drag. diff --git a/.claude/plans/sitting-instrument.md b/.claude/plans/sitting-instrument.md new file mode 100644 index 00000000..e87121e7 --- /dev/null +++ b/.claude/plans/sitting-instrument.md @@ -0,0 +1,286 @@ +# The instrument that asks the question has to be trustworthy first + +## Context + +Sixteen sittings are about to be sat. Between them they cover all **1,877** marks the machine +could not place from ink — every one seen, not sampled — at roughly an hour each. That is +sixteen hours of the scarcest thing this project has. + +Before spending it, the page that does the asking was audited against the frontend-design +brief, and three of its findings do not make the sitting *slower*. They make its answers +**mean something other than what they say**: + +1. **In dark mode the rectangles are drawn in near-invisible colours.** The crop's paper is + deliberately never re-themed — a mus'haf page stays on white — but the two rectangle stroke + colours *are* re-themed, and then drawn on that white. Measured: our box 5.05:1 → **2.49:1**, + the reader's box 4.89:1 → **1.70:1**, at a constant 1.64px stroke. A reader who cannot see the + box affirms it. This finding runs in exactly the direction that looks like success. +2. **A destructive control lands where the reader's thumb already is.** The answer list sits + directly above Back/Next, each answer adds a ~28px row, and the "take it back" button in that + row is right-aligned — into the corner Next occupied a moment earlier. Two taps of Next can + retract the answer just given. +3. **Every pointer frame re-parses the page.** The drag handler rebuilds the stage from + `c.svg` — 2.0 / 9.2 / **23.2 KB** of path data (min/median/max) — on every `pointermove`. A + correction that stutters is a correction the reader gives up on and affirms instead. + +Below those, four findings cost time and accuracy rather than truth: the affirm button looks +the same pressed as unpressed; it sits at y≈842 on a 393px phone, below the fold, behind a +~275px lede that never goes away; 91 of 115 cards carry the mark's containing word and throw it +away, while the sentence naming the mark omits the mark's own name and is the dimmest text on +the card; and four **destructive or load-bearing** controls are under 44px. + +**Decided with the reader, 2026-08-14:** the buttons get pinned to the bottom of the screen and +the picture keeps its own shape (nothing ever shrinks); the fixes land **before** part 1 is sat, +which costs nothing because the sixteen parts were re-dealt and **0 of 1,877** have been +answered on the new deal; scope is the three data-corrupting findings plus the four +time-and-accuracy ones. + +**Outcome:** all 1,877 marks are answered on one instrument, and the transcript means what it +says. + +### What the audit explicitly warns against, and why + +- **Do not put the fault buttons behind a disclosure.** Adding a tap to reporting a fault while + affirming stays free biases the exact ratio the sitting exists to measure. Change weight, not + cost. +- **Do not highlight the ligature's own ink in the crop.** Where the ink is, is the unknown + being measured. Also ruled out and not to be revisited here: centring the crop on the mark's + own ink; SVG resize handles on the rectangle. + +--- + +## Also in scope: the scorer is reporting the wrong number + +`score-mark-report.mjs` (~377-387) medians the `by` field across every placement **event**. Two +things are wrong with that, and they were verified against the banked transcript, not argued: + +**It medians increments, and increments cancel.** `by` is always an increment — both the nudge +pad (`flush`) and the drag (`pointerup`) send the burst, and `to` is the running total. Of 179 +consecutive pairs on the same mark, **133** chain exactly; the other 46 are marks where the +reader pressed *put it back where it was*, and a banked transcript has retractions already +applied, so the superseded placements are gone and the chain legitimately restarts. Nothing is +wrong with the field. What is wrong is medianing it: opposite-signed ±0.1 increments cancel, and +one mark with 44 events outvotes 25 marks with one each. The scorer prints **0.000 / 0.000**. + +**And `to` is not the reader's hand.** `to = at + total − box`, so it is measured from the +**shipped** box and already contains the correction the pipeline applied. Three different +numbers, all real, currently collapsed into one wrong one: + +| question | across | down | +|---|---|---| +| what the scorer prints today | 0.000 | 0.000 | +| where the reader put it, against what ships | −3.569 | −3.134 | +| **how far the reader's own hand moved it** | **−2.468** | **−2.010** | + +The middle row is what corroborates the ink measurement: the corpus sets its text lower and +further across than the ornament fit predicts, `dty` negative on **599 of 600** pages. Two +instruments, same direction, same rough size. That is worth printing correctly. + +--- + +## The work + +Fixes are ordered so each lands in one place rather than in a string another fix is about to +rewrite. **Do them in this order.** + +### ① Parse the paths once per card — `build-mark-report.mjs`, `paint()` ~1084-1122 + +Split `paint()` into `mount(c)` and `paint()`. + +- Module-level `let drawnFor = null, hitEl = null, boxEl = null, mineEl = null;` +- `mount(c)` writes `stage.innerHTML` **once per card**: `c.svg` plus the `.hit`, `.grab` and a + hidden reader's rect; caches the three nodes; sets `drawnFor = c.id`. +- `paint()` calls `mount(c)` only when `drawnFor !== c.id`, then `setAttribute`s x/y/width/height + on the cached nodes and toggles the reader's rect's `visibility`. The `viewBox` write stays. +- The `.hit` slack `t = vb[2]/30` stays per-frame — it is unit-based and framing-dependent. +- **Drop `sw = vb[2] / 220`** and use `vector-effect="non-scaling-stroke"` with `stroke-width` in + CSS pixels. That constant is 1.64px *only because* the stage is `width: 100%`; leaving it in + makes stroke width a function of each card's aspect the moment anything sizes the stage + differently. One fewer per-frame attribute write, too. + +This is what `crop()`'s two-viewBox design was always for — the framing toggle becomes one +attribute write instead of a 23 KB reparse — and it has never actually delivered it. + +**Correction to the audit:** pointer capture is on the **stage** (`stage.setPointerCapture`, +~1158), gated by `onRect(p)` (~1141) — not on a rect node. `docs/map.json:752` describes the +gate. So rebuilding mid-gesture never risked capture; do this fix for the parse cost alone. + +### ② Keep the rectangles legible on white paper, in both themes — CSS ~514-525 + +The rule to establish and then never break: **nothing drawn on the paper is themed.** + +- Add to bare `:root` **only**: `--ours-line`, `--ours-wash`, `--yours-line`, `--yours-wash` + (suggested `#c2410c` / `#c2410c1f` and `#15803d` / `#15803d24`). +- Leave `--ours` / `--yours` / `--ours-fill` / `--yours-fill` exactly as they are and keep them + re-themed — they are **chrome** (affirm button, pressed state, focus ring, the answer list, the + bank panel) and sit on `--field`, so they must stay themed. +- `mount()` draws with the four new tokens. Add one line of comment saying they join `--paper` + and `--ink` in the never-re-themed set, and *why*. +- Carry the distinction independently of hue at the same time: + `stroke-dasharray="0.6 0.4"` on the reader's rect. Two rectangles that differ in dash as well + as colour survive deuteranopia, and survive anyone re-theming the palette later. + +Light theme comes out byte-identical. Dark theme returns to 5.05:1 and 4.89:1. + +### ③ Pin the buttons to the bottom — markup ~679-697, CSS + +Two independent halves; **③b must land whichever layout is chosen.** + +**③a — the dock.** Move the affirm row and the Back/Next row into one `
` with +`position: sticky; bottom: 0; background: var(--field); border-top: 1px solid var(--edge); +padding: .5rem 0 calc(.5rem + env(safe-area-inset-bottom));` and bump the main column's bottom +padding. **Sticky, not fixed** — it keeps document flow and does not fight the iOS URL bar. +Requires `viewport-fit=cover` on the viewport meta (~511) or `env()` resolves to zero; that is +also the whole of the safe-area finding. + +The picture keeps its natural per-card shape (295–410px judging, 266–529px identifying, measured +across all 1,877 marks). Nothing shrinks, `ptIn()` is untouched, and on the tallest cards the +reader scrolls the picture behind a dock that never moves. **Explicitly not doing:** normalising +the two viewBoxes to a constant aspect inside `crop()` — it either shrinks the mark or makes +every card as tall as the tallest, and the ring clip would have to be re-ordered after the +normalisation or the new margin comes back empty of the ink that belongs in it. + +**③b — get the destructive control out of the thumb corner.** In the answer list row, move "take +it back" to the **start** of the row (button, then the kind, then the detail) so nothing tappable +sits bottom-right, and give the list at least 44px of bottom margin. **This must ship before ⑦ +enlarges that button to 44px**, or the mis-tap target doubles. + +### ④ Make the affirm button look pressed — CSS ~596 + +`button.affirm[aria-pressed="true"]` gets a solid fill: background `var(--ours)`, text +`var(--field)`, matching border. Inverts legibly against both themes and reads as *on* without a +second glance. Add a check glyph via `::before` on the pressed state only. **Do not change the +label text** — that would move the fold ⑤ just fixed. + +*(The audit's specificity claim was off by one rule: `button.affirm` and the generic pressed rule +are both (0,1,1) and settled by source order; the actual out-specifier is +`button.affirm[aria-pressed="true"]` at (0,2,1). Conclusion unchanged.)* + +### ⑤ Get the lede off the fold without touching the fault buttons — markup ~640-646 + +Split the lede into the full text and a one-line brief, with a quiet toggle between them. The +brief shows once the reader has demonstrably read it: `seen > 0`, persisted through a +`keepRead`/`keptRead` pair written **exactly** like the existing `keepSeen`/`keptSeen` +(~733-736) — try/catch on every call, because a `file:` origin throws. + +Roughly 230px comes off the fold. **No answer costs a tap more than it does today**, so the +affirm/fault ratio the sitting measures is untouched. That constraint is the whole design. + +### ⑥ Say which mark, properly — `identify()` ~1240-1264 + +- The card already carries the containing word and throws it away for the 91 cards that have + both it and a single letter. Keep the big letters as they are, and append the word after them + at ~1rem, dim, `lang="ar" dir="rtl"`. +- Put the mark's **name** into all three branches. It reaches only the `of === 1` branch today. +- The sentence naming the mark is the question, not a footnote: `--dim` → `--text`, + .82rem → .9rem. + +### ⑦ 44px on the four — CSS + +All four are omissions from a convention this file already keeps elsewhere (the note pad and the +chips are already 44/48): + +| control | today | why it matters | +|---|---|---| +| take it back | ~24px | destructive — **land ③b first** | +| the two view toggles | ~31px | pressed constantly, both framings | +| hand over what I have said | ~29px | the reader's safety net | +| put it back where it was | 34px | destructive; also give it `margin-left: auto` so it is not shoulder-to-shoulder with "Finer steps" | + +Land **after** ⑤ and ③, since each of those moves the fold. + +### ⑧ The scorer — `score-mark-report.mjs` ~377-387 + +Do 1-4 **before** 5. Steps 1-4 work on the two transcripts already on disk, which cannot be +re-recorded; once the headline reads `to`, the rename stops being load-bearing. + +1. Collapse to **one row per mark** — walk in order, keep the last placement carrying a `to`. +2. Print the reader's **hand**: `to` minus the displacement already shipped + (`drawnAt(r) − r.box`, both already available). Median/p90/worst of the magnitude, plus signed + per-axis medians. **−2.468 / −2.010** on the banked sitting. +3. Print, under a **separately worded** sentence, where they landed **against what ships** — + median final `to`, **−3.569 / −3.134** — and say in the prose that this one includes the + correction the pipeline already applied, so no reader ever differences the two. +4. Say *n marks*, and say how many events those marks took. 26 marks / 205 events is itself a + finding about the nudge pad. +5. ~~Rename the drag's field.~~ **Dropped.** Both paths genuinely send an increment and `to` is + the running total, so one name is right, and a rename would only make the two transcripts + already on disk unreadable. (The drag path does not need `flush()`'s save-and-restore around + `dropVague` either: it captures the total at ~1190, before, and reassigns at ~1199, after.) + +--- + +## The hazard, and the guard for it + +**The whole page — CSS, markup and JS — lives inside a template literal.** Backticks are +forbidden anywhere in the emitted region *including comments*, and `${` must not appear except +as a deliberate interpolation. This has broken the file three times. + +Add two assertions to `build-mark-report.test.mjs` that say so out loud: the emitted HTML +contains no backtick and no `${`. Two lines, and they document the trap. + +## Tests + +The suite today parses the head and the cards out of the emitted HTML and string-matches the +emitted script. Both styles extend cleanly; there is no visual coverage and none is being +invented here. + +| fix | what a test would actually assert | +|---|---| +| ② **highest value** | the dark block re-themes **none** of the paper, the ink, or the four new rectangle tokens; and both rectangle line colours clear 3.0:1 against the paper. Twelve lines, no browser, and it is precisely the invariant that broke. | +| ① | the paint function contains no `innerHTML`, and `c.svg` appears exactly once in the emitted script, inside `mount`. Coarse, in the same spirit as the existing replay test, and it catches the regression that matters. | +| ⑥ | data, not layout: a single-letter card always carries a longer containing word — this is what makes the fix possible and what would silently stop being true. Plus that the mark's name reaches all three branches. | +| ⑤ | the brief lede exists and the collapse is keyed off the stored flag; a regression deletes the second copy. | +| ⑧ | in `score-mark-report.test.mjs`, which already builds synthetic transcripts and asserts on stdout: one mark nudged +0.5, −0.4, +0.4 (final `to` 0.5) and a second moved once by 0.5 — assert the printed median is **0.5, not 0.4**, and that the line says **2 marks**, not 4 events. That fixture would have caught this. | +| ③④⑦ | not testable here beyond CSS-text presence. Fold position, per-frame cost and thumb geometry need a browser, and there is no in-repo harness for one. | + +## Registers + +- **`docs/map.json:752`** — the `build-mark-report.mjs` note is the canonical home and already + records *why* each control sits where it does, including two prior interaction regressions. + Fixes ①②③ belong there. Hand-edited. +- **`docs/map.json`** — `serve-sittings.mjs` has **no row anywhere in `docs/`**. It needs one; + it is the thing that makes an answer survive the browser losing it. +- **`docs/validation/ledger.json` → `placement-what-kind-of-wrong`** — its 13 runbook steps + **are** the reader's on-screen instructions. ⑤ and ⑥ change what the reader sees; the steps + change with them. Then `pnpm guide`. +- **`docs/issues.json`** — only for the findings that **distorted a measurement**, which is this + repo's convention for a review tool (precedent: the "wrong instrument" paragraph inside + `nobody-has-looked-at-the-placement-verdicts`). That is finding ② and the scorer ⑧ — not the + ergonomics. Then `pnpm issues:doc && pnpm gate:issues`. +- **A design doc is not warranted yet.** `docs/design/encoding-inspector.md` is the template if + it ever is. + +### Owed from the sitting already completed, and still unrecorded + +`make record CHECK=placement-what-kind-of-wrong RESULT='…'` — sixty marks from the placed set, +every one explicitly affirmed, no faults. Bounds visible placement error at about **5%, not at +zero**. The caveat that must survive into the wording: the placed population is defined by a +match of 0.55 or better and a displacement under 3 units on a mark 5.6 × 3.6, so **gross errors +were structurally impossible on those cards**. The check also owes its `tunes` step — a manual +result must tighten something automated, and the gate fails if it tunes nothing. + +## Verification + +1. Rebuild all sixteen and confirm the deal is unchanged — same slice fingerprint, 1,877 marks + across sixteen parts, 0 answered marks re-asked: + `for n in 1..16; node scripts/build-mark-report.mjs --rows out/mark-rows.line-tilt.json + --set fallback --seed 23 --part $n/16 --answered … --out out/sit.fallback-$n-of-16.html` +2. `pnpm --filter @hifth/etl test` — the new assertions above, including the two hazard guards. +3. Re-run the scorer over the two banked transcripts and confirm it prints the hand and the + against-what-ships figures as **two** numbers under two sentences. +4. Open part 1 on the phone over the tailnet, in **dark mode**, and confirm by eye: both boxes + visible on white paper; the buttons do not move between cards; the drag does not stutter on + the largest card; affirm is above the fold and obviously pressed when pressed. +5. `pnpm issues:doc && pnpm gate:issues`; `pnpm guide` after the ledger edit; + `pnpm gate:validation`. +6. `git add -A && make ci`. Commit code and docs **separately**. + +## Operational + +- `cd /Users/omareid/Workspace/git/hifth && ./scripts/with-lock.sh
diff --git a/apps/web/src/components/PageStage.budget.test.tsx b/apps/web/src/components/PageStage.budget.test.tsx index ca0ae827..b99836b6 100644 --- a/apps/web/src/components/PageStage.budget.test.tsx +++ b/apps/web/src/components/PageStage.budget.test.tsx @@ -11,6 +11,7 @@ * an `aria-labelledby="page-label-"`, set in `ensurePage`. That is the DOM * cost the cap exists to bound — one inline mus'haf page is ~150 KB of paths. */ +import { StrictMode } from "react"; import { describe, it, expect, vi, beforeAll, afterAll } from "vitest"; import { render, waitFor } from "@testing-library/react"; import { MOUNTED_PAGE_CAP, Resolver, spreadBudget, type AssetManifest } from "@hifth/core"; @@ -129,4 +130,30 @@ describe("the stage's DOM budget", () => { }); expect(spreadBudget().facing).toBeLessThan(MOUNTED_PAGE_CAP); }); + + /* + * A leaf that magnifies while the one beside it stays put — the desktop + * spread's "zoom is broken" — is this defect wearing a costume. React's dev + * StrictMode mounts, tears down, and remounts against the *same* layer; the + * teardown has to remove each host from that layer, not only forget it in the + * Map. A host left behind is a second
for one page, outside the Map: a + * duplicated `page-label-N` landmark, and — because zoom transforms only the + * Map's host — an un-magnified twin sitting under the one that grew. It never + * reached production (StrictMode is a dev-only wrapper) but it is exactly what + * a reader running the dev server saw, so the guard belongs where that wrapper + * is real. One host per page is the whole invariant. + */ + it("mounts one host per page under StrictMode's double-invoke, not a hidden twin", async () => { + const { container } = render({stage([PAGES[0]!])}); + await waitFor(() => expect(mountedIn(container)).toContain(PAGES[0])); + // Both halves of the double-invoke have to settle before the count means + // anything: the twin is appended by the *second* mount, one microtask behind. + await waitFor(() => { + const mounted = mountedIn(container); + expect(new Set(mounted).size, "a page is mounted twice — the zoom-orphan twin").toBe( + mounted.length, + ); + expect(mounted).toEqual([PAGES[0]]); + }); + }); }); diff --git a/apps/web/src/components/PageStage.settle.test.ts b/apps/web/src/components/PageStage.settle.test.ts new file mode 100644 index 00000000..d3746cf7 --- /dev/null +++ b/apps/web/src/components/PageStage.settle.test.ts @@ -0,0 +1,34 @@ +/** + * Every road onto a page settles through one step — kept by counting. + * + * The desktop thread that produced this fixed four leaf-placement defects one + * road at a time (a juz jump, a turn's landing, a tap, a zoom), each a copy of + * the same three lines that one road had dropped or reordered. The fix that + * reaches the *next* road is structural: `arrive` in PageStage is the only + * place a road may reveal-and-centre, and `centerCurrent` may be called from + * exactly two places — `arrive`, and `crossFade`, which has to reveal the + * incoming leaf itself so both leaves can fade. + * + * This reads the source rather than rendering it because the claim is about + * the source: a fifth road that centres by hand would render correctly today + * and be the next defect found by eye. The behavioural half of the same claim + * is the desktop e2e "every road onto a page lands the leaves level". + */ +import { describe, it, expect } from "vitest"; +import src from "./PageStage.tsx?raw"; + +describe("PageStage · one settle step", () => { + it("centres a leaf from arrive and from the fade under a turn's band, nowhere else", () => { + const calls = src.match(/\bcenterCurrent\(\);/g) ?? []; + expect( + calls, + "a road centred the leaf by hand — route it through arrive(next) instead", + ).toHaveLength(2); + }); + + it("brings every road onto a page through arrive", () => { + // The four roads: a turn's landing, a hop, a deep link, the cold mount. + const roads = src.match(/\barrive\((next|loc\.page|page)\);/g) ?? []; + expect(roads, "a road onto a page skipped the settle step").toHaveLength(4); + }); +}); diff --git a/apps/web/src/components/PageStage.tsx b/apps/web/src/components/PageStage.tsx index ed5f8b68..b1214e09 100644 --- a/apps/web/src/components/PageStage.tsx +++ b/apps/web/src/components/PageStage.tsx @@ -205,6 +205,21 @@ interface PageStageProps { foldTarget?: RefObject | null; } +/** + * Which point a button-driven zoom holds still as the paper grows. + * + * `"center"` is the lone-leaf answer — a single page has no reason to grow to + * one side. `"left"` and `"right"` name a *visual* edge of the leaf's box (not a + * reading side), and they are the spread answer: the leaf grows *away* from the + * fold. Pin each leaf at the edge its gutter is on — the right-hand leaf at its + * left edge, the left-hand leaf at its right edge — and the two stay joined at + * the seam and open outward together like one sheet, instead of each swelling + * from its own middle, crushing the fold while their outer margins run off the + * screen. A stage does not know whether it is the live or the facing leaf, so + * the caller — which does — names the edge. + */ +export type ZoomAnchor = "center" | "left" | "right"; + /** What App can drive imperatively on the stage. */ export interface PageStageHandle { /** Pan/zoom to an ayah, mounting its page if needed. Resolves when landed. */ @@ -236,13 +251,15 @@ export interface PageStageHandle { */ turnTo: (page: number) => Promise; /** - * Magnify to `z`, anchored at the middle of the stage, and answer with what - * was actually applied. + * Magnify to `z`, anchored at `anchor` (default the middle of the stage), and + * answer with what was actually applied. * * Anchored at the middle because a button has no pointer to zoom about: the * wheel could keep the paper under the cursor still, and a control in the * chrome is nowhere near the paper at all. The middle is the only point the - * reader can predict. + * reader can predict — for a lone leaf. With the book open the predictable + * point is the fold, not each leaf's own centre, so the caller pins each leaf + * at its gutter edge (`ZoomAnchor`) and the opening grows outward as one. * * It **returns** rather than reports through a callback, and the returned * number is the one the caller must store. `clampView` runs inside the same @@ -251,7 +268,7 @@ export interface PageStageHandle { * the first press against a limit, and the readout would then be describing a * magnification nobody is looking at. */ - setZoom: (z: number) => number; + setZoom: (z: number, anchor?: ZoomAnchor) => number; /** * The magnification the paper is at right now. * @@ -267,6 +284,46 @@ export interface PageStageHandle { * magnification nobody is looking at. */ zoomNow: () => number; + /** + * Turn the page from a grab that this stage's own gesture cannot feel. + * + * On a desktop spread the reader turns by grabbing the book's outer edge — + * and one of those two edges is the *facing* leaf, a page this stage does not + * own and never receives a pointer from. So the grab lives on the book and + * drives the one stage that owns turning through these three verbs, one per + * phase of the drag. + * + * The grab is a **trigger, not a track**. It deliberately does not run the + * swipe path's finger-locked band across the spread: that band is a thin + * fore-edge strip built to *sweep* a leaf in a couple of hundred milliseconds, + * and dragging it slowly instead — 1:1 with a hand at the book's edge — draws + * a bar creeping over a spread whose two pages have not changed yet, which + * reads as a stuck line on one page rather than a page turning (see the + * desktop-turn decision, and #11: a faithful drag-to-peel that reveals the + * destination *opening* has to drive both leaves at once, which is a larger + * rework). Until that lands, the grab measures the drag and, on release, hands + * a committed turn to the ordinary animated turn — the same flip a wheel or an + * arrow key plays — so what the reader sees is a real transition, never a + * half-dragged band. + * + * `step` is +1 forward / −1 back, and it is fixed by *which edge* was grabbed, + * not by the drag: the outer edge of the later leaf pulls forward, the outer + * edge of the earlier leaf pulls back. The drag then only decides whether that + * latched turn commits or is dropped — a grab dragged the wrong way, or not + * far enough, is a reader changing their mind, and the commit rule takes it + * back by simply doing nothing. + */ + beginEdgeTurn: (step: 1 | -1) => void; + /** + * A frame of the grab moved. The drag's distance is what the commit rule reads + * on release, so nothing has to be done per-frame yet — the method is kept so + * the rails have one verb per phase and so a later peel can hang tracking here + * without the rails changing. The cursor's grabbing state is the drag's only + * feedback for now. + */ + trackEdgeTurn: (dx: number) => void; + /** The grab came up: if the drag committed, play the turn; otherwise nothing. */ + releaseEdgeTurn: (dx: number, velocityX: number) => void; } /** The three states of a fold that is actually drawn; `"none"` draws nothing. */ @@ -452,6 +509,14 @@ export const PageStage = forwardRef(function Pa const turnRef = useRef(0); const foldTargetRef = useRef(foldTarget); foldTargetRef.current = foldTarget; + /* + * The step an edge grab latched, held between the grab going down and coming + * up. The desktop edge grab is a trigger, not a tracked band (see the edge + * verbs on the handle): begin latches which way the grabbed edge turns, and + * release asks the commit rule whether the drag earned it. Null whenever no + * edge is being held. + */ + const edgeStepRef = useRef<1 | -1 | null>(null); /* * The turn the finger is currently holding open (`page-transition.md` §3.2, * `tracking`). Null whenever no drag has latched `"turn"`. @@ -809,6 +874,14 @@ export const PageStage = forwardRef(function Pa } catch { return null; // unvendored / fetch failed — caller degrades gracefully } + // A rival mount may have landed this same page while our fetch was in + // flight — the cold-link race the comment in `ensurePage` describes, but + // also StrictMode's remount after it cleared the pending map. Everything + // below is synchronous, so re-reading the Map here (after the last await) + // and yielding to the winner is airtight: the caller that sets the Map + // first runs straight through to its `set` before this line runs again. + const landed = pagesRef.current.get(targetPage); + if (landed) return landed; const host = document.createElement("div"); host.className = styles.host ?? ""; // Which of this leaf's edges is free. Per *mounted page*, not per visible @@ -892,6 +965,38 @@ export const PageStage = forwardRef(function Pa applyTransform(); }, [applyTransform, measureFit]); + /** + * The one settle step every road onto a page ends with. + * + * Four roads bring a leaf onto the stage — a cold open, a deep link + * (`showPage`), a hop (`navigateTo`) and a turn's landing (`land`) — and each + * of them used to re-derive the same three lines by hand: stop whatever tween + * is still writing the view, reveal the page, put it at fit at the layer + * origin. Four defects in one desktop thread were each *one road* with the + * step missing or mis-ordered: a juz jump whose incoming leaf flashed a + * centring-offset high, the same flash on a turn's landing, a tap that shifted + * the page, a zoom that landed off-centre — each found by eye, each fixed on + * its own road, none of the fixes reaching the next road. A rule kept in four + * places is kept in three, so it is kept here once. `crossFade` is the fifth + * road and cannot come through here (it reveals the incoming leaf itself, + * because both leaves must be visible for the fade), so it calls + * `centerCurrent` directly — which is the only other place that may. + * `PageStage.settle.test.ts` counts, and the desktop e2e "every road onto a + * page lands the leaves level" drives each road. + * + * `navigatedRef` is claimed here too: a page has arrived, whichever road + * brought it, and the cold-mount effect must not re-centre over it. + */ + const arrive = useCallback( + (next: number): void => { + navigatedRef.current = true; + cancelTween(); + setCurrentPage(next); + centerCurrent(); + }, + [cancelTween, centerCurrent, setCurrentPage], + ); + /** * Swap the two pages *under* the band, with the fade's midpoint at its centre. * @@ -927,9 +1032,7 @@ export const PageStage = forwardRef(function Pa // paints for one frame at the layer's origin — unclamped, top-left — and // the fade shows a page sliding into place under the band. currentPageRef.current = to; - view.current = { x: 0, y: 0, z: 1 }; - measureFit(); - applyTransform(); + centerCurrent(); // Flush, so the transition has a start value to run from instead of // coalescing both writes into one. void incoming.host.offsetWidth; @@ -940,24 +1043,21 @@ export const PageStage = forwardRef(function Pa outgoing.host.style.opacity = "0"; } }, - [applyTransform, measureFit], + [centerCurrent], ); /** End a turn on the destination page: display swapped, inline fades cleared. */ const land = useCallback( (next: number): void => { - navigatedRef.current = true; - cancelTween(); - setCurrentPage(next); + arrive(next); for (const mp of pagesRef.current.values()) { mp.host.style.transition = ""; mp.host.style.opacity = ""; } - centerCurrent(); setErrorPage(null); setStatus("ready"); }, - [cancelTween, centerCurrent, setCurrentPage], + [arrive], ); /** @@ -1263,7 +1363,7 @@ export const PageStage = forwardRef(function Pa useImperativeHandle( ref, (): PageStageHandle => ({ - setZoom(z) { + setZoom(z, anchor = "center") { const layer = layerRef.current; if (!layer) return view.current.z; // The same two things a wheel gesture used to do at its first event: @@ -1274,15 +1374,16 @@ export const PageStage = forwardRef(function Pa measureFit(); const rect = layer.getBoundingClientRect(); const base = { z: view.current.z, x: view.current.x, y: view.current.y }; - // The centre of the stage, because a button has no pointer to anchor to. - // Through `zoomAbout` rather than writing `view` directly: the anchor - // arithmetic §7 ⑨ fixed has one implementation and this is not a second. - zoomAbout( - clampZoom(z, MIN_ZOOM, MAX_ZOOM), - rect.left + rect.width / 2, - rect.top + rect.height / 2, - base, - ); + // A button has no pointer to anchor to, so the caller names the point. + // The lone leaf grows from its own centre; an open book pins each leaf at + // its gutter edge (`left`/`right`) so the fold stays put and the opening + // grows outward as one sheet. Vertically always the middle — the fold is + // a vertical line, so height has no side to prefer. Through `zoomAbout` + // rather than writing `view` directly: the anchor arithmetic §7 ⑨ fixed + // has one implementation and this is not a second. + const ox = + anchor === "left" ? rect.left : anchor === "right" ? rect.right : rect.left + rect.width / 2; + zoomAbout(clampZoom(z, MIN_ZOOM, MAX_ZOOM), ox, rect.top + rect.height / 2, base); // The rail sits beside the selected ayah and has just been moved. emitSelectionRect(); // What was *applied*, which is not always what was asked: `clampView` @@ -1312,10 +1413,26 @@ export const PageStage = forwardRef(function Pa return; // no ghost page } setStatus("ready"); - // Claimed only once the target really mounted: a navigateTo that fails + // Arrive only once the target really mounted: a navigateTo that fails // above must leave the initial page in charge rather than blank the stage. - navigatedRef.current = true; - if (loc.page !== currentPageRef.current) setCurrentPage(loc.page); + // + // And arrive unconditionally, not `if (loc.page !== currentPageRef.current)`: + // on a cold open the stage mounts already numbered for its page, so a deep + // link to *that same* page finds `currentPageRef` matching and a guard + // would skip the reveal — but the host `ensurePage` just built is hidden + // (§ host default `display:none`), and the mount effect's own reveal is + // gated on `!navigatedRef`, which arriving falsifies. The two reveal paths + // then race to zero: neither fires and the leaf stays blank. Arriving is + // idempotent, so doing it every time is free when the page was already + // shown and correct when it was not. + // + // Centred *before* the tween below has a chance to yield: a freshly + // mounted host wears no transform, so it sits at the layer origin — + // top-left, one centring-offset high — and the framing below is an + // `await tweenTo` whose first positioned frame is one paint away. A + // zoom = 1 jump (a juz or page hop) then simply rests at this centre; a + // closer hop tweens out from fit. + arrive(loc.page); const bbox = mp.hl.bboxOf(loc.elementIds); const fit = measureFit(); if (bbox && fit) { @@ -1340,14 +1457,45 @@ export const PageStage = forwardRef(function Pa return; } setStatus("ready"); - navigatedRef.current = true; - if (next !== currentPageRef.current) setCurrentPage(next); - cancelTween(); - centerCurrent(); + // Always arrive — see `navigateTo` above for why a `next !== + // currentPageRef.current` guard blanked a cold-opened page link (`p7`) + // whose stage had mounted already numbered for 7. Idempotent when already on. + arrive(next); }, turnTo(next) { return runTurn(next); }, + beginEdgeTurn(step) { + // A grab does not measure the page on a first drag frame the way the + // gesture does, so do it here: the commit rule at release reads the + // stage width off `fitRef`, and a spread toggle can have changed it + // since the last resize without touching this surface. + measureFit(); + edgeStepRef.current = step; + }, + trackEdgeTurn() { + // Nothing per-frame: release reads the drag distance, and the grabbing + // cursor is the only feedback the trigger owes a slow pull. A future + // peel (#11) hangs here. + }, + releaseEdgeTurn(dx, velocityX) { + const step = edgeStepRef.current; + edgeStepRef.current = null; + if (step === null) return; + // The same commit rule the swipe and the wheel use, so a grab, a flick + // and a spun wheel all agree on what counts as a turn. A drag that did + // not reach the threshold — or reversed past it — leaves `verdict` off + // `step`, and the honest answer to "did they turn the page?" is no: + // nothing animates, the reader stays where they were. + const stageWidth = fitRef.current?.stageWidth ?? stageRef.current?.clientWidth ?? 0; + const verdict = turnCommit({ dx, velocityX, stageWidth }); + if (verdict !== step) return; + // Hand the committed turn to the ordinary animated turn — the same flip + // a wheel or arrow key plays (App → turnTo → runTurn). No band was ever + // drawn during the drag, so this is the reader's first and only sight of + // the turn, and it is a real one. + onTurnRef.current?.(step); + }, }), [ resolver, @@ -1378,10 +1526,7 @@ export const PageStage = forwardRef(function Pa // A deep link may have navigated while this fetch was in flight. Showing // `page` now would hide the page the reader actually asked for, and // re-centering would throw away the hop's framing. - if (!navigatedRef.current) { - setCurrentPage(page); - centerCurrent(); - } + if (!navigatedRef.current) arrive(page); setStatus("ready"); }) .catch(() => { @@ -1404,7 +1549,17 @@ export const PageStage = forwardRef(function Pa // Bumping the generation is what makes those wake-ups no-ops rather than // writes to detached hosts. turnRef.current += 1; - for (const mp of pages.values()) mp.hl.destroy(); + // Remove each host from the layer, not only from the Map. On a real + // unmount the layer goes with the component, so clearing the Map would be + // enough — but StrictMode's dev double-invoke tears down and *reuses the + // same layer*, and a host left in that layer becomes an orphan the rebuilt + // Map no longer holds. Two overlapping copies of one page result, and zoom + // (which transforms only the Map's host) grows one and leaves the other — + // the "one leaf zooms, one stays" a reader sees in `pnpm dev`. + for (const mp of pages.values()) { + mp.hl.destroy(); + mp.host.remove(); + } pages.clear(); lru.length = 0; // Drop in-flight mounts too: their hosts are appended to a layer that is diff --git a/apps/web/src/components/RevisionMap.module.css b/apps/web/src/components/RevisionMap.module.css index 832dcf65..e245d1d1 100644 --- a/apps/web/src/components/RevisionMap.module.css +++ b/apps/web/src/components/RevisionMap.module.css @@ -145,16 +145,23 @@ margin: var(--space-4) 0 0; padding: 0; list-style: none; - --cell: 26px; + --cell: 30px; + --num-size: 12px; } .grid[data-scope="juz"] { - --cell: 40px; + --cell: 44px; + --num-size: 15px; } +/* Page scope carries a three-digit number (up to 604) and once could not: the + cells were 16px, too small for anything but a colour. They are larger now so + the number fits — the map trades a little of its all-at-once density for cells + a reader can name without counting from a corner. Still the densest scope. */ .grid[data-scope="page"] { - --cell: 16px; - gap: 2px; + --cell: 28px; + --num-size: 10px; + gap: 3px; } /* @@ -181,6 +188,8 @@ * fixed size below, and absent cells are grid items where it is a no-op. */ .cell { + display: grid; + place-items: center; inline-size: 100%; aspect-ratio: 1; padding: 0; @@ -188,11 +197,32 @@ border-radius: var(--radius-sm); background: var(--paper-sunk); font: inherit; + overflow: hidden; } -/* Pressable, and it says so. The cursor is the only affordance a cell this small - can carry — there is no room for a label, and a hover fill would collide with - the warmth ramp, which is the one thing the square is drawn to say. */ +/* + * The division's own number, centred in the cell. + * + * The cells used to be too small to carry a label — the cursor was the only + * affordance a 16px square could hold. The cells are larger now precisely so + * the number fits, because a grid of identical squares makes the reader count + * from a corner to name the one they mean, and the number is the thing that + * ends the counting. It scales with the scope: roomy at juz, tight at page. + * + * `line-height: 1` and `tabular-nums` so a three-digit page number and a + * one-digit one sit the same, and the glyph never pushes the cell's box around. + */ +.num { + font-size: var(--num-size, 10px); + line-height: 1; + font-variant-numeric: tabular-nums; + color: var(--ink-soft); + pointer-events: none; +} + +/* Pressable, and it says so. A hover fill would collide with the warmth ramp — + the one thing the square is drawn to say — so the cursor and the number carry + the affordance instead. */ button.cell { cursor: pointer; } @@ -243,6 +273,27 @@ button.cell { background: var(--accent-strong); } +/* + * The number's ink, per background, so it clears contrast on every square it can + * land on. The three lighter greens and the sunk paper take the ordinary dark + * ink; only the deepest band is dark enough to need a light glyph. An absent + * cell has no paper behind it, so its number is the faint ink that says so — the + * same "outline, not a neglected square" distinction the fill draws. + */ +.cell[data-warmth="1"] .num, +.cell[data-warmth="2"] .num, +.cell[data-warmth="3"] .num { + color: var(--ink); +} + +.cell[data-warmth="4"] .num { + color: var(--paper-raised); +} + +.cell[data-state="absent"] .num { + color: var(--ink-faint); +} + /* Where the stage is. A ring, not a fill — the cell still has to say its own state, and "here" is a second fact about it rather than a replacement one. */ .cell[data-here] { @@ -306,12 +357,16 @@ button.cell { * book you are, which is the only thing this sheet is opened to see. * * So where the phone lets the window decide the shape, a wide window makes the - * map decide it itself: a named column count per scope, near √N, so the picture - * is about as tall as it is wide at every scope — 6×5 juz, 8×8 hizb, 25×25 page. - * The cells take a fixed size rather than `1fr` because `1fr` at juz scope would - * inflate them to 100px squares; the grid is centred in the card instead, and - * the card holds one width across all three scopes so switching scope redraws - * the map without moving the sheet under the cursor. + * map decide it itself: a named column count per scope, so the picture is about + * as tall as it is wide — 6×5 juz, 8×8 hizb — while page scope trades a little + * squareness for cells wide enough to hold their own number (21 across, ~29 + * down). The count is also bounded from below: fewer columns would stack 604 + * pages too deep to read at a glance, the opposite of the strip this rule fixes, + * so `desktop.spec.ts` guards both walls. The cells take a fixed size rather + * than `1fr` because `1fr` at juz + * scope would inflate them to 100px squares; the grid is centred in the card + * instead, and the card holds one width across all three scopes so switching + * scope redraws the map without moving the sheet under the cursor. */ @media (min-width: 900px) { .sheet { @@ -333,7 +388,7 @@ button.cell { } .grid[data-scope="page"] { - grid-template-columns: repeat(25, 20px); + grid-template-columns: repeat(21, 25px); } } diff --git a/apps/web/src/components/RevisionMap.test.tsx b/apps/web/src/components/RevisionMap.test.tsx index 5b3b071a..ea0e2ea9 100644 --- a/apps/web/src/components/RevisionMap.test.tsx +++ b/apps/web/src/components/RevisionMap.test.tsx @@ -144,6 +144,76 @@ describe("RevisionMap", () => { expect(hizb2.getAttribute("aria-label")).toBe("الحزب ٢ · غير متوفّر في هذه النسخة"); }); + it("keys absent on a trimmed build, and drops the row when nothing is absent", async () => { + // The legend is a key to what is drawn, and "no paper here" is a treatment a + // full edition's picture never contains. On the trimmed fixture hizb 2 is + // absent, so the key earns its row; on a build that holds its whole span the + // row would be naming a state the reader cannot find on the grid, and the one + // thing this component refuses is a picture claiming more than the record. + const trimmed = render( + {}} + pages={[PAGE_7]} + edition={EDITION} + totalPages={604} + page={7} + onGoToPage={() => {}} + today={TODAY} + />, + ); + await waitFor(() => { + if (trimmed.container.querySelectorAll("[data-state]").length === 0) + throw new Error("not drawn yet"); + }); + expect(trimmed.queryByText("غير متوفّر في هذه النسخة")).toBeTruthy(); + trimmed.unmount(); + + // A whole edition of one page: page scope, one page held, one cell, span one, + // so `held.size` meets the span and nothing on the grid is absent. + const whole = render( + {}} + openAt="page" + pages={[page(1, 1, 1, 7)]} + edition={EDITION} + totalPages={1} + page={1} + onGoToPage={() => {}} + today={TODAY} + />, + ); + await waitFor(() => { + if (whole.container.querySelectorAll("[data-state]").length === 0) + throw new Error("not drawn yet"); + }); + expect(whole.queryByText("غير متوفّر في هذه النسخة")).toBeNull(); + }); + + it("draws each cell's number, absent ones included, in the scope's own digits", async () => { + // The number is what ends the counting-from-a-corner a grid of identical + // squares forces. A hizb number takes the Arabic-Indic digits of the Arabic + // chrome; an absent cell shows its number too, so an unheld division can + // still be found by eye. The glyph is `aria-hidden` — the cell's aria-label + // already says the number in a sentence — so it is read off `textContent`. + draw(); + const grid = await cells(); + expect(grid[0]!.textContent).toBe("١"); // hizb 1, present + expect(grid[1]!.textContent).toBe("٢"); // hizb 2, absent, still numbered + expect(grid[11]!.textContent).toBe("١٢"); // two Arabic-Indic digits + }); + + it("numbers page-scope cells in Latin, the way a page number is read off the corner", async () => { + // `pageN`'s rule, applied to the cell: a page number is Latin in both + // languages because it is read off the printed page's corner, unlike a hizb + // or juz number which follows the chrome. Page 7 is the one the fixture holds. + draw({ openAt: "page" }); + const grid = await cells(); + expect(grid[6]!.getAttribute("data-state")).toBe("cold"); + expect(grid[6]!.textContent).toBe("7"); + }); + it("counts the inventory, not the book", async () => { // PageSlider's precedent, one division coarser: the grid spans the print and // the count says how much of it is actually here. @@ -166,7 +236,7 @@ describe("RevisionMap", () => { // is visibly a *young* record. await recordLook({ key: `quran/${EDITION}/2:30`, page: 7 }, at("2026-03-18T12:00:00Z")); draw(); - expect(await screen.findByText("يُسجَّل منذ ٢٠٢٦-٠٣-١٨")).toBeTruthy(); + expect(await screen.findByText("نشِط منذ ١٨ مارس ٢٠٢٦")).toBeTruthy(); }); it("warms a division by when it was last opened, and says the number out loud", async () => { @@ -182,6 +252,39 @@ describe("RevisionMap", () => { expect(grid[0]!.getAttribute("aria-label")).toBe("الحزب ١ · فُتح قبل ٢ يومًا"); }); + it("scopes the colouring to a calendar month, and cools what falls outside it", async () => { + // A look last month — TODAY is the 20th of March, so this lands in February. + // Under "all time" it warms hizb 1; under "this month" nothing in March + // opened it, so it cools; under "last month" it warms again. The squares do + // not move — only which looks the warmth is drawn from. + await recordLook({ key: `quran/${EDITION}/2:30`, page: 7 }, at("2026-02-10T12:00:00Z")); + draw(); + const grid = await cells(); + await waitFor(() => { + expect(grid[0]!.getAttribute("data-state")).toBe("seen"); + }); + + fireEvent.click(screen.getByRole("radio", { name: "هذا الشهر" })); + await waitFor(() => { + expect(grid[0]!.getAttribute("data-state")).toBe("cold"); + }); + // The line no longer dates the record — it names the window the picture is of. + expect(screen.getByText("نشاط مارس ٢٠٢٦")).toBeTruthy(); + + fireEvent.click(screen.getByRole("radio", { name: "الشهر الماضي" })); + await waitFor(() => { + expect(grid[0]!.getAttribute("data-state")).toBe("seen"); + }); + expect(screen.getByText("نشاط فبراير ٢٠٢٦")).toBeTruthy(); + + // Back to the whole record, and the line dates the record again. + fireEvent.click(screen.getByRole("radio", { name: "كل الوقت" })); + await waitFor(() => { + expect(grid[0]!.getAttribute("data-state")).toBe("seen"); + }); + expect(screen.getByText(/^نشِط منذ/)).toBeTruthy(); + }); + it("marks where the reader is standing without overwriting what the cell says", async () => { draw(); const grid = await cells(); diff --git a/apps/web/src/components/RevisionMap.tsx b/apps/web/src/components/RevisionMap.tsx index ab053083..aac27131 100644 --- a/apps/web/src/components/RevisionMap.tsx +++ b/apps/web/src/components/RevisionMap.tsx @@ -5,6 +5,7 @@ import { daysBetween, lastSeen, scopesOf, + type DayStamp, type EditionId, type PageMeta, type RevisionEvent, @@ -107,6 +108,33 @@ function warmth(days: number): 1 | 2 | 3 | 4 { return 1; } +/** + * How much of the record the colouring is drawn from. + * + * The whole record by default; a single calendar month otherwise. A reader who + * keeps up a broad revision reads a warm map every day and cannot tell this + * week's work from last quarter's — so the month scopes let them ask "what did I + * touch *this* month" and see only that, everything else falling to cold. It is + * a filter on which looks count, not on the warmth ramp: a division opened + * inside the window is still coloured by how many days ago that was, so "opened + * two days ago" reads the same whichever range is chosen. + */ +type MapRange = "all" | "thisMonth" | "lastMonth"; + +/** The `YYYY-MM` a day stamp falls in — its bytes, no clock consulted. */ +function monthKey(day: DayStamp): string { + return day.slice(0, 7); +} + +/** The `YYYY-MM` of the calendar month before the one `day` falls in. */ +function prevMonthKey(day: DayStamp): string { + const year = Number(day.slice(0, 4)); + const month = Number(day.slice(5, 7)); + const py = month === 1 ? year - 1 : year; + const pm = month === 1 ? 12 : month - 1; + return `${String(py).padStart(4, "0")}-${String(pm).padStart(2, "0")}`; +} + /** * Focusable descendants of `root`, in tab order. Colophon's helper, with one * clause added — and the clause is the point. @@ -249,12 +277,13 @@ export function holdings( * answer to the window and nothing in the DOM has it. A stride guessed from the * scope would be right on a desktop and quietly wrong on every phone. * - * The cells are small — 16px at page scope — which is under WCAG 2.5.8's 24px - * and is not fixable here: a map of 604 pages drawn at 24px is no longer a map - * you can see at once, which is the only reason to open it. The criterion's - * *equivalent control* exception is what carries it, and the equivalent is real - * rather than argued: the jumper in the chrome goes to any page in the print, - * with a text field. Hizb (26px) and juz (40px) clear the threshold outright. + * Every cell now carries its own number, so a reader names the division they + * mean instead of counting from a corner — and the page cells grew to 28px to + * hold three digits, which as a side effect brings them over WCAG 2.5.8's 24px + * target size, where at 16px they had leaned on the criterion's *equivalent + * control* exception (the jumper's text field). Hizb (30px) and juz (44px) clear + * it with room to spare. The map is a little less dense for it and still shows + * the whole book at a glance, which is the only reason to open it. */ export function RevisionMap({ open, @@ -272,6 +301,11 @@ export function RevisionMap({ const gridRef = useRef(null); const restoreRef = useRef(null); const [scope, setScope] = useState("hizb"); + // Which slice of the record the colouring is drawn from. The whole record by + // default; a single month otherwise. Independent of `scope` — a reader may ask + // "which juz this month" — so it has its own state and clears no cursor: the + // squares do not move, only their warmth. + const [range, setRange] = useState("all"); // Which cell holds the grid's single tab stop, or `null` for "wherever the // reader is standing" — resolved at render, because `here` is not known until // the pages and the scope are both in hand. Cleared on a scope change: hizb 47 @@ -383,10 +417,25 @@ export function RevisionMap({ // untouched, and that is not an oversight: those divisions are the same in // every print, so filtering them would drop looks that really do belong to the // square. The asymmetry lives in core (`revision.ts`), not here. - const seen = useMemo( - () => lastSeen(comparableEvents(record?.events ?? [], scope, edition), scope), - [record, scope, edition], - ); + // The month the range is scoped to, or null for the whole record. Read off + // `asOf` — the day the sheet resolved — so "this month" is this month in the + // reader's own clock, the same clock the record's days were stamped in. + const rangeMonth = useMemo(() => { + if (range === "all" || asOf === null) return null; + return range === "thisMonth" ? monthKey(asOf) : prevMonthKey(asOf); + }, [range, asOf]); + + const seen = useMemo(() => { + const all = record?.events ?? []; + // Filter by the calendar month first, then by edition through + // `comparableEvents`: the month is a fact about the reader's clock and the + // edition filter a fact about the paper, and neither belongs inside the + // other. A look outside the month never reaches `lastSeen`, so its division + // is cold unless another look inside the month warmed it. + const within = + rangeMonth === null ? all : all.filter((e) => dayOf(e).startsWith(rangeMonth)); + return lastSeen(comparableEvents(within, scope, edition), scope); + }, [record, scope, edition, rangeMonth]); // The division the stage is currently showing, so the reader can find // themselves on a grid of sixty identical squares. const here = useMemo(() => { @@ -540,6 +589,28 @@ export function RevisionMap({ ))}
+ {/* The time-range switch, the scope switch's twin: which slice of the + record the colouring is drawn from. No cursor is cleared — the + squares stay where they are, only their warmth changes. */} +
+ {(["all", "thisMonth", "lastMonth"] as const).map((r) => ( + + ))} +
+ {!revisionStoreSupported() ? (

{t.mapNoStore}

) : record === undefined ? ( @@ -580,10 +651,27 @@ export function RevisionMap({ ? t.mapCellSeen(label(id), days) : t.mapCellNever(label(id)), }; + // The division's own number, drawn in the cell. A page number is + // Latin in both languages — it is read off the corner of the + // printed page, the same rule `pageN` follows — while a hizb or + // juz number takes the language's digits. `aria-hidden`: the + // cell's `aria-label` already says the number in a full sentence, + // so the glyph is for the eye only and must not be read twice. + const num = ( + + ); // No paper behind it, so nothing to press — and no button, which // is the whole distinction: a control that refuses is worse than - // an outline that never offered. - if (at === undefined) return
  • ; + // an outline that never offered. It still shows its number, in a + // fainter ink, so an absent page can be found by eye. + if (at === undefined) + return ( +
  • + {num} +
  • + ); return (
  • ); })} @@ -605,10 +695,18 @@ export function RevisionMap({

    {t.mapHeld(held.size, span, scope)}

    - {/* Always shown, empty record or not. This is the line that keeps an - ITP wipe from reading as "you have revised nothing": a record - dated this morning is visibly a new one. */} - {record.since &&

    {t.mapSince(record.since)}

    } + {/* Under "all time" this dates the record — the line that keeps an + ITP wipe from reading as "you have revised nothing", since a + record dated this morning is visibly a new one. Under a month it + dates the *window* instead: the picture is no longer the whole + record, so saying when the record began would misname what is on + screen. */} + {rangeMonth === null ? ( + record.since &&

    {t.mapSince(record.since)}

    + ) : ( +

    + {t.mapActiveIn(Number(rangeMonth.slice(0, 4)), Number(rangeMonth.slice(5, 7)))} +

    + )} )} diff --git a/apps/web/src/components/TrailBeads.module.css b/apps/web/src/components/TrailBeads.module.css index 9cd1995c..ffb1f09f 100644 --- a/apps/web/src/components/TrailBeads.module.css +++ b/apps/web/src/components/TrailBeads.module.css @@ -10,7 +10,12 @@ align-items: center; gap: var(--space-2); overflow-x: auto; - padding-block: var(--space-1); + /* No block padding. The beads are already `--touch-min` tall, and the footer + that holds this string reserves exactly that row plus its own block padding + (see App.module.css `.trail`). Adding padding here made this string taller + than the bead — 52px against 44px — so the populated bar outgrew the height + the empty bar reserves and the mus'haf jumped up on the first selection. + The footer's padding is the breathing room; this row just holds the beads. */ padding-inline: var(--space-2); scrollbar-width: none; } @@ -72,10 +77,18 @@ white-space: nowrap; } -/* The live bead — current ayah, amber like the on-page selection wash. */ +/* The live bead — current ayah, amber like the on-page selection wash. + + The wash is deliberately translucent (it echoes the on-page selection, which + multiplies over scripture), and a translucent bead let the string behind it + show straight through the label — a hairline struck through the current + ayah's text. The other beads never showed it because their paper is opaque. + So the wash is composited over that same opaque paper here: the amber tint is + unchanged, but the bead now occludes the thread the way every other bead + does. A solid amber would have lost the wash's kinship with the selection. */ .beadCurrent { border-color: var(--highlight-ring); - background: var(--highlight-wash); + background: linear-gradient(var(--highlight-wash), var(--highlight-wash)), var(--paper-raised); color: var(--ink); } diff --git a/apps/web/src/format.test.ts b/apps/web/src/format.test.ts new file mode 100644 index 00000000..3af5d0bd --- /dev/null +++ b/apps/web/src/format.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { longDay, longMonth } from "./format"; + +// `longDay` turns the day-stamp the record is opened with into the line the map +// shows — «Active since …». It parses the stamp by hand rather than through +// `new Date()`, because a bare "YYYY-MM-DD" is read as UTC midnight and would +// slip a day west of Greenwich. These pin the shape a reader sees, not the clock. +describe("longDay", () => { + it("writes an English stamp as a month name, an ordinal day, and a year", () => { + expect(longDay("2026-09-01", "en")).toBe("Sept 1st, 2026"); + }); + + it("writes an Arabic stamp as day, Arabic month name, year, in Arabic digits", () => { + expect(longDay("2026-03-18", "ar")).toBe("١٨ مارس ٢٠٢٦"); + }); + + it("gets the English ordinal right across the awkward cases", () => { + expect(longDay("2026-01-02", "en")).toBe("Jan 2nd, 2026"); + expect(longDay("2026-01-03", "en")).toBe("Jan 3rd, 2026"); + expect(longDay("2026-01-11", "en")).toBe("Jan 11th, 2026"); + expect(longDay("2026-01-12", "en")).toBe("Jan 12th, 2026"); + expect(longDay("2026-01-13", "en")).toBe("Jan 13th, 2026"); + expect(longDay("2026-01-21", "en")).toBe("Jan 21st, 2026"); + expect(longDay("2026-01-22", "en")).toBe("Jan 22nd, 2026"); + expect(longDay("2026-01-23", "en")).toBe("Jan 23rd, 2026"); + }); + + it("does not shift the day at a timezone boundary", () => { + // A "YYYY-MM-DD" fed to `new Date()` is UTC midnight; rendered in a western + // zone it reads as the evening before. Parsing the fields by hand keeps the + // first of the month the first of the month. + expect(longDay("2026-09-01", "en")).toBe("Sept 1st, 2026"); + expect(longDay("2026-09-01", "ar")).toBe("١ سبتمبر ٢٠٢٦"); + }); + + it("leaves a stamp it cannot parse alone, only localising its digits", () => { + expect(longDay("not-a-date", "en")).toBe("not-a-date"); + expect(longDay("2026-13-01", "en")).toBe("2026-13-01"); + expect(longDay("2026-13-01", "ar")).toBe("٢٠٢٦-١٣-٠١"); + }); +}); + +// `longMonth` names the calendar month the revision map's colouring is scoped to. +describe("longMonth", () => { + it("names the month with its year in each language's names and digits", () => { + expect(longMonth(2026, 9, "en")).toBe("Sept 2026"); + expect(longMonth(2026, 8, "ar")).toBe("أغسطس ٢٠٢٦"); + expect(longMonth(2026, 1, "en")).toBe("Jan 2026"); + expect(longMonth(2026, 12, "ar")).toBe("ديسمبر ٢٠٢٦"); + }); + + it("returns empty rather than an out-of-range name for a bad month", () => { + expect(longMonth(2026, 0, "en")).toBe(""); + expect(longMonth(2026, 13, "ar")).toBe(""); + }); +}); diff --git a/apps/web/src/format.ts b/apps/web/src/format.ts index 7b671130..16415203 100644 --- a/apps/web/src/format.ts +++ b/apps/web/src/format.ts @@ -129,6 +129,76 @@ export function digitsIn(text: string, lang: Lang): string { return LOCALES[lang].digits === "latin" ? text : toArabicDigits(text); } +/** Month names, in the reader's own language — Gregorian, the calendar the day + * stamp is written in. "Sept" is the four-letter form on purpose: it is how the + * month is abbreviated in running English prose, where the other elevens' three + * letters read as abbreviations and September's "Sep" reads as a typo. */ +const MONTHS_EN = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", +] as const; +const MONTHS_AR = [ + "يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", + "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر", +] as const; + +/** The English ordinal suffix for a day of the month: 1st, 2nd, 3rd, 4th … and + * the 11th–13th exception that catches a naive "last digit" rule. */ +function ordinalEn(d: number): string { + const tens = d % 100; + if (tens >= 11 && tens <= 13) return "th"; + switch (d % 10) { + case 1: + return "st"; + case 2: + return "nd"; + case 3: + return "rd"; + default: + return "th"; + } +} + +/** + * A `YYYY-MM-DD` day stamp as a person would read it aloud: "Sept 1st, 2026" in + * English, «١ سبتمبر ٢٠٢٦» in Arabic. The revision map's "active since" line is + * the one caller — a stored ISO stamp is a fact for a computer, not a date a + * reader recognises as *theirs*. + * + * It parses the stamp by hand rather than through `new Date(stamp)`: that + * constructor reads a bare `YYYY-MM-DD` as UTC midnight, which is the day before + * anywhere west of Greenwich, and the whole reason the record stores its own + * day (`dayOf`) is to never let a timezone move a date. English writes the day + * with an ordinal; Arabic writes a plain cardinal, which is how the month is + * spoken in both. A stamp that is not three numbers is handed back untouched, so + * a malformed value degrades to visible rather than throwing. + */ +export function longDay(stamp: string, lang: Lang): string { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(stamp); + if (!m) return digitsIn(stamp, lang); + const year = Number(m[1]); + const month = Number(m[2]); + const day = Number(m[3]); + if (month < 1 || month > 12) return digitsIn(stamp, lang); + return LOCALES[lang].digits === "latin" + ? `${MONTHS_EN[month - 1]} ${day}${ordinalEn(day)}, ${year}` + : `${toArabicDigits(day)} ${MONTHS_AR[month - 1]} ${toArabicDigits(year)}`; +} + +/** + * A month named with its year: "Sept 2026", «سبتمبر ٢٠٢٦». The revision map's + * time-range line uses it — when the colouring is scoped to one calendar month, + * this is the month it is scoped to. Same month names and digits as `longDay`, + * one fewer field. `month` is 1–12; anything else yields an empty string rather + * than an out-of-range name, so a bad call is visibly blank, not silently wrong. + */ +export function longMonth(year: number, month: number, lang: Lang): string { + if (!Number.isInteger(month) || month < 1 || month > 12) return ""; + const name = LOCALES[lang].digits === "latin" ? MONTHS_EN[month - 1] : MONTHS_AR[month - 1]; + const yearText = LOCALES[lang].digits === "latin" ? String(year) : toArabicDigits(year); + return `${name} ${yearText}`; +} + /** * A number to one decimal, in the digits *and the decimal mark* of the UI * language: "5.8" / «٥٫٨». diff --git a/apps/web/src/i18n.tsx b/apps/web/src/i18n.tsx index 2aa0c18e..4e1e2367 100644 --- a/apps/web/src/i18n.tsx +++ b/apps/web/src/i18n.tsx @@ -96,7 +96,8 @@ import { ayahRef as fmtAyahRef, rangeLabel as fmtRangeLabel, digits, - digitsIn, + longDay, + longMonth, surahName as fmtSurahName, surahNames, tenths, @@ -145,6 +146,17 @@ export interface Strings { /* ---- chrome ------------------------------------------------------------- */ about: string; + /** + * The wordmark and its tagline in the header. These follow the UI language — + * «حفظ / مِلاحة للحُفّاظ» in Arabic, "Hifth / Navigation for huffaz" in English + * — because the wordmark is chrome, not scripture, and the note under the + * language switch already promises that only the mus'haf and the verse text + * stay Arabic. The name is a name, not a translated word; the English wordmark + * is its transliteration, which is what the app is called in English prose + * throughout (`about`, `aboutTitle`). + */ + wordmark: string; + tagline: string; pageWord: string; /** * "صفحة 7" / "Page 7" — the stage's accessible name and the page-turn @@ -349,6 +361,7 @@ export interface Strings { licenceHead: string; licenceBody: string; sourceLink: string; + designsLink: string; devBuild: string; devBuildNote: string; sourcesHead: string; @@ -385,6 +398,21 @@ export interface Strings { * tell that lie — an empty map dated this morning is visibly a new record. */ mapSince(day: string): string; + /** The time-range switch's accessible name, beside the division switch. */ + mapRangeGroup: string; + /** Colour by every look on record. */ + mapRangeAll: string; + /** Colour by this calendar month's looks only. */ + mapRangeThisMonth: string; + /** Colour by the previous calendar month's looks only. */ + mapRangeLastMonth: string; + /** + * Which month the colouring is scoped to, when it is scoped to one: + * "Activity in Sept 2026" / «نشاط سبتمبر ٢٠٢٦». It replaces the "active since" + * line under a bounded range, because that line dates the *record* and a + * bounded picture is dating the *window* instead. `month` is 1–12. + */ + mapActiveIn(year: number, month: number): string; /** * The grid's own name. Sixty unlabelled squares inside a dialog are sixty * anonymous list items to a screen reader; naming the list is what separates @@ -483,14 +511,6 @@ export interface Strings { * group called «التكبير», and an announcement arrives with no such context. */ arrivedZoom(percent: number): string; - /** - * Why the stepper is greyed out with the book open. - * - * A disabled control owes an explanation — otherwise it reads as broken rather - * than as not-now — and the explanation names the way out, which is the toggle - * sitting beside it. - */ - zoomTwoPage: string; /** Where a `Shift`+wheel landed: the juz, and the page it opens on. */ arrivedJuz(juz: number, page: number): string; /** …and where it did not, because that was the first or last juz we hold. */ @@ -544,6 +564,8 @@ export function buildStrings(lang: Lang, m: Catalog): Strings { langSwitchTo: (other) => m.langSwitchTo({ other }), about: m.about, + wordmark: m.wordmark, + tagline: m.tagline, pageWord: m.pageWord, pageN: (page) => m.pageN({ page }), goTo: m.goTo, @@ -748,6 +770,7 @@ export function buildStrings(lang: Lang, m: Catalog): Strings { licenceHead: m.licenceHead, licenceBody: m.licenceBody, sourceLink: m.sourceLink, + designsLink: m.designsLink, devBuild: m.devBuild, devBuildNote: m.devBuildNote, sourcesHead: m.sourcesHead, @@ -770,10 +793,17 @@ export function buildStrings(lang: Lang, m: Catalog): Strings { totalText: n(total), scope: scope === "juz" ? "other" : scope, }), - // The day stamp is a string with hyphens in it, not a number, which is why - // it goes through `digitsIn` rather than `digits` — «٢٠٢٦-٠٧-٣٠» keeps its - // shape and the separators survive. - mapSince: (day) => m.mapSince({ dayText: digitsIn(day, lang) }), + // The stored day is an ISO stamp — a fact for a computer. `longDay` turns it + // into the date a reader recognises as theirs: "Sept 1st, 2026" / «١ سبتمبر + // ٢٠٢٦», in the reader's own month names and digits. + mapSince: (day) => m.mapSince({ dayText: longDay(day, lang) }), + mapRangeGroup: m.mapRangeGroup, + mapRangeAll: m.mapRangeAll, + mapRangeThisMonth: m.mapRangeThisMonth, + mapRangeLastMonth: m.mapRangeLastMonth, + // `longMonth` names the month in the reader's own month names and digits — + // "Sept 2026" / «سبتمبر ٢٠٢٦» — the same table `longDay` draws from. + mapActiveIn: (year, month) => m.mapActiveIn({ monthText: longMonth(year, month, lang) }), mapGrid: m.mapGrid, mapNoStore: m.mapNoStore, mapLoading: m.mapLoading, @@ -829,7 +859,6 @@ export function buildStrings(lang: Lang, m: Catalog): Strings { zoomOut: m.zoomOut, zoomLevel: (percent) => m.zoomLevel({ pctText: n(percent) }), arrivedZoom: (percent) => m.arrivedZoom({ pctText: n(percent) }), - zoomTwoPage: m.zoomTwoPage, arrivedJuz: (juz, page) => m.arrivedJuz({ juzText: n(juz), page }), juzEdge: (juz) => m.juzEdge({ juzText: n(juz) }), }; diff --git a/apps/web/src/messages/ar.gen.ts b/apps/web/src/messages/ar.gen.ts index 1da843da..287dea2c 100644 --- a/apps/web/src/messages/ar.gen.ts +++ b/apps/web/src/messages/ar.gen.ts @@ -44,6 +44,7 @@ const messages: Catalog = { "coachSteps.3.title": "المس رقاقة", copied: "نُسخ الرابط", copyFailed: "تعذّر النسخ", + designsLink: "التصاميم والقرارات", devBuild: "نسخة تطوير", devBuildNote: "هذه نسخة تطوير محلّية، فلا تقابلها إصدارة معيّنة؛ الرابط يفتح المستودع.", dismiss: "إخفاء", @@ -97,6 +98,7 @@ const messages: Catalog = { licenceBody: "برنامج حرّ برخصة GNU GPL الإصدار الثالث أو ما بعده: لك أن تدرسه وتعدّله وتنشره. وهذه شيفرة هذه النسخة بعينها، لا فرعٌ قد يتغيّر:", licenceHead: "الرخصة والمصدر", mapAbsent: "غير متوفّر في هذه النسخة", + mapActiveIn: (d) => "نشاط " + d.monthText, mapCaveat: "النقر دليل على أنّك فتحت الآية، لا على أنّك راجعتها.", mapCellAbsent: (d) => d.label + " · غير متوفّر في هذه النسخة", mapCellNever: (d) => d.label + " · لم يُفتح", @@ -108,9 +110,13 @@ const messages: Catalog = { mapNeverOpened: "متوفّر ولم يُفتح", mapNoStore: "لا يمكن حفظ السجلّ في هذا المتصفّح، فلا شيء هنا لعرضه.", mapOpen: (d) => "صفحة " + d.page + " · ما فتحتَه من المصحف", + mapRangeAll: "كل الوقت", + mapRangeGroup: "المدة", + mapRangeLastMonth: "الشهر الماضي", + mapRangeThisMonth: "هذا الشهر", mapRecent: "فُتح حديثًا", mapScopeGroup: "التقسيم", - mapSince: (d) => "يُسجَّل منذ " + d.dayText, + mapSince: (d) => "نشِط منذ " + d.dayText, mapTitle: "ما فتحتَه من المصحف", mapWentTo: (d) => d.label + " · صفحة " + d.page, mushaf: "المصحف", @@ -202,6 +208,7 @@ const messages: Catalog = { stageFailed: (d) => "تعذّر تحميل صفحة " + d.pageText + ". أعد المحاولة.", stageLoading: "…جاري التحميل", surahN: (d) => "سورة " + d.surahText, + tagline: "مِلاحة للحُفّاظ", tajweed: "تجويد", tajweedCredit: "أحكام التجويد مأخوذة من quran-tajweed (Collin Fair)، رخصة CC BY 4.0.", tapHint: "المس آية على الصفحة لتحديدها", @@ -209,11 +216,11 @@ const messages: Catalog = { twin: "توأم", wordHops: (d) => plural(LC, d.n, { zero: "لا مواضع مشابهة", one: "موضع مشابه واحد", two: "موضعان متشابهان", few: d.nText + " مواضع مشابهة", many: d.nText + " موضعًا مشابهًا", other: d.nText + " موضع مشابه" }) + plural(LC, d.u, { zero: "", one: "، ووصلة واحدة لا تسمّي كلمات", two: "، ووصلتان لا تسمّيان كلمات", few: "، و" + d.uText + " وصلات لا تسمّي كلمات", many: "، و" + d.uText + " وصلة لا تسمّي كلمات", other: "، و" + d.uText + " وصلة لا تسمّي كلمات" }), wordLevelPending: "الربط على مستوى الكلمة يصل مع الحزمة القادمة", + wordmark: "حفظ", zoomIn: "تكبير", zoomLevel: (d) => d.pctText + "٪", zoomOut: "تصغير", zoomSectionTitle: "التكبير", - zoomTwoPage: "التكبير يحتاج صفحة واحدة — بدّل إلى صفحة واحدة أولًا", }; export default messages; diff --git a/apps/web/src/messages/ar.json b/apps/web/src/messages/ar.json index 2101ff06..0869dd80 100644 --- a/apps/web/src/messages/ar.json +++ b/apps/web/src/messages/ar.json @@ -3,6 +3,8 @@ "langSectionNote": "تتغيّر لغة الأزرار والقوائم فقط؛ المصحف ونصّ الآيات عربيّ دائمًا.", "langSwitchTo": "التبديل إلى {other}", "about": "عن حِفظ · الرخصة والمصادر", + "wordmark": "حفظ", + "tagline": "مِلاحة للحُفّاظ", "pageWord": "صفحة", "pageN": "صفحة {page}", "goTo": "اذهب إلى", @@ -143,6 +145,7 @@ "licenceHead": "الرخصة والمصدر", "licenceBody": "برنامج حرّ برخصة GNU GPL الإصدار الثالث أو ما بعده: لك أن تدرسه وتعدّله وتنشره. وهذه شيفرة هذه النسخة بعينها، لا فرعٌ قد يتغيّر:", "sourceLink": "الشيفرة المصدرية", + "designsLink": "التصاميم والقرارات", "devBuild": "نسخة تطوير", "devBuildNote": "هذه نسخة تطوير محلّية، فلا تقابلها إصدارة معيّنة؛ الرابط يفتح المستودع.", "sourcesHead": "المصادر", @@ -156,7 +159,12 @@ "scopeJuz": "جزء", "hizbN": "الحزب {hizbText}", "mapHeld": "المتوفّر {haveText} من {totalText} {scope, select, page{صفحة} hizb{حزبًا} other{جزءًا}}", - "mapSince": "يُسجَّل منذ {dayText}", + "mapSince": "نشِط منذ {dayText}", + "mapRangeGroup": "المدة", + "mapRangeAll": "كل الوقت", + "mapRangeThisMonth": "هذا الشهر", + "mapRangeLastMonth": "الشهر الماضي", + "mapActiveIn": "نشاط {monthText}", "mapGrid": "خريطة المصحف", "mapNoStore": "لا يمكن حفظ السجلّ في هذا المتصفّح، فلا شيء هنا لعرضه.", "mapLoading": "جارٍ فتح السجلّ…", @@ -200,7 +208,6 @@ "zoomOut": "تصغير", "zoomLevel": "{pctText}٪", "arrivedZoom": "التكبير {pctText}٪", - "zoomTwoPage": "التكبير يحتاج صفحة واحدة — بدّل إلى صفحة واحدة أولًا", "arrivedJuz": "الجزء {juzText} · صفحة {page}", "juzEdge": "لا جزء في هذا الاتجاه · الجزء {juzText}" } diff --git a/apps/web/src/messages/catalog.gen.ts b/apps/web/src/messages/catalog.gen.ts index e5454f0c..fa45052c 100644 --- a/apps/web/src/messages/catalog.gen.ts +++ b/apps/web/src/messages/catalog.gen.ts @@ -38,6 +38,7 @@ export interface Catalog { readonly "coachSteps.3.title": string; readonly copied: string; readonly copyFailed: string; + readonly designsLink: string; readonly devBuild: string; readonly devBuildNote: string; readonly dismiss: string; @@ -91,6 +92,7 @@ export interface Catalog { readonly licenceBody: string; readonly licenceHead: string; readonly mapAbsent: string; + readonly mapActiveIn: (d: { readonly monthText: string | number }) => string; readonly mapCaveat: string; readonly mapCellAbsent: (d: { readonly label: string | number }) => string; readonly mapCellNever: (d: { readonly label: string | number }) => string; @@ -102,6 +104,10 @@ export interface Catalog { readonly mapNeverOpened: string; readonly mapNoStore: string; readonly mapOpen: (d: { readonly page: string | number }) => string; + readonly mapRangeAll: string; + readonly mapRangeGroup: string; + readonly mapRangeLastMonth: string; + readonly mapRangeThisMonth: string; readonly mapRecent: string; readonly mapScopeGroup: string; readonly mapSince: (d: { readonly dayText: string | number }) => string; @@ -196,6 +202,7 @@ export interface Catalog { readonly stageFailed: (d: { readonly pageText: string | number }) => string; readonly stageLoading: string; readonly surahN: (d: { readonly surahText: string | number }) => string; + readonly tagline: string; readonly tajweed: string; readonly tajweedCredit: string; readonly tapHint: string; @@ -203,9 +210,9 @@ export interface Catalog { readonly twin: string; readonly wordHops: (d: { readonly n: number; readonly nText: string | number; readonly u: number; readonly uText: string | number }) => string; readonly wordLevelPending: string; + readonly wordmark: string; readonly zoomIn: string; readonly zoomLevel: (d: { readonly pctText: string | number }) => string; readonly zoomOut: string; readonly zoomSectionTitle: string; - readonly zoomTwoPage: string; } diff --git a/apps/web/src/messages/en.gen.ts b/apps/web/src/messages/en.gen.ts index 1cc76cac..208e3f1a 100644 --- a/apps/web/src/messages/en.gen.ts +++ b/apps/web/src/messages/en.gen.ts @@ -44,6 +44,7 @@ const messages: Catalog = { "coachSteps.3.title": "Tap a chip", copied: "Link copied", copyFailed: "Could not copy", + designsLink: "Designs and decisions", devBuild: "dev build", devBuildNote: "This is a local development build, so no released version corresponds to it; the link opens the repository.", dismiss: "Hide", @@ -97,6 +98,7 @@ const messages: Catalog = { licenceBody: "Free software under the GNU GPL, version 3 or later: you may study it, change it and pass it on. This is the source of this exact build, not a branch that may move:", licenceHead: "Licence and source", mapAbsent: "Not in this build", + mapActiveIn: (d) => "Activity in " + d.monthText, mapCaveat: "A tap is evidence you opened an ayah, not that you revised it.", mapCellAbsent: (d) => d.label + " · not in this build", mapCellNever: (d) => d.label + " · never opened", @@ -108,9 +110,13 @@ const messages: Catalog = { mapNeverOpened: "Here, never opened", mapNoStore: "This browser cannot keep the record, so there is nothing here to show.", mapOpen: (d) => "Page " + d.page + " · what you have opened", + mapRangeAll: "All time", + mapRangeGroup: "Time range", + mapRangeLastMonth: "Last month", + mapRangeThisMonth: "This month", mapRecent: "Opened recently", mapScopeGroup: "Division", - mapSince: (d) => "Recording since " + d.dayText, + mapSince: (d) => "Active since " + d.dayText, mapTitle: "What you have opened", mapWentTo: (d) => d.label + " · page " + d.page, mushaf: "Mus'haf", @@ -202,6 +208,7 @@ const messages: Catalog = { stageFailed: (d) => "Could not load page " + d.pageText + ". Try again.", stageLoading: "Loading…", surahN: (d) => "Surah " + d.surahText, + tagline: "Navigation for huffaz", tajweed: "Tajweed", tajweedCredit: "Tajweed rules from quran-tajweed (Collin Fair), licensed CC BY 4.0.", tapHint: "Tap an ayah on the page to select it", @@ -209,11 +216,11 @@ const messages: Catalog = { twin: "twin", wordHops: (d) => plural(LC, d.n, { "0": "No similar places", one: d.nText + " similar place", other: d.nText + " similar places" }) + plural(LC, d.u, { "0": "", one: ", and " + d.uText + " link that names no words", other: ", and " + d.uText + " links that name no words" }), wordLevelPending: "Word-level links arrive with the next data pack", + wordmark: "Hifth", zoomIn: "Zoom in", zoomLevel: (d) => d.pctText + "%", zoomOut: "Zoom out", zoomSectionTitle: "Zoom", - zoomTwoPage: "Zoom needs one page — switch to one page first", }; export default messages; diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 14291223..079d3d4b 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -3,6 +3,8 @@ "langSectionNote": "This changes the buttons and menus only. The mus'haf and the verse text are always Arabic.", "langSwitchTo": "Switch to {other}", "about": "About Hifth · licence and sources", + "wordmark": "Hifth", + "tagline": "Navigation for huffaz", "pageWord": "Page", "pageN": "Page {page}", "goTo": "Go to", @@ -143,6 +145,7 @@ "licenceHead": "Licence and source", "licenceBody": "Free software under the GNU GPL, version 3 or later: you may study it, change it and pass it on. This is the source of this exact build, not a branch that may move:", "sourceLink": "Source code", + "designsLink": "Designs and decisions", "devBuild": "dev build", "devBuildNote": "This is a local development build, so no released version corresponds to it; the link opens the repository.", "sourcesHead": "Sources", @@ -156,7 +159,12 @@ "scopeJuz": "Juz", "hizbN": "Hizb {hizbText}", "mapHeld": "{haveText} of {totalText} {scope, select, page{pages} hizb{hizb} other{juz}} in this build", - "mapSince": "Recording since {dayText}", + "mapSince": "Active since {dayText}", + "mapRangeGroup": "Time range", + "mapRangeAll": "All time", + "mapRangeThisMonth": "This month", + "mapRangeLastMonth": "Last month", + "mapActiveIn": "Activity in {monthText}", "mapGrid": "Map of the mus'haf", "mapNoStore": "This browser cannot keep the record, so there is nothing here to show.", "mapLoading": "Opening the record…", @@ -200,7 +208,6 @@ "zoomOut": "Zoom out", "zoomLevel": "{pctText}%", "arrivedZoom": "Zoom {pctText}%", - "zoomTwoPage": "Zoom needs one page — switch to one page first", "arrivedJuz": "Juz {juzText} · page {page}", "juzEdge": "No juz that way · juz {juzText}" } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 1843cde2..739754d8 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -91,6 +91,11 @@ export default defineConfig({ // 604 pages × ~160 KB would make the install download the whole book. globIgnores: ["assets/pages/**"], navigateFallback: "index.html", + // The design pages the build stages under /docs/ (scripts/stage-docs.mjs) + // are real documents, not app routes: a reader with the app installed + // must get the page, not the shell. They are not in the precache either + // — the glob above runs before they are copied in. + navigateFallbackDenylist: [/\/docs\//], cleanupOutdatedCaches: true, // Fill the offline cache from the *first* visit rather than the second: // without claiming, the tab that ran the install stays uncontrolled, so diff --git a/docs/PLAN.md b/docs/PLAN.md index e997be2a..6032ec68 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -53,6 +53,180 @@ described in both files. | 7 — Polish + beta | in flight — its four engineering items are all in (popover ordering, keyboard map, shard prefetch, the 5-page golden sweep, 2026-08-07); what remains is not a loop but a person | Hafiz revision session, zero friction notes → **web v1.0** | — | | Track B — Capacitor | gated on web v1.0 | Same web build wrapped for iOS/Android; Universal Links | — | +### Desktop UI triage — a running thread under Loop 7 (opened 2026-09-01) + +A hafiz using the desktop spread reported a run of paper-cuts and asked for a few new +surfaces. These are being worked as a **task list** (the session-tracked kind, the one the +"track the task files" commit made survive a clean open), not as loop items, because each is +small and none blocks web v1.0. The task list is the working copy; **this section is the one +that survives a session**, so when the two disagree, bring this one current. An audit on +2026-09-01 found it two defects stale and eight asks short, which is how this paragraph came +to be written. + +**What this thread is for.** Every paper-cut a hafiz meets on the desktop spread gets fixed +with a guard, and the *class* of defect behind it gets a tool, so the next one of its kind is +caught before a reader meets it. The thread is done when the open list below is empty or +every remaining item is waiting on a decision the owner has been asked for. Its state lives +in three places: this section (the record), the session task list (the working copy) and +`docs/tasks.md` (the rendered page, which the gate keeps current). + +**Done, each with its guard** (all 2026-09-01): + +- **Zoom the open book.** The magnify buttons work with two pages showing and grow both + leaves together, from the fold outward, so the middle gap is kept and a modest step clips + nothing. It reverses an earlier "disabled while open" finding, on the reader's own call. + Record: [`spread-zoom.md`](decisions/spread-zoom.md); the reversal is noted on + [`wheel-and-zoom.md`](decisions/wheel-and-zoom.md). +- **Turn a leaf by grabbing its outer fore-edge** (a hand cursor on a rail that widens at the + corners); a drag across the middle pans and selects, never turns. `b90b450`. +- **The two leaves stay level** after a juz jump (`b90b450`) and after a turn (`ba436e6`). + Two fixes of one rule at two of its four sites — the rule now has one home, below. +- **One home for "the leaves land level and centred."** Every road onto a page — a cold + open, a deep link, a hop, a turn's landing — now ends in one settle step in the stage, + where four had each copied its three lines by hand; the fifth copy, hidden inside the + fade under a turn's band, was found by looking for the other four. Two guards: a unit + test that counts the sites and refuses a new road that centres by hand, and a desktop + e2e that drives eight roads (cold open, deep link, page bar, turn, juz jump, zoom step, + closing and reopening the book, a resize across the breakpoint) through one measurement. + *A finding on the way:* that e2e first failed the reopen road by 313 px — a frame in + which both leaves still wear their one-leaf transforms when an animation-frame callback + measures them, and which the resize observer corrects before it is painted. A reader never + sees it, so the rig reads after paint; the two older guards read inside the frame and + pass only because their roads happen to settle earlier. In the tree, **not yet committed**. +- **A turned leaf moves the address** in the URL bar, as a jump already did. `c1d702c`. +- **Ayah boxes drawn wrong.** 2:249's six-line middle inked as one slab, and 10:44 dropped + to the coarse fallback because the print rounds to a tenth — with **110 other boxes** + silently on the same fallback. `b2ffce3`. Nothing yet sweeps the book for the next one; + see the follow-ups. +- **The revision map**: a number on every cell, "not in this build" keyed only when a cell in + fact is, a date a person would write, and a picker for all time / last month / this month. + `7d4402e` `631278b` `0ab9619` `ae8d01d`. +- **Zoom on the spread grew one leaf and left the other.** A fault of the dev server's + double-mount only — the built app never had it — but the dev server is what a developer + previews in. Fixed and guarded. `ff81f8e`. +- **The page bar redrawn**: thirty green detents, one per juz; a page-icon handle over the + native slider (kept for its keyboard and screen-reader contract); a popover that names + page · juz · surah while dragging. In the tree with seven tests, **not yet committed**. + Two calls were made without a record — whether a detent is a landmark or a magnet, and + which juz a boundary page belongs to — and a decision is owed for the pair. + +**Still open, worst-first:** + +- **A desktop page turn that peels from the corner** — asked for, and **refused by the + page-transition record** (§8 ①: a curl necessarily moves the glyphs, and it cannot draw + the difference between a crease and a gap). The ask and the record conflict, and only the + owner can settle it: reopen that record with a curl drawn that expresses all four fold + states, or improve the turn inside the fold vocabulary it already has. **Blocked on that + answer.** Interim: the grab plays the current flip on release. +- **The ayah options on the facing leaf.** On a spread, selecting on one page should raise + its options on the other, so the menu never covers the ayah being worked on; the phone + keeps its bottom sheet. Tests owed: the drawer lands opposite for either side, and the + stage does not move on selection. +- **A tafseer section** inside those options, with sourced text and its provenance checked. + After the facing-leaf drawer. +- **Comment-style mistake marking.** Zoom in, click a letter, a harakah or a marker, and + drop a note anchored to that glyph; colour by kind (a comment, a correction, a note to the + developers); icons shown only past a zoom threshold; hover reveals the span. A drag across + text must **select rather than turn the page**. Must reconcile with the mark and sitting + system already in the tree, not start fresh. A decision before code. +- **Keeping marks and comments on the device, and exporting them in a batch** (a file, or + email). Touches the revision-privacy question. +- **A bookmark you drop by tapping the fold**, animating downward like a ribbon, and every + bookmark action written to a calendar. Needs a decision (where a bookmark is stored, what the + animation is) before code. +- **An activity calendar in the menu bar** — a day's reading, interactions and pages counted — + and, inside it, **managing bookmarks** (clear a surah's, clear all, with a guard on the + destructive ones). This is the long-planned revision record reaching the chrome; the shape + is sketched in [`.claude/plans/interaction-calendar.md`](../.claude/plans/interaction-calendar.md). + Needs its own decision, and it touches the revision-privacy question. +- **A highlighter setting** — outline vs. filled, and how see-through the fill is — reached from + a menu-bar icon. Needs a decision that draws each style on a real page at reading size. + +**What the thread taught, as follow-ups** — the pattern under the paper-cuts, filed so the +next one is caught by a tool: + +- **One home for "the leaves land level and centred"** — done, above. +- **A whole-book sweep for ayah boxes** — done. Three box defects were each found by a + reader on one page, and the fix for one uncovered 110 more nobody had reported. Now + `pnpm gate:boxes` runs the app's own pen over all 6,236 boxes (12,350 rectangles) and holds + the count of boxes it cannot draw as lines at 8, all on the decorated pages 1–2, and the + count of rectangles off the page's line grid at 2 — as ceilings *and* floors, so a pen that + starts accepting something is as visible as one that starts refusing. It also counts, but + does not hold, the 582 rectangles that fuse several lines (up to 13) and the 99 one-word + dots. `make box-sweep` draws every flagged box on its real page at twice print size in + `docs/design/ayah-box-sweep.html`. The two off-grid boxes are a new class, filed as + follow-up 17. +- **A recipe for seeing a change** — done: the `run-app` skill now says which server is which, + that a hash-only navigation is not a reload, and to rebuild before e2e. Two sessions had + lost turns to each. +- **The page-bar decisions** — record and page done, publish pending. Two open rows in the + decision register, *juz-detents* (does a marker pull the release onto it) and *boundary-juz* + (which juz a page carrying a seam is in), one record `docs/decisions/page-bar.md`, one drawn + page `docs/design/page-bar-options.html` rebuilt by `scripts/build-page-bar-options.mjs` + from the manifest and the bar's own stylesheet. The bar is 1.34 px a page on a laptop and + 0.4 px on a phone; four pages (62, 121, 201, 502) carry a juz seam, and on those the bar's + bubble and the pack shelf already name different juz. The decision gate is red on exactly + one thing — an open row with no published copy — until the owner says to publish, which + is a per-action ask. +- **The goldens were stale before this thread touched them.** Verifying the settle step ran + the whole Chromium suite, and twelve phone goldens failed — all of them at a clean checkout + of the branch head, and all of them since `b90b450`, the commit that made a hop land the + leaves level. Almost certainly that fix reaching the phone framing; whether the new framing + is the wanted one is the owner's call, and only after that a re-baseline of darwin and + linux together. Filed as follow-up 16 under §Open follow-ups and indexed. +- **A register went stale at a commit and nothing said so until now.** The use-cases page + had not been re-rendered since `b90b450`; its gate failed at a clean checkout of the branch + head, so that commit shipped with the gate red. Re-rendered (only line-number pointers had + moved). The commit had edited `docs/use-cases.json` itself, and the pre-commit hook let it + through: the hook ran the use-cases gate *scoped to the staged files*, which asks whether + each pointer still resolves and never whether the page was re-rendered. Fixed in the hook, + which now runs the use-cases, issues and tasks stale checks unscoped on every commit (three + hash comparisons, under a second) and names `make render-docs`, a new target that rebuilds + all four generated pages. The decisions README was already covered, since its hash depends + only on its register. Not a new Claude-side hook: the failure was at the commit, and that + is where the numbers freeze. +- **The bundle grew 2.2 KB and the baseline now says so.** The bar redesign and the settle + step together moved the main bundle from 117.9 to 120.1 KB gzipped, past the 1 KB the + budget gate absorbs quietly; 29.9 KB of headroom remains. The baseline was re-recorded + so the number lands in the diff a reviewer reads. + +- **The adjacency tree now names its third parent.** 510 of 3,002 shipped pairings carry a + same-juz flag computed from the core package's juz table, which is Tanzil's structural + metadata under CC BY, and neither the licensing map's row nor the shipped notice said so + (what-we-depend-on ③). Named by hand in the notices gate's declaration, so the row and the + notice must both carry "Tanzil" or the gate fails; the adjacency builder writes a paragraph + into the notice, and a rebuild changed only that notice. The half still open is whether the + trace should follow imports into core, now a question rather than a defect. + +- **The licensing map is organised by door.** Three tables — repository, deployed site, store + — each row saying what goes out, through which, under whose terms, with the measured numbers + from the distribution audit (what-we-distribute ①, answered). Rows keep the shape the notices + gate parses, and every gate that reads the file is green. Answered, not fixed: nothing checks + that the tables stay sorted by door. + +- **Every design is public, on the site — a tenet now, 2026-09-01.** Asked whether there was + an admin page to put the designs on instead of publishing copies to another host, and the + owner's answer was that all of them should be public-facing, for transparency, and that this + should be a tenet of the repo. So: the web build now stages every page under `docs/` onto the + site at the same path, with a generated front door at `/docs/` and a link to it from the + colophon; the decision gate derives each decision's public address from its path and refuses + any other; the eight decision rows with a page now carry the site address, and the records + link it (earlier other-host copies stay named as copies); the tenet is in `CLAUDE.md` and the + decide skill follows it. The page-bar page is therefore published by merging, and the + decisions gate is green without anything being put on another host. + +**Not yet committed, awaiting the owner's go-ahead** (as of 2026-09-01): the page bar +redesign and its seven tests; the settle step, its unit test and the every-road e2e; the +`run-app` recipe; the box sweep (its library, gate, page builder, the drawn page and its +wiring into the gates, the Makefile, CI and the map); follow-ups 16 and 17 and their index +rows; the page-bar decision record, its drawn page, its builder and its two register rows; +the re-rendered use-cases page; the bundle baseline; the by-door `LICENSES.md`, the adjacency notice and +builder, the notices gate declaration, the two design-doc markers with their index rows; the +public-designs tenet (the staging step and the site module, the build and CI wiring, the +service-worker denylist, the derived decision addresses in the gate, the eight register rows +and their records, the colophon link and its strings, the `CLAUDE.md` section, the decide +skill, the map and artifact-register notes); this section and the tasks page it re-renders. + ### Open follow-ups These fifteen are indexed in [`issues.json`](issues.json) alongside the design docs' open @@ -770,6 +944,63 @@ in for that. `pnpm gate:issues` checks the number still exists and reads no furt of what a span claims, why a run has three outcomes rather than two, and what the refined search still does not do — the rail beside the page is not filtered, only counted. +16. **The golden baselines predate the spread triage, and twelve shots fail at a clean + checkout.** Found 2026-09-01 while verifying the settle step of the desktop-triage thread, + which ran the whole Chromium suite. The phone goldens (`e2e/golden.spec.ts`, Chromium; the + darwin baselines under `e2e/__screenshots__/darwin`) were last written in `259fde0` + (2026-08-07). Twelve now fail: the selection, breadcrumb and phrase states on pages 1, 7, 9, + 19 and 604, and the page-7 marquee. The signature is one change, not twelve — the whole + page sits a few pixels lower after a hop, so every shot taken after a hop differs, from + 2,624 px (0.01 of the image) on the page-9 breadcrumb to 59,475 px (0.12) on the page-7 + selection, and the marquee shot grew one row, 574 → 575 px. Attributed by bisection, one + build per commit: the page-7 selection shot passes at `7d4402e` and fails at `b90b450` + with the identical 59,475-pixel count, and every commit since inherits it; the working tree + adds nothing (the same counts with the whole tree stashed at `ff81f8e`). `b90b450` is the + spread-triage commit that, among other things, made a hop re-centre the page so the two + leaves land level — so the shift is most likely that fix reaching the phone framing, which + is what a golden exists to make *deliberate*, not to forbid. **What would answer it:** the + owner looking at one diff (`npx playwright show-report` after a `--project golden` run) and + saying whether the new framing is the wanted one. If it is, `make golden` rewrites the + darwin baselines and `make golden-linux` the linux ones in the pinned image, in one commit + that says why; if it is not, the fix goes back into `b90b450`'s stage change. Not + re-baselined silently: twelve shots changing under a commit that never mentions them is + exactly the event the goldens are for. The linux baselines were not checked here — they + run only in the image. Blocks nothing; the rest of the Chromium suite is green (176 + passed, 1 skipped, the iphone project not run — WebKit is not installed on this machine). + **Closed 2026-09-01.** The owner looked at the diff — the whole page one pixel lower, + every glyph edge different, the highlight bands themselves unchanged — and accepted the + framing. Both sets were rewritten in one commit: darwin by `make golden-update`, linux by + `make golden-linux UPDATE=1` in the pinned Playwright image, twelve shots each, the same + twelve on both platforms and nothing else. **Closed by** + [`apps/web/e2e/golden.spec.ts`](../apps/web/e2e/golden.spec.ts) against the new + baselines: the next shift of the framing fails the same twelve shots again, on both + platforms, which is the event these exist to make deliberate. + +17. **Two ayah boxes are cut short of the line, and the pen draws them thin.** Found + 2026-09-01 by the whole-book box sweep (`pnpm gate:boxes --list`), the first thing it + flagged that no reader had reported. Of the 12,350 rectangles that make up the 6,236 ayah + boxes, two are not a whole number of lines tall: 68:3 on page 564 (27.9 units on a 36-unit + line, 0.77 of a line) and 107:2 on page 602 (28.3 units, 0.79). Both are the same shape — + an ayah's short tail at the left margin, where the polygon layer cut the box some 8 units + short of the line and started the *next* ayah's box that much early, so the two boxes + still tile the column but the seam is in the wrong place. The pen accepts them (they are + rectangles) and draws a band 0.72 of the *rectangle's* height rather than the line's, so + the lit tail is a little thinner and sits a little higher than its neighbours; at print + size in `docs/design/ayah-box-sweep.html` it is hard to see, and a reader would sooner + notice the next ayah's band starting above its own first word. Every other rectangle in + the book is on the grid within 0.2 of a line, which is what makes two worth a row: this is + a print quirk, not a class the pen has to learn. **What would answer it:** either a repair + in the vendoring step — a `POLYGON_REPAIRS` entry in `scripts/vendor-pages.mjs` that + stretches each tail to the line and pulls the next box back, then re-vendor and re-pin the + two pages — with `OFF_GRID_COUNT` in `scripts/gate-boxes.mjs` lowered to 0 in the same + change; or a decision to tolerate it, written here, in which case the gate keeps holding + 2 so that a third is still an event. Not chosen here because the fix is a corpus edit, and + corpus edits are owner-visible in the pinned pages. Blocks nothing. **Blocked, 2026-09-01:** the repair entry has to be + written against the *upstream* polygon, and the upstream page cache is gone — it was a + symlink into a scratch directory that has since been emptied, so re-vendoring means a + 348 MB fetch first, which nobody starts without asking. Until then the gate holds the + count at 2 and the two tails ship as the print drew them. + **The half of these a machine cannot run now has a register — and a runbook.** Follow-ups ① (the phone), ② (the browser glance) and ④ (VoiceOver/TalkBack) still wait on a human, and prose cannot answer "is that still true, on what device, and when?" — ⑤ (does the source @@ -1294,7 +1525,7 @@ an hour. ### Track B (gated on stable web beta) — Capacitor iOS, then Android Wrap `apps/web`; bundle the full corpus in-app (offline by default); native share sheet; -**Universal Links** (links open the app — solves research §5 on iOS); haptics on hop-land +**Universal Links** (links open the app — solves research §6 on iOS); haptics on hop-land and bead taps; state restoration; iPad two-page spread; App Store review notes lead with KFGQPC provenance. Android fast-follow (~95% shared code). Do not start until web v1.0. diff --git a/docs/artifacts.json b/docs/artifacts.json new file mode 100644 index 00000000..6467fb10 --- /dev/null +++ b/docs/artifacts.json @@ -0,0 +1,194 @@ +{ + "$comment": [ + "The published-page register: one row per page this project has put on the", + "web for somebody to look at, and the only place that knows they exist.", + "", + "WHY THIS EXISTS. Nine pages had been published. The tree knew about five.", + "The other four were built in a scratch directory, published, discussed, and", + "then the scratch directory was cleared — so for those four the link is the", + "only copy, and nothing anywhere named the link. That is the exact failure", + "docs/decisions.json already refuses for a decision page ('a link with no", + "copy dies the day the host does'), and it had simply never been asked about", + "a page that was not attached to a decision. A diagnosis, a comparison, a", + "recommendation and a plan all went out that door.", + "", + "It is also not recoverable by looking. A published page's address is minted", + "by the publish and is never written back into the repository; the only", + "record of it is the session log the publish happened in, which lives", + "outside this repository on one laptop. So the register cannot be generated", + "and cannot be checked in CI. scripts/artifact-sweep.mjs reads those logs on", + "the machine that has them and reports what this file is missing, and a", + "PostToolUse hook runs it after each publish. That is a reminder, not a", + "gate, and it is written down here so nobody mistakes it for one.", + "", + "WHAT IT IS NOT. This file does not restate a decision, and for the four", + "pages that belong to one it does not restate the page's location or its", + "build command either — docs/decisions.json owns those, and a second copy", + "would be right for a while and then quietly stop being right. Same rule as", + "every other register here: an index points, it does not paraphrase.", + "", + "WHAT IT ADDS. The two facts no other file in this repository can hold: that", + "the page exists at a public address at all, and what it calls itself there.", + "", + "THE ONE DUPLICATION, ON PURPOSE. `url` is repeated from docs/decisions.json", + "for the four decision pages. It is the identity, not prose — an inventory", + "whose whole job is 'every page we published, in one place' has to list all", + "of them or it is not an inventory. The sweep checks the two agree.", + "", + "THE FIELDS.", + " url https://claude.ai/... — the address, and the identity.", + " title what the page calls itself. Stored, because the page lives on", + " a host nothing here can read at build time. It is the one fact", + " this file keeps a copy of, and retitling a page means editing", + " this line by hand.", + " published the day the host last saw a change. Also stored, same reason.", + " shows one plain-language sentence: what a reader sees on opening it.", + " The only prose this file owns. No file names, no symbols — the", + " CLAUDE.md tenet, same as `question` in the decision register.", + " decision the docs/decisions.json row that owns this page, or null. When", + " it is set, that row holds the checked-in copy and the script", + " that rebuilds it, and the next three fields stay null.", + " page the same page, checked in, for a page no decision owns.", + " builtBy the script that regenerates `page`. A checked-in page nobody", + " can rebuild is a screenshot with extra steps.", + " note required when there is no copy anywhere — `decision` null and", + " `page` null. It says why not, and what would give it a home.", + " A row that needs this field is a page one outage away from", + " being gone, so the field exists to make that legible rather", + " than to excuse it.", + "", + "SINCE 2026-09-01 the site itself carries every page under docs/ at the", + "same path (scripts/stage-docs.mjs, run by the app's build), so a page is", + "public by being merged and its address is derived from its path. This", + "register is for copies put on another host — still worth listing, still", + "not checkable from here — and the decision register no longer accepts", + "such a copy as a decision's address.", + "", + "node scripts/artifact-sweep.mjs what has been published that is not here", + "node scripts/build-decision-board.mjs redraws the board, which shows this", + "", + "Hand-edited, never generated — like every register in docs/." + ], + "artifacts": [ + { + "url": "https://claude.ai/code/artifact/1d9c4cff-f0a5-4aa3-a70b-2717f0814b1a", + "title": "Copy your confusion map off the phone?", + "published": "2026-08-31", + "shows": "One question — whether a reader can copy their private record of where their memory slips off the phone, and by which of three means — drawn at phone size on a sample map that carries verse references only, with what each option takes, gets and costs.", + "decision": "confusion-map-export", + "page": null, + "builtBy": null, + "note": null + }, + { + "url": "https://claude.ai/code/artifact/b0b6d0b5-5bec-4309-8dc4-701ab767e4a1", + "title": "Where You Slip", + "published": "2026-08-29", + "shows": "An eight-screen walkthrough, at phone size and in English, of how the app would record the exact place a reader's memory slips from one verse onto a look-alike, mark it quietly on the page for later, and let the reader compare the two and beat it — with the states a slip moves through and the four open questions the feature still holds.", + "decision": null, + "page": "docs/design/where-you-slip.html", + "builtBy": null, + "note": "The design walkthrough the confusion-points feature is being reviewed against, and the page the owner has been leaving feedback on. builtBy is null on purpose: this page is written by hand, not generated from anything, so the checked-in copy is itself the source — there is nothing to rebuild it from. Not a decision page; it draws the whole feature, and the individual questions it raises become their own decision rows as they are opened." + }, + { + "url": "https://claude.ai/code/artifact/84b4bfe6-f875-4186-9d32-c37f26ad71be", + "title": "Where should the sittings live?", + "published": "2026-08-17", + "shows": "One real question from a checking session drawn at phone size, the measured cost of the way those sessions run today, and three options each drawn as the journey a single answer takes from a reader's finger to the project — now carrying the answer, which was the first of them.", + "decision": "sitting-hosting", + "page": "docs/design/sitting-hosting.html", + "builtBy": "scripts/build-sitting-hosting-options.mjs", + "note": "Republished on the day it was opened, once it was settled. The two options that lost are left on the page unchanged rather than trimmed away: they are why the choice was a choice, and the page is now the only place that says the choice was made without looking at what anybody outside this project does." + }, + { + "url": "https://claude.ai/code/artifact/9a42955e-4c2f-4dc9-8c95-a50942c46a00", + "title": "Hifth Decision Board", + "published": "2026-08-17", + "shows": "Every question this project has had to answer, on one time line, with the constraints running between them and every page it has published listed underneath.", + "decision": null, + "page": "docs/design/decision-board.html", + "builtBy": "scripts/build-decision-board.mjs", + "note": null + }, + { + "url": "https://claude.ai/code/artifact/b7be0f22-3ff5-45b7-a5c1-850dd73ef1f1", + "title": "What should the panel show around the ayah?", + "published": "2026-08-17", + "shows": "Five ways the look-alike panel could stop showing neighbouring verses as though they were part of the match, each drawn on the real printed page at the size it would be used.", + "decision": "comparison-crop", + "page": null, + "builtBy": null, + "note": null + }, + { + "url": "https://claude.ai/code/artifact/7652b2f5-61a1-4072-bfab-ef3b649e55f5", + "title": "Where the rectangles go", + "published": "2026-08-15", + "shows": "Six ways to line up the rectangles the app draws over a page of the mus'haf, each drawn on a real page at the size a phone uses.", + "decision": "mark-placement", + "page": null, + "builtBy": null, + "note": null + }, + { + "url": "https://claude.ai/code/artifact/e4f6003d-de26-44b4-9545-326e2f2f7c8c", + "title": "Registered on the Ornaments", + "published": "2026-08-13", + "shows": "Why the boxes drawn around tajweed marks sit off the ink: what was measured, the same mistake found named in medical imaging and in typesetting, and the plan that came out of it.", + "decision": null, + "page": null, + "builtBy": null, + "note": "No copy anywhere. It is the diagnosis that the still-open question about lining up the rectangles grew out of, and it holds two things that question's own page does not: the count showing not one mark in three hundred thousand is named wrongly, and the reason the alignment can report itself excellent while being a whole mark-height out. When that question is settled, its record should absorb both, and this row should point at a page instead of at nothing." + }, + { + "url": "https://claude.ai/code/artifact/5f230dff-e4e8-40c6-9c4e-888786eeef9b", + "title": "One colour a verse — should Hifth colour tajweed more finely?", + "published": "2026-08-08", + "shows": "Three choices for how finely the app colours tajweed, drawn on a real page, with what printed mus'hafs already do about it.", + "decision": "mark-granularity", + "page": null, + "builtBy": null, + "note": null + }, + { + "url": "https://claude.ai/code/artifact/74b37363-d082-45e7-9651-54c47c328844", + "title": "Whose colours are they? — can a reader recolour Hifth's tajweed marks", + "published": "2026-08-08", + "shows": "Four choices for how much of the app's tajweed colouring a reader is allowed to change, each drawn on a real page.", + "decision": "tajweed-colours", + "page": null, + "builtBy": null, + "note": null + }, + { + "url": "https://claude.ai/code/artifact/085bf1d2-e4df-4eb0-a2d7-ee7da65f5dbb", + "title": "Two spellings, one word", + "published": "2026-08-07", + "shows": "Why two machine readings of the same printed page disagree about ten verses, and why a reader looking at the page would see nothing different.", + "decision": null, + "page": null, + "builtBy": null, + "note": "No copy anywhere. It settles a question — the disagreement is in the two readings, not in the print — but it was never written down as a decision, so the finding survives only at this address. It belongs in the record that covers where the two readings of the text come from." + }, + { + "url": "https://claude.ai/code/artifact/27493ed0-8f09-4ea4-b778-85377014e820", + "title": "Two segmentations, one text — PLAN ⑬", + "published": "2026-08-04", + "shows": "Two different ways of cutting the same text into words, how far the two agree, and where that agreement ought to be kept.", + "decision": null, + "page": null, + "builtBy": null, + "note": "No copy anywhere, and its title is written in this project's own shorthand, which is the other half of the same problem: a page nobody outside can name is one nobody outside can use. It predates the rule that a decision page must be checked in." + }, + { + "url": "https://claude.ai/code/artifact/b4e0d26b-fb7d-430f-9f16-de815d4ceae1", + "title": "Should the two leaves pan and zoom together?", + "published": "2026-08-04", + "shows": "Three screenshots of the running app comparing what zooming into one leaf does to a two-page spread, with a recommendation.", + "decision": null, + "page": null, + "builtBy": null, + "note": "No copy anywhere. This one is a decision in everything but filing: the title is the question, the page puts options, and it carries a recommendation. What is missing is the record — the reasons, who chose, and when — so it should become a row in the decision register rather than stay here." + } + ] +} diff --git a/docs/decisions.json b/docs/decisions.json index a5bd335d..f424c781 100644 --- a/docs/decisions.json +++ b/docs/decisions.json @@ -67,10 +67,97 @@ "resolve, or if a record exists in docs/decisions/ with no row here." ], "decisions": [ + { + "id": "juz-detents", + "question": "When a reader lets go of the page bar near a juz marker, should the bar pull the page onto it?", + "status": "open", + "options": [ + { + "id": "A", + "label": "A marker only marks" + }, + { + "id": "B", + "label": "A marker pulls, a few pages either side" + }, + { + "id": "C", + "label": "A marker is a button; the drag is unchanged" + } + ], + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/page-bar-options.html", + "page": "docs/design/page-bar-options.html", + "builtBy": "scripts/build-page-bar-options.mjs", + "doc": "docs/decisions/page-bar.md#when-a-reader-lets-go-near-a-marker-should-the-bar-pull-the-page-onto-it", + "decided": null, + "by": null, + "date": null, + "related": [ + "boundary-juz", + "desktop-vs-mobile" + ] + }, + { + "id": "boundary-juz", + "question": "When a juz begins partway down a page, which juz is that page in?", + "status": "open", + "options": [ + { + "id": "A", + "label": "The juz that begins on it" + }, + { + "id": "B", + "label": "The juz that was already running" + }, + { + "id": "C", + "label": "Both, on those four pages" + } + ], + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/page-bar-options.html", + "page": "docs/design/page-bar-options.html", + "builtBy": "scripts/build-page-bar-options.mjs", + "doc": "docs/decisions/page-bar.md#when-a-juz-begins-partway-down-a-page-which-juz-is-that-page-in", + "decided": null, + "by": null, + "date": null, + "related": [ + "juz-detents", + "desktop-vs-mobile" + ] + }, + { + "id": "confusion-map-export", + "question": "Should a reader be able to copy their confusion map off the phone — and if so, by which means?", + "status": "open", + "options": [ + { + "id": "A", + "label": "Keep it on the phone only" + }, + { + "id": "B", + "label": "A file the reader saves and restores" + }, + { + "id": "C", + "label": "A cloud-sync switch the reader controls" + } + ], + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/confusion-map-export.html", + "page": "docs/design/confusion-map-export.html", + "builtBy": "scripts/build-confusion-export-options.mjs", + "doc": "docs/decisions/confusion-map-export.md", + "decided": null, + "by": null, + "date": null, + "related": [] + }, { "id": "mark-granularity", "question": "Should the app colour tajweed one colour a verse, or should it colour the exact letter a rule lands on?", - "status": "open", + "status": "decided", "options": [ { "id": "A", @@ -85,23 +172,140 @@ "label": "Prepare the ground, colour nothing yet" } ], - "artifact": "https://claude.ai/code/artifact/5f230dff-e4e8-40c6-9c4e-888786eeef9b", + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/mark-granularity.html", "page": "docs/design/mark-granularity.html", "builtBy": "scripts/build-mark-options.mjs", - "doc": "docs/design/sub-word-marks.md", + "doc": "docs/decisions/mark-granularity.md", + "decided": "B", + "by": "omar", + "date": "2026-08-19", + "related": [ + "word-selection", + "loop-6a", + "tajweed-colours", + "mark-placement" + ] + }, + { + "id": "mark-placement", + "question": "Should the app line up what it draws one page at a time, or one printed line at a time?", + "status": "open", + "options": [ + { + "id": "A", + "label": "Leave the rectangles where they are" + }, + { + "id": "B", + "label": "Line each page up as a whole" + }, + { + "id": "F", + "label": "Line each printed line up on its own, and let it tilt" + }, + { + "id": "G", + "label": "Line the marks up, and leave the words where they are" + }, + { + "id": "I", + "label": "Line each printed line up on its own, and let it bend" + }, + { + "id": "H", + "label": "Put each mark where its own ink is, and line the rest up" + } + ], + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/mark-placement.html", + "page": "docs/design/mark-placement.html", + "builtBy": "scripts/build-placement-options.mjs", + "doc": "docs/design/mark-registration.md", "decided": null, "by": null, "date": null, "related": [ + "mark-granularity", "word-selection", - "loop-6a", - "tajweed-colours" + "comparison-crop", + "sitting-hosting" + ] + }, + { + "id": "sitting-hosting", + "question": "When somebody sits down to check the app's rectangles against the printed page, where should those questions come from — a laptop at home, or a link they can open anywhere?", + "status": "decided", + "options": [ + { + "id": "A", + "label": "Leave it as it is — one laptop hands out the sittings over a private network" + }, + { + "id": "B", + "label": "Publish each sitting as a page, and hand the answers over at the end" + }, + { + "id": "C", + "label": "Publish each sitting as a page, and let it bank each answer to a hosted table" + } + ], + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/sitting-hosting.html", + "page": "docs/design/sitting-hosting.html", + "builtBy": "scripts/build-sitting-hosting-options.mjs", + "doc": "docs/decisions/sitting-hosting.md", + "decided": "A", + "by": "omar", + "date": "2026-08-17", + "related": [ + "mark-placement" + ] + }, + { + "id": "comparison-crop", + "question": "When the app shows a verse cut out of the printed page, what should it do about the neighbouring verses that come with it?", + "status": "decided", + "options": [ + { + "id": "A", + "label": "Leave the crop as it is" + }, + { + "id": "B", + "label": "Cut each line down to the verse's own words" + }, + { + "id": "C", + "label": "Fade everything that is not this verse" + }, + { + "id": "E", + "label": "Mark where the verse begins and ends" + }, + { + "id": "D", + "label": "Mark the words the two verses share, as well as the ones they do not" + }, + { + "id": "F", + "label": "Fade the neighbours, mark the shared words green and the differing words yellow" + } + ], + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/comparison-crop.html", + "page": "docs/design/comparison-crop.html", + "builtBy": "scripts/build-crop-options.mjs", + "doc": "docs/decisions/comparison-crop.md", + "decided": "F", + "by": "omar", + "date": "2026-08-16", + "related": [ + "mark-placement", + "word-selection", + "loop-4a" ] }, { "id": "tajweed-colours", "question": "Should a reader be able to change the colours the app uses for recitation rules, and how many colours are there to change?", - "status": "open", + "status": "decided", "options": [ { "id": "A", @@ -120,13 +324,13 @@ "label": "Offer a few ready-made schemes, no picker" } ], - "artifact": "https://claude.ai/code/artifact/74b37363-d082-45e7-9651-54c47c328844", + "artifact": "https://blog.bytesofpurpose.com/hifth/docs/design/tajweed-colours.html", "page": "docs/design/tajweed-colours.html", "builtBy": "scripts/build-palette-options.mjs", "doc": "docs/decisions/tajweed-colours.md", - "decided": null, - "by": null, - "date": null, + "decided": "B", + "by": "omar", + "date": "2026-08-19", "related": [ "mark-granularity", "loop-6a" @@ -138,7 +342,10 @@ "status": "living", "doc": "docs/decisions/desktop-vs-mobile.md", "related": [ - "wheel-and-zoom" + "wheel-and-zoom", + "spread-zoom", + "juz-detents", + "boundary-juz" ] }, { @@ -149,6 +356,34 @@ "by": "omar", "date": "2026-08-06", "related": [ + "desktop-vs-mobile", + "spread-zoom" + ] + }, + { + "id": "spread-zoom", + "question": "When two pages are open, should the zoom buttons enlarge both pages together?", + "status": "decided", + "options": [ + { + "id": "off", + "label": "Leave magnifying switched off while two pages show" + }, + { + "id": "close-first", + "label": "Close to one page automatically, then magnify" + }, + { + "id": "both", + "label": "Magnify both pages together, to the same size" + } + ], + "doc": "docs/decisions/spread-zoom.md", + "decided": "both", + "by": "omar", + "date": "2026-09-01", + "related": [ + "wheel-and-zoom", "desktop-vs-mobile" ] }, @@ -173,7 +408,9 @@ "date": "2026-08-04", "related": [ "word-search", - "mark-granularity" + "mark-granularity", + "mark-placement", + "comparison-crop" ] }, { @@ -236,7 +473,8 @@ "date": "2026-07-25", "related": [ "loop-4b", - "word-search" + "word-search", + "comparison-crop" ] }, { diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 1920c81f..7b768410 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -1,4 +1,4 @@ - + # Decisions @@ -11,14 +11,16 @@ open, is a page you can open in a browser and look at: the options drawn on a re the mus'haf, with no code in it. You do not need to have opened this repository to take part in an open decision, and that is on purpose. -## Open — 2 +## Open — 4 Nobody has chosen yet. Each one has a page you can open and look at without reading any code. | question | options | look at it | record | | --- | --- | --- | --- | -| Should the app colour tajweed one colour a verse, or should it colour the exact letter a rule lands on? | **A** Leave it as it is — one colour a verse
    **B** Colour the exact letter or mark
    **C** Prepare the ground, colour nothing yet | [open](https://claude.ai/code/artifact/5f230dff-e4e8-40c6-9c4e-888786eeef9b) · [in repo](../design/mark-granularity.html) | [Sub-word marks: the level below a word, and the print that already named it](../design/sub-word-marks.md) | -| Should a reader be able to change the colours the app uses for recitation rules, and how many colours are there to change? | **A** Leave the colours fixed
    **B** A reader recolours the seven groups
    **C** A reader recolours all eighteen rules
    **D** Offer a few ready-made schemes, no picker | [open](https://claude.ai/code/artifact/74b37363-d082-45e7-9651-54c47c328844) · [in repo](../design/tajweed-colours.html) | [Whose colours are they?](tajweed-colours.md) | +| When a reader lets go of the page bar near a juz marker, should the bar pull the page onto it? | **A** A marker only marks
    **B** A marker pulls, a few pages either side
    **C** A marker is a button; the drag is unchanged | [open](https://blog.bytesofpurpose.com/hifth/docs/design/page-bar-options.html) · [in repo](../design/page-bar-options.html) | [Two questions about the page bar: does a juz marker pull, and whose juz is a boundary page?](page-bar.md#when-a-reader-lets-go-near-a-marker-should-the-bar-pull-the-page-onto-it) | +| When a juz begins partway down a page, which juz is that page in? | **A** The juz that begins on it
    **B** The juz that was already running
    **C** Both, on those four pages | [open](https://blog.bytesofpurpose.com/hifth/docs/design/page-bar-options.html) · [in repo](../design/page-bar-options.html) | [Two questions about the page bar: does a juz marker pull, and whose juz is a boundary page?](page-bar.md#when-a-juz-begins-partway-down-a-page-which-juz-is-that-page-in) | +| Should a reader be able to copy their confusion map off the phone — and if so, by which means? | **A** Keep it on the phone only
    **B** A file the reader saves and restores
    **C** A cloud-sync switch the reader controls | [open](https://blog.bytesofpurpose.com/hifth/docs/design/confusion-map-export.html) · [in repo](../design/confusion-map-export.html) | [Should a reader be able to copy their confusion map off the phone?](confusion-map-export.md) | +| Should the app line up what it draws one page at a time, or one printed line at a time? | **A** Leave the rectangles where they are
    **B** Line each page up as a whole
    **F** Line each printed line up on its own, and let it tilt
    **G** Line the marks up, and leave the words where they are
    **I** Line each printed line up on its own, and let it bend
    **H** Put each mark where its own ink is, and line the rest up | [open](https://blog.bytesofpurpose.com/hifth/docs/design/mark-placement.html) · [in repo](../design/mark-placement.html) | [Are the marks in the right place, and how would we know?](../design/mark-registration.md) | ## Living records — 1 @@ -28,13 +30,18 @@ Never finished. A new decision on one of these subjects gets a row appended, not | --- | --- | | When should a wide window behave differently from a phone? | [Desktop vs mobile — the running record](desktop-vs-mobile.md) | -## Decided — 14 +## Decided — 19 The record holds the reasons. Read it before changing anything it covers. | question | what was decided | when | | --- | --- | --- | +| Should the app colour tajweed one colour a verse, or should it colour the exact letter a rule lands on? | [One colour a verse, or the letter itself?](mark-granularity.md) (option **B**) | omar, 2026-08-19 | +| When somebody sits down to check the app's rectangles against the printed page, where should those questions come from — a laptop at home, or a link they can open anywhere? | [The instrument needs a house, and today it is one laptop](sitting-hosting.md) (option **A**) | omar, 2026-08-17 | +| When the app shows a verse cut out of the printed page, what should it do about the neighbouring verses that come with it? | [What should the panel show around the ayah?](comparison-crop.md) (option **F**) | omar, 2026-08-16 | +| Should a reader be able to change the colours the app uses for recitation rules, and how many colours are there to change? | [Whose colours are they?](tajweed-colours.md) (option **B**) | omar, 2026-08-19 | | Should rolling the mouse wheel magnify the page? | [The wheel navigates, buttons magnify](wheel-and-zoom.md) | omar, 2026-08-06 | +| When two pages are open, should the zoom buttons enlarge both pages together? | [When two pages are open, do the zoom buttons grow both together?](spread-zoom.md) (option **both**) | omar, 2026-09-01 | | When a reader picks out a few words, what should the app go looking for? | [A run of words asks a question, and the answer is a number](word-search.md) | omar, 2026-08-05 | | Should a reader be able to pick single words, or only whole verses? | [The grain gets finer where the finger already is](word-selection.md) | omar, 2026-08-04 | | How does the app keep a page it was told to save without storing it twice? | [One bit of provenance, and the line it does not cross](pin-marker.md) | omar, 2026-08-04 | @@ -55,16 +62,22 @@ An earlier answer usually rules out part of a later question, and the reason it the record, not here. Read the ones listed beside a question before answering it: that is where the option you were about to invent has often already been ruled out. -- **Should the app colour tajweed one colour a verse, or should it colour the exact letter a rule lands on?**
    reads with: [The grain gets finer where the finger already is](word-selection.md) · [Loop 6a — Skin, editions, wayfinding, offline foundation](loop-6a.md) · [Whose colours are they?](tajweed-colours.md) -- **Should a reader be able to change the colours the app uses for recitation rules, and how many colours are there to change?**
    reads with: [Sub-word marks: the level below a word, and the print that already named it](../design/sub-word-marks.md) · [Loop 6a — Skin, editions, wayfinding, offline foundation](loop-6a.md) -- **When should a wide window behave differently from a phone?**
    reads with: [The wheel navigates, buttons magnify](wheel-and-zoom.md) -- **Should rolling the mouse wheel magnify the page?**
    reads with: [Desktop vs mobile — the running record](desktop-vs-mobile.md) +- **When a reader lets go of the page bar near a juz marker, should the bar pull the page onto it?**
    reads with: [Two questions about the page bar: does a juz marker pull, and whose juz is a boundary page?](page-bar.md#when-a-juz-begins-partway-down-a-page-which-juz-is-that-page-in) · [Desktop vs mobile — the running record](desktop-vs-mobile.md) +- **When a juz begins partway down a page, which juz is that page in?**
    reads with: [Two questions about the page bar: does a juz marker pull, and whose juz is a boundary page?](page-bar.md#when-a-reader-lets-go-near-a-marker-should-the-bar-pull-the-page-onto-it) · [Desktop vs mobile — the running record](desktop-vs-mobile.md) +- **Should the app colour tajweed one colour a verse, or should it colour the exact letter a rule lands on?**
    reads with: [The grain gets finer where the finger already is](word-selection.md) · [Loop 6a — Skin, editions, wayfinding, offline foundation](loop-6a.md) · [Whose colours are they?](tajweed-colours.md) · [Are the marks in the right place, and how would we know?](../design/mark-registration.md) +- **Should the app line up what it draws one page at a time, or one printed line at a time?**
    reads with: [One colour a verse, or the letter itself?](mark-granularity.md) · [The grain gets finer where the finger already is](word-selection.md) · [What should the panel show around the ayah?](comparison-crop.md) · [The instrument needs a house, and today it is one laptop](sitting-hosting.md) +- **When somebody sits down to check the app's rectangles against the printed page, where should those questions come from — a laptop at home, or a link they can open anywhere?**
    reads with: [Are the marks in the right place, and how would we know?](../design/mark-registration.md) +- **When the app shows a verse cut out of the printed page, what should it do about the neighbouring verses that come with it?**
    reads with: [Are the marks in the right place, and how would we know?](../design/mark-registration.md) · [The grain gets finer where the finger already is](word-selection.md) · [Loop 4a — Edge-data ETL (full mutashabihat corpus)](loop-4a.md) +- **Should a reader be able to change the colours the app uses for recitation rules, and how many colours are there to change?**
    reads with: [One colour a verse, or the letter itself?](mark-granularity.md) · [Loop 6a — Skin, editions, wayfinding, offline foundation](loop-6a.md) +- **When should a wide window behave differently from a phone?**
    reads with: [The wheel navigates, buttons magnify](wheel-and-zoom.md) · [When two pages are open, do the zoom buttons grow both together?](spread-zoom.md) · [Two questions about the page bar: does a juz marker pull, and whose juz is a boundary page?](page-bar.md#when-a-reader-lets-go-near-a-marker-should-the-bar-pull-the-page-onto-it) · [Two questions about the page bar: does a juz marker pull, and whose juz is a boundary page?](page-bar.md#when-a-juz-begins-partway-down-a-page-which-juz-is-that-page-in) +- **Should rolling the mouse wheel magnify the page?**
    reads with: [Desktop vs mobile — the running record](desktop-vs-mobile.md) · [When two pages are open, do the zoom buttons grow both together?](spread-zoom.md) +- **When two pages are open, should the zoom buttons enlarge both pages together?**
    reads with: [The wheel navigates, buttons magnify](wheel-and-zoom.md) · [Desktop vs mobile — the running record](desktop-vs-mobile.md) - **When a reader picks out a few words, what should the app go looking for?**
    reads with: [The grain gets finer where the finger already is](word-selection.md) · [Loop 4a — Edge-data ETL (full mutashabihat corpus)](loop-4a.md) -- **Should a reader be able to pick single words, or only whole verses?**
    reads with: [A run of words asks a question, and the answer is a number](word-search.md) · [Sub-word marks: the level below a word, and the print that already named it](../design/sub-word-marks.md) +- **Should a reader be able to pick single words, or only whole verses?**
    reads with: [A run of words asks a question, and the answer is a number](word-search.md) · [One colour a verse, or the letter itself?](mark-granularity.md) · [Are the marks in the right place, and how would we know?](../design/mark-registration.md) · [What should the panel show around the ayah?](comparison-crop.md) - **How does the app keep a page it was told to save without storing it twice?**
    reads with: [Loop 6b — Pin-a-juz packs](loop-6b.md) -- **Where does the list of look-alike verses come from, and is it the same list every time it is built?**
    reads with: [Loop 4b — Page corpus + streaming (all 604 pages)](loop-4b.md) · [A run of words asks a question, and the answer is a number](word-search.md) +- **Where does the list of look-alike verses come from, and is it the same list every time it is built?**
    reads with: [Loop 4b — Page corpus + streaming (all 604 pages)](loop-4b.md) · [A run of words asks a question, and the answer is a number](word-search.md) · [What should the panel show around the ayah?](comparison-crop.md) - **Can the app carry the whole mus'haf instead of three pages?**
    reads with: [Loop 4a — Edge-data ETL (full mutashabihat corpus)](loop-4a.md) -- **Can a reader turn the colouring off, jump anywhere in the book, and reopen it with no signal?**
    reads with: [Sub-word marks: the level below a word, and the print that already named it](../design/sub-word-marks.md) · [Loop 6b — Pin-a-juz packs](loop-6b.md) · [Whose colours are they?](tajweed-colours.md) +- **Can a reader turn the colouring off, jump anywhere in the book, and reopen it with no signal?**
    reads with: [One colour a verse, or the letter itself?](mark-granularity.md) · [Loop 6b — Pin-a-juz packs](loop-6b.md) · [Whose colours are they?](tajweed-colours.md) - **Can a reader keep one thirtieth of the book on their phone for good?**
    reads with: [One bit of provenance, and the line it does not cross](pin-marker.md) · [Loop 6a — Skin, editions, wayfinding, offline foundation](loop-6a.md) --- diff --git a/docs/decisions/comparison-crop.md b/docs/decisions/comparison-crop.md new file mode 100644 index 00000000..a51b874a --- /dev/null +++ b/docs/decisions/comparison-crop.md @@ -0,0 +1,193 @@ +# What should the panel show around the ayah? + +**Status:** decided — **option F**, by omar on 2026-08-16. + +**Picture:** (an earlier copy on another host: ) — six options, each +drawn on the real artwork at the panel's real width, with the chosen one drawn again enlarged. Checked +in as `comparison-crop.html`, rebuilt by `node scripts/build-crop-options.mjs`, which writes that copy +and the published one from the same pass. + +Read the picture first. This file is the reasons; the page is the subject, and no paragraph here +substitutes for seeing a crop at the size a reader actually gets it. + +## What this is + +The look-alike panel used to show its two ayahs as retyped text out of a hand-built table. That table +is gone — it was the larger of the two sites holding running scripture, and it was wrong on its own +terms besides, because its spelling did not match the mus'haf the reader was looking at. The panel now +crops both ayahs out of the printed page. One set of letters, and 2,544 pairs covered where the table +covered twelve. + +An ayah almost never begins and ends at the edge of a printed line, so the rectangle cut around it +carries whatever else sits on those lines. The wash marks the words the two ayahs do not share. It +does not, and cannot, mark the difference between *the rest of this ayah, which the two have in +common* and *a different ayah that happens to share a line*. A reader can take unmarked for shared. + +That is the whole question. Which words get washed is read off the edge's `span`/`toSpan` and is +correct; it is not reopened here. + +## The cost of leaving it + +Measured over every drawable side of every pair — 5,088 crops, by `scripts/build-crop-options.mjs`, +which recomputes the figures on every build so the page cannot drift from the tree: + +| | | +| --- | --- | +| mean share of a crop that is the ayah it names | 69.8% | +| median | 70.2% | +| crops that are more neighbour than ayah | 820 (16.1%) | +| under a third | 220 (4.3%) | +| worst | 10.3% — 80:36 on page 585 | +| single-line crops, which are already perfect | 584 | +| two-line crops, the worst class | 60.7% mean | + +Two of those matter more than the average. The single-line crops contain nothing but their ayah and +whatever is decided must not disturb them. And the worst case is not the longest ayah but the shortest +multi-line one: both of its lines are partial and there is no full line in the middle to dilute the +edges. + +The sharpest statement of the problem is not a number. On the app's own signature pair, the crop +labelled 2:123 has its entire first line occupied by 2:122, ending in a printed ayah marker, with +nothing marking it — the ayah the label names does not start until line two. That is on the page, +under option A, at the size it ships. + +## Prior art + +The composite — cropping an ayah out of a printed page and setting it beside its look-alike — I did +not find anyone doing. The largest public library of Quran data publishes the halves separately and has +not joined them: [twenty approved mushaf layouts](https://qul.tarteel.ai/resources/mushaf-layout) in +one place, [5,277 mutashabihat entries](https://qul.tarteel.ai/resources/mutashabihat) and +[4,001 similar-ayah links](https://qul.tarteel.ai/resources/similar-ayah) in another. quran.com shows +no look-alike feature on an ayah's own page. + +The sub-problem — marking a run of text that wraps across lines of a page — is solved, and four +independent traditions solve it the same way: **per line, never by a box around the whole run.** + +- [CSS Pseudo-Elements 4 §3.4](https://www.w3.org/TR/css-pseudo-4/#highlight-bounds) — a highlight is a + single overlay for the document, and "Each box owns the piece of the overlay" for the text inside it. + Every text selection in every browser is per-fragment. +- [CSSOM View](https://drafts.csswg.org/cssom-view/) — the platform names the two shapes separately: + a list with "one for each box fragment", or "the smallest rectangle that includes all of the + rectangles in list". `DiffView` is calling the second one. +- [W3C Web Annotation Data Model](https://www.w3.org/TR/annotation-model/) — the rectangle selector is + explicitly limited ("even a simple circular region of an image, or a diagonal line across it, are not + possible") and the spec points at an SVG selector when the region is not a rectangle. +- [IIIF Image API 3.0 §4.1](https://iiif.io/api/image/3.0/) — region requests are rectangle-only, with + no multi-part form. Worth naming because it explains why our crop is a rectangle: a rectangle is what + the tooling hands you, not what the content is. +- [hOCR 1.2](https://kba.github.io/hocr-spec/1.2/) and the + [ALTO 4.4 schema](https://github.com/altoxml/schema) — both put geometry on the text *line*; logical + units group lines rather than having shapes of their own. ALTO's `Shape`/`Polygon` is documented as + describing "the bounding shape of a block, if it is not rectangular", which is exactly an ayah + crossing three lines. + +This changed the reading of option B: it is the conventional answer, not the bold one, and its +remaining objection is appearance rather than correctness — none of those precedents has to look like a +page of a mus'haf. F was chosen over it anyway, for the reason given above: the precedents all answer +*where to draw* and none answers *what the drawing means*. + +**Not confirmed, and not counted above.** A multi-line PDF highlight is, I believe, stored as several +quadrilaterals rather than one rectangle — the same convention a fifth time — but both specification +sources I tried returned nothing and this session's search budget was spent. WebSearch was exhausted +at 200/200, so all of the above is primary-source fetching against known URLs rather than a search; +`loc.gov`'s ALTO page returned 403 and the finding was recovered from the schema repository instead. I +also could not open QUL's layout or look-alike data files, so that finding rests on their own +descriptions of them. + +## What constrains it + +- **The printed page is never edited.** Any option draws *over* the artwork. Re-flowing or re-setting + letters is off the table — avoiding exactly that is why the retyped table went. +- **The page is never re-themed** (`tajweed-colours`, and the app has no dark theme at all). The + printer's ink is a fixed dark; every specimen on the page is on paper even at night, deliberately. +- **The wash stays translucent.** Nothing solid goes over the ink. +- **The no-edge fallback stays.** No `span`, no shard, or no page → nothing is drawn and the hop row + keeps its plain note. Every option must keep it. +- **`mark-placement` is the same tension in a second place** — whether a rectangle should register + against a printed line or a whole page. The two should not be answered in opposite directions + without somebody saying why. Both are open. + +## The options + +Grouped by what they do about the neighbours. They were never exclusive — and F, which won, is the +proof: it is C and D composed, plus a change neither parent asked for. + +| | | | +| --- | --- | --- | +| **A** | Leave it as it is | The rectangle as it ships. Zero work, and the only option with no precedent behind it. | +| **B** | Cut each line down to the ayah's own words | One strip per line, in place. The problem disappears by construction; it is what everything else does; it is ragged. | +| **C** | Fade everything that is not this ayah | One rectangle, a veil over the neighbours. Keeps context, puts a scrim over printed Quran. | +| **E** | Mark where the ayah begins and ends | Two brackets, nothing hidden. Lightest intervention; states the boundary without quieting anything. | +| **D** | Mark the shared words too | A third neutral tint, so plain means *not part of the comparison*. Fixes the inference rather than its cause; needs a legend there is no room for. | +| **F ✓** | C and D at once, green for shared and yellow for divergent | Every state named. Costs the per-side colour identity, and reopens what the two washes mean. | + +Implementation for each is a change to `apps/web/src/components/DiffView.tsx` alone; B is the only one +that would have changed the crop from one element into several. `WordIndex.bandsFor` already returns +the per-line rectangles — it is what every option here is drawn from, including A's union. + +## What was decided, and why + +**Option F.** The neighbours veiled, both meaningful states marked, and the two tints re-assigned from +*which ayah you are looking at* to *what these words are*: green where the two agree, yellow where they +part, identically on both halves of the panel. + +The colour re-assignment is the substantive half and it is worth stating why it is not a side effect. +Which ayah is which was already written above each crop in words, so the colour was spending itself on +something the reader could already read. Whether a phrase is shared or divergent was written nowhere — +and it is the single thing a hafiz opens this panel to learn. + +The prior art below pointed at B, and B lost anyway. The reason: every precedent solves *where to +draw*, and none of them solves *what the drawing means*. B removes the ambiguity by removing the +neighbours; F removes it by naming all three states outright. On a page somebody is memorising from, +saying what a mark means beat inheriting a convention about its shape. + +**What it costs, recorded rather than buried.** The panel's two halves no longer differ by colour, so a +crop glanced at without its label has lost a cue. Yellow is the hardest colour to lay on cream and is +pushed to an ochre to survive at a fifth opacity over ink; a reader may fairly say that is not yellow. +The green is deliberately a leaf green rather than the app's verdigris, because verdigris is one of the +two colours F retires and reusing it would carry the old meaning. And F is the busiest of the six: it +inherits C's objection, that a veil goes over printed Quran, and D's, that most of the crop ends up +under colour. + +**It supersedes something this record had listed as settled.** The two wash colours were chosen +elsewhere, and F changes not merely which they are but what they are about. That is part of the +decision, not a consequence of it. + +## Still to build + +`DiffView` draws option A today. F needs, in that component alone: the evenodd scrim path over the +frame minus the bands, a green band over the shared range, a yellow band over each divergent run, and +the two new tokens. `divergentRuns` and `WordIndex.bandsFor` already return everything it needs — the +options page draws F from exactly those calls, so the geometry is proven before the component moves. + +## Rejected + +- **Show the whole page and point at the ayah.** The panel is a strip under a row in a list. A whole + page at that size is unreadable, and the reader already has one. +- **Re-flow the ayah onto its own line.** Moves the printer's letters. That is the defect cropping was + brought in to fix. +- **Blur the neighbours.** Same objection, and harder to defend over printed Quran than a tint of equal + strength. +- **Draw only the differing words.** The shared opening is *why* the pair is confusable; remove it and + the panel stops answering its own question. + +## What would reopen it + +- **A hafiz saying the veil gets in the way.** Everything against A rested on a guess about somebody + else's habit — that the neighbours distract rather than help — and F acts on that guess by veiling + them. Still tested on nobody who memorises. This is the assumption most likely to be wrong. +- **Green and yellow failing on paper rather than on screen.** Yellow at a fifth opacity over ink on + cream is the hardest thing here, and it has been judged only at the sizes on the page. +- **Anyone reading green and yellow as right and wrong.** They mean *the same* and *not the same*. If + readers take yellow for an error, the palette is wrong even though the structure is right. +- **The panel leaving the list row.** F is busy because the crop is small; on a full screen the veil + could be lighter and the tints weaker, and this balance would want revisiting. +- **Any change to how the page is themed.** The veil is drawn in the printed page's own paper colour. + +## Not settled here + +Which words get marked — read off the pair, verified, correct; F changes what the marks look like and +mean, never which words get them. The colours everywhere else in the app: terracotta and verdigris keep +every other job they have, and only this panel changes. The exact green and the exact yellow, chosen to +be legible over ink on cream and open to argument without reopening the decision. Whether the panel +should exist. Anything about the vendored artwork. diff --git a/docs/decisions/confusion-map-export.md b/docs/decisions/confusion-map-export.md new file mode 100644 index 00000000..16291349 --- /dev/null +++ b/docs/decisions/confusion-map-export.md @@ -0,0 +1,174 @@ +# Should a reader be able to copy their confusion map off the phone? + +*Status: open. This record states the question and draws the options; it does not choose.* + +**The picture:** (an earlier copy on another host: ) — the three +options drawn at phone size, on a sample map that carries verse references only and no scripture. +Checked in as `confusion-map-export.html`, rebuilt by `scripts/build-confusion-export-options.mjs` +from the committed data beside it. + +## A few words, defined once + +- **Confusion map** — the private record this feature keeps of the exact places one reader's + memory slips from a verse onto a similar-sounding one. It is built up, slip by slip, over + months of revision, and it is personal: it is a map of where *this* reader is weak. +- **A slip** — the moment the tongue takes a wrong turn onto a verse that opens the same way. + Recording one is what fills the map. +- **On the phone only** — the app keeps everything in the phone's own private storage and + sends nothing anywhere. This is how everything the app remembers works today. + +## What is being decided? + +Should the confusion map be something a reader can copy off their phone — to keep it safe, or +to move it to a new phone — and if so, by which of two very different means: a file they save +themselves, or an automatic copy to a cloud service? + +A reader could answer with one of the options below. + +## Why is this being asked now? + +Reviewing the feature's walkthrough, the app's owner asked for exactly this: that the map be +durable enough to survive clearing the phone's browser data, "with the option of backing up to +a downloaded file and re-uploading," and — one day, once there is a phone app — saved to the +cloud. That is a direct request, and answering it means first sharpening the stance the app +has leaned on everywhere else — from a flat *nothing leaves the phone* to *nothing leaves unless +it is in the reader's interest, and under their control* (see below). Changing a stance the whole +app rests on is decided in the open, not built quietly. + +## What happens if nobody decides? + +The map stays on the phone only, and that has a real cost the owner has already named: the map +is the distilled product of months of revision, and the phone can wipe it silently. On one +common phone, a web app that has not been opened in about a week can have its stored data +cleared by the system with no warning and no way to get it back. Installing the app to the home +screen makes that far less likely, but "far less likely" is not "safe," and the whole point of +the map is that a reader comes to rely on it. + +Nothing else is blocked behind this. The feature can be built, and used, with the map on the +phone only; this decision changes how well it survives, not whether it works. So it can sit open +without stopping other work — but every week it sits open is a week a reader could lose the map. + +## What does the app do today, and what is it costing? + +Today the app keeps everything on the phone and offers no way to copy anything off it — no +export, no backup, no sync. That is a deliberate, settled stance for everything the app has +remembered so far, and for good reason: the app has, until now, only remembered things that are +cheap to lose (which page you last had open, which page you pinned). The confusion map is the +first thing the app would remember that is *expensive* to lose. So the stance that was free for +everything before now carries a cost for the first time, and this decision is where that cost +gets weighed rather than inherited. + +## What do people outside this project do about this? + +**We looked, and here is the honest state of it.** Note-taking and habit apps that hold +personal history almost universally offer *some* way out — an export file at the least, cloud +sync at the most — precisely because users will not trust months of their own data to a single +device. That is evidence that a way out is expected, not that any particular one is right. + +The closer reference — how other apps handle a record of *religious practice* specifically — we +did **not** research in depth, and should before settling, because the privacy weight here is +different from a habit tracker's. A record of where a person's memory of the Qur'an fails is +intimate in a way a reading streak is not, and the norms that intimate-health and faith apps +have converged on are the relevant prior art. This record does not claim to have surveyed them. + +## What have we already decided that touches this? + +Two things constrain the space, and neither is a formal entry in this register — they are +stances written into how the app already works: + +- **Nothing leaves unless it is in the reader's interest.** Until now the app has kept everything + private and local, and that read as a flat rule — *nothing leaves the phone*. The owner has + sharpened it: the durable rule is not that nothing leaves, but that nothing leaves *unless it + serves the reader, and stays under their control*. A flat *nothing leaves* breaks the first time + the reader's own interest points outward — as it does here, where they want their hard-won map to + survive a lost phone — and once broken it reads as a promise abandoned. Naming the test instead + keeps the promise: a thing may leave when, and only when, it is in the reader's interest and they + hold the controls. That reframes this whole decision — the question is not *whether to make the + first exception to "nothing leaves,"* but *which of these ways of leaving actually serves the + reader, and at what cost to them.* +- **The cloud option is not reachable soon.** A web page in a browser cannot copy anything into + a personal cloud like the one built into the phone; that needs a real installed phone app. That + app is on the long-term plan, but it is held up by a licensing question the project cannot + answer itself and is waiting on qualified advice for. So the cloud option below is real, but it + is not something that can ship this year regardless of what is decided here. + +That second constraint does most of the work: it takes the heaviest option off the near-term +table on its own, and leaves the near-term choice as *on the phone only* versus *a file the +reader saves*. + +## What are the options? + +Each is drawn, at phone size, on the published page this record points to — +. In short: + +### Option A — Keep it on the phone only + +Change nothing. The map lives in the phone's private storage and never leaves. A reader who +wants it safer installs the app to their home screen, which makes a silent wipe far less likely. + +- **Takes:** nothing — this is what the app does today. +- **Gets:** the strongest possible privacy. The map physically cannot leak, because it never + exists anywhere but the one phone. +- **Costs:** the map can still be lost — to a wipe, a broken phone, a new phone — and when it is + lost it is lost silently and completely. This is the cost the owner asked to remove. + +### Option B — A file the reader saves and restores *(the owner's lean)* + +The app grows two plain actions: save a backup, which hands the reader a single file they put +wherever they like; and restore from a backup, which reads that file back. Nothing leaves the +phone unless the reader deliberately saves the file, and it goes only where they choose to put +it — including, if they like, their own cloud drive. + +- **Takes:** a modest, self-contained piece of work — turn the map into a file and back — with no + server, no account, and no change to the app's stay-on-the-phone nature. +- **Gets:** the map survives a wiped or lost phone and moves to a new one, while the reader keeps + full control of the one copy that ever leaves. The privacy exception is small and reader-driven. +- **Costs:** a saved file is a file — it can be read by anything the reader lets near it, and a + reader who never saves one is no safer than under Option A. It protects those who use it; it + cannot protect those who do not. + +### Option C — A cloud-sync switch the reader controls + +The reader turns on a sync switch; from then on the map keeps a copy in the personal cloud built +into their phone — so it survives a lost phone and appears on every device they own — with tooling +to see and clear what is stored, and the copy reachable through the phone's own Files. The owner's +note is the reason it is drawn as a switch and not as something always on: sync that serves the +reader is sync the reader turned on and can turn off — which is the governing rule above, applied. + +- **Takes:** the installed phone app that does not yet exist, and which is blocked on the + licensing question above — plus the switch, the management tooling, and the Files access that + keep it under the reader's control. Not reachable this year. +- **Gets:** once switched on, the map is safe without the reader having to remember to do + anything, and follows them across devices — the strongest protection against loss. +- **Costs:** while the switch is on, an intimate record lives continuously on someone else's + servers, not only when the reader reaches for it. Making it a switch the reader controls — not + something on by default — is what keeps it inside the "only in the reader's interest" line; the + record is still off the phone the whole time it is enabled. + +## What else could we consider, and why is it not here? + +- **Letting a teacher see a student's map.** A genuinely valuable use — a teacher could drill a + student exactly where they are weak — and a real pull toward some way out of the phone. It is + left off this page because it is a *sharing* decision, not a *backup* decision: it sends the map + to another person, which is a different and larger privacy question than a reader keeping their + own copy. It deserves its own page if it is ever wanted, and Option B does not settle it (a + backup file a reader chooses to send a teacher is not the app sharing it). +- **A password on the backup file.** Worth considering as a refinement of Option B, not a rival + to it — so it belongs in the building of B, not as a separate option here. + +## What would change the answer? + +- If the closer prior art — how faith and intimate-health apps handle export — comes back saying + even a reader-saved file is a norm those apps deliberately avoid, that would weigh against B. +- If the licensing question that blocks the phone app were answered in the app's favour, Option C + would move from "not this year" to a live choice, and the question would become B versus C + rather than A versus B. +- If real use showed readers almost never clear their phone's data or lose the map in practice, + the cost of Option A would shrink and doing nothing would look better. + +## What is this not settling? + +It is not settling teacher-sharing (named above and deliberately held apart). It is not settling +the shape of the backup file, whether it is encrypted, or what it is called — those are the +building of Option B, once B is chosen. And it is not settling anything about the cloud option's +timing beyond "not this year," because that timing is owned by a different, unanswered question. diff --git a/docs/decisions/loop-3.md b/docs/decisions/loop-3.md index 71cdf698..b0ad640c 100644 --- a/docs/decisions/loop-3.md +++ b/docs/decisions/loop-3.md @@ -1,6 +1,13 @@ # Loop 3 — Diffs, share links, a11y pass **Status:** complete. **Date:** 2026-07-25. +**Superseded in one part, 2026-08-16.** What this record calls the *token diff* — a table of +twelve ayahs typed out by hand and split into shared and unshared pieces — no longer exists. The +comparison it describes is still there and is better, but it is now drawn out of the printed page +itself rather than from typed text, so it covers 2,544 look-alike pairs instead of twelve and +shows the reader the same letters the page under it shows. The typed table was also the largest +of the two places in this project holding running scripture, which is the reason it went. Nothing +else in this record moved; it is left as written because it says what shipped that day. **Exit criterion (PLAN §Loop 3):** cold-opening a teacher link restores the exact view incl. trail; the screen reader announces ayahs and hops. **Result:** every spec-§7 link form cold-opens through the *same* select/navigateTo path a live diff --git a/docs/decisions/mark-granularity.md b/docs/decisions/mark-granularity.md new file mode 100644 index 00000000..07516ace --- /dev/null +++ b/docs/decisions/mark-granularity.md @@ -0,0 +1,54 @@ +# One colour a verse, or the letter itself? + +**Status:** decided — **option B**, by omar on 2026-08-19. + +**Picture:** (an earlier copy on another host: ) — three +options drawn over a real page of the mus'haf. Checked in as `mark-granularity.html`, rebuilt +by `node scripts/build-mark-options.mjs`. + +Read the picture first. This file is the reasons; the page is the subject. + +## What this is + +Hifth colours tajweed by verse today: one tint per ayah, chosen for whichever rule is rarest +in it. `docs/design/sub-word-marks.md` measured what that wash actually shows a reader against +what the corpus already knows — a vocabulary of thirty-four drawn shapes, each pinned to one +name by elimination and checked against 62,931 held-out cases the matching never saw — and +found the two a long way apart: 83.29% of what the data knows renders as nothing, because a +verse gives its one colour to whatever rule is rarest and the other three or four rules in it +go unlit. + +Three options: leave the verse-level wash as it is (A), colour the exact letter or mark a rule +names (B), or build the vocabulary and ship nothing yet (C). C is already true of the tree — +the vocabulary exists, unshipped, on purpose — so choosing between A and B was the real +question. + +## Why B + +A verse-level wash cannot get finer without losing legibility of a different kind: ten of the +eighteen rules name a consonant, not a mark, so they have nothing to light at letter +granularity and would need to keep painting the word — the split `sub-word-marks.md` calls +"an answer, not a shortfall." But for the eight rules that do name a mark, the print reaches +one almost every time: 98%+ for the wasla and madd families, 99.8%+ for iqlab. Coarsening +those down to a verse-wide tint throws away a distinction the data already makes correctly, +for no reason but that nobody had wired it up. + +Leaving it as it is (A) keeps that loss permanently — not a stage on the way to something +finer, since nothing about shipping the verse wash forces the finer vocabulary to ever get +used. B is the option that spends the thirty-four-token vocabulary on what it was built to +answer. + +## What this does not settle + +Two questions sub-word-marks.md keeps open and this decision does not close: + +- Whether a tajweed span lands on a mark that reads as *that rule* to someone looking at the + page, rather than merely on a mark that happens to be nearby — still open, and answerable + only by putting it in front of a reader, not by more arithmetic. +- Whether the corpus is worth the bytes it costs to ship — a probe until there is a shipped + mark-granular highlight to weigh it against. + +Both carry into the implementation task (mark-C on the task board), which this decision +unblocks rather than replaces. [Whose colours are they?](tajweed-colours.md) is the next +question this one opens: a mark-granular highlight is what would make a per-rule colour picker +mean anything finer than the seven groups already visible today. diff --git a/docs/decisions/page-bar.md b/docs/decisions/page-bar.md new file mode 100644 index 00000000..030247aa --- /dev/null +++ b/docs/decisions/page-bar.md @@ -0,0 +1,138 @@ +# Two questions about the page bar: does a juz marker pull, and whose juz is a boundary page? + +*Status: both open. This record states the questions and draws the options; it does not choose.* + +**The picture:** — `page-bar-options.html`, checked in and rebuilt by +`scripts/build-page-bar-options.mjs` from the vendored print's page-of-every-verse table, the +juz table and the bar's own stylesheet. The address is the page's own on the app's site, so it +is public the day the page is merged. + +## A few words, defined once + +- **The page bar** — the control along the bottom of the app that scrubs through all 604 + pages of the print. Dragging it moves the book; letting go opens the page under the thumb. +- **Juz** — one of the thirty roughly equal parts the book is divided into for reading it + over a month. A juz begins at a fixed verse, not at a page. +- **Marker** — the short green tick drawn on the bar at the page a juz begins on. Thirty + of them, added on 2026-09-01 when the bar was redesigned. +- **The bubble** — the label that floats over the thumb while dragging: the page number, + then the juz and surah of that page, then — if the page is not in this build — the + nearest page that is. + +## Why are these being asked now? + +The bar was redesigned in one sitting: thirty markers, a page-shaped handle, and a bubble +that names the juz under the thumb. Two things in that redesign were built one way because +something had to be built, and each is a small decision a stranger to this code could hold +an opinion on. The redesign is not yet committed, so this is the cheap moment to ask. + +## What happens if nobody decides? + +Nothing breaks. The bar works today as option A of the first question and option A of the +second, and both are defensible. The cost of leaving them is that the second question's +answer already differs between two parts of the app (below), and a third part that reads a +page's juz will inherit whichever it copies from. Nothing else is blocked behind either. + +## What have we already decided that touches these? + +- The juz jump lands on the page a juz begins on, and says so + (`desktop-vs-mobile`, row 27: the wheel with Shift held). Both questions take that as given. +- The revision map has a cell per juz that opens it on the lowest page of it this build + holds (`desktop-vs-mobile`, row 24). That is one of the three exact roads to a juz that + the first question weighs the bar against. +- The bar's markers cannot be touched — they sit under the thumb so a tick never eats a + drag. Option C of the first question would reverse that. + +## When a reader lets go near a marker, should the bar pull the page onto it? + +Today it does not. A release lands on the page under the thumb and the bubble says which; +the marker is a landmark you steer by, not a magnet that catches you. + +**What the app does today, measured.** The bar spans 604 pages. Read off its stylesheet, +one page is 1.34 px of track on a laptop (the bar is held to 960 px) and 0.4 px on a +390 px phone. A fingertip is about 26 px, so a drag ends within about 19 pages of where it +was aimed on a laptop and about 65 on a phone. A juz opening cannot be hit by drag on +either; on a phone, its neighbourhood cannot be either. There are three roads that land +exactly: the wheel with Shift held, a cell of the revision map, and the jump box given +"juz 9". Nobody has reported reaching for the bar to get to a juz and missing; the bar is a +week old. + +**The options**, each drawn at both widths on the page: + +- **A · A marker only marks** — today. Takes nothing. Every page is reachable by drag and a + release means exactly where the thumb is. Costs the exact road the bar does not give. +- **B · A marker pulls, a few pages either side** — release within three pages of a marker + and the bar lands on the juz's first page, saying so in the bubble before you let go, in + the sentence it already uses for a page not in this build. Gets an exact road at the size + a finger can hit. Costs six pages around each of thirty markers — 180 pages, 30% of the + book — that a drag can no longer land on, though the arrows still reach them; and on a + phone the reach is 2.4 px wide, a tenth of the thumb, so it is really "the marker wins + whenever you are near it". +- **C · A marker is a button; the drag is unchanged** — tap a marker to open that juz. + Gets an exact road without changing what a release means. Costs thirty touch targets of + 44 px on a 262 px phone track, five times the room there is, so on a phone every drag + would start on a button. + +**What else was considered.** A magnet that only pulls when the drag is slow, or only on a +laptop where the pointer is precise: both make the bar do two different things by device, +which the living desktop-versus-phone record refuses without a phone constraint to name. +Not drawn. + +**What would change the answer.** A reader saying they miss. A phone in hand — everything +above is arithmetic on a 390 px window. A hizb layer on the map, which would make a magnet +on thirty marks into one on sixty. + +## When a juz begins partway down a page, which juz is that page in? + +Twenty-six juz begin at the top of a page. Four begin partway down one — juz 4 on page 62, +juz 7 on page 121, juz 11 on page 201, juz 26 on page 502 — so that page carries the end of +one juz and the start of the next. The page draws each of the four as a schematic with the +seam where the verses put it. + +**What the app does today, measured.** It answers two ways. The bar's bubble says the page +is the juz that *begins* on it, because that is the juz whose marker sits there. The +offline-pack shelf and the wheel's "no juz that way" message say it is the juz that was +*already running*, the lowest juz with any verse on the page. On 600 pages the two agree; +on these four they do not. Page 62 is 1 verse of juz 3 and 8 of juz 4; page 201 is 6 verses +of juz 10 and 1 of juz 11. + +**The options**, each drawn as the bubble over page 62: + +- **A · The juz that begins on it** — today, in the bar. Takes one line elsewhere so the + pack shelf and the wheel agree. Gets the marker, the bubble and the jump naming the page + the same way. Costs calling page 201 "juz 11" when it is six verses of juz 10 and one of + juz 11. +- **B · The juz that was already running** — today, in the pack shelf. Takes one line in + the bar. Gets the page named for what is at its top, which is what a reader meets first. + Costs the marker and the bubble disagreeing on four pages: the tick says juz 4 starts here, + the line under it says juz 3. +- **C · Both, on those four pages** — "juz 3 → 4". Takes a second way of writing the line in + both languages. Gets no page misnamed and the seam visible from the bar. Costs a wider + bubble on four pages, and the shelf and the wheel still have to pick one for their own + sentences. + +**What people outside this project do.** The printed Madani mus'haf names the juz in the +running head of every page, so on these four pages the print has already picked one. That +is the convention a hafiz carries, and it is the answer to copy — but nobody has yet turned +to page 62 of a physical copy and looked, and the vendored pages are outlines, so it cannot +be read from the bytes here. A web search on 2026-09-01 for how other mus'haf apps name a +boundary page found nothing that addressed it; the apps looked at list juz and let you jump +to one, and none was seen naming a page's juz at all. So: looked, found no answer, and the +one that matters is in the owner's bookcase. + +**What would change the answer.** What the print says on page 62 — that alone should settle +it. A hizb layer, whose boundaries fall mid-page far more often, would make a four-page +special case into a sixty-page one. + +## What is this not settling? + +- Whether the bar shows hizb marks at all. The map answers that first. +- What a marker looks like. The tick's size and colour were chosen so thirty of them read + apart from the grey inventory rail, not for either question here. +- Which page a juz jump lands on. + +## So what is being decided? + +Two things, separately: whether letting go near a marker should pull the page onto it (A, B +or C above), and which juz the bar should call a page that carries a juz seam (A, B or C). +The second has an answer waiting in a printed copy. diff --git a/docs/decisions/sitting-hosting.md b/docs/decisions/sitting-hosting.md new file mode 100644 index 00000000..737e39cf --- /dev/null +++ b/docs/decisions/sitting-hosting.md @@ -0,0 +1,208 @@ +# The instrument needs a house, and today it is one laptop + +**Status:** decided — **A**, the laptop, by Omar on 2026-08-17. +**Date opened:** 2026-08-17. **Date settled:** 2026-08-17. +**Asked as:** *"how can we host this review in a way where we can do it from anywhere? can +claude artifacts hold state?"*. +**The picture:** [`docs/design/sitting-hosting.html`](../design/sitting-hosting.html), built by +`node scripts/build-sitting-hosting-options.mjs`, published at + (an earlier copy on another host: ). +**Constrains and is constrained by:** [mark placement](../design/mark-registration.md) — the +question the sittings exist to answer. If that one is settled on the evidence already in hand, +this one evaporates. + +## The question + +Where a sitting should live: on the laptop that hands it out over the household's private +network, or published as a page anyone can open from anywhere — and if published, whether each +answer is banked as it is given or held in the browser until the end. + +| | | +|---|---| +| **A** | Leave it as it is — one laptop hands out the sittings over a private network | +| **B** | Publish each sitting as a page, and hand the answers over at the end | +| **C** | Publish each sitting as a page, and let it bank each answer to a hosted table | + +Each is drawn on the options page as the journey a single answer takes, from the reader's +finger to a line checked into the repository, with the steps that can fail marked. + +## What was chosen, and what it commits us to + +**A.** The laptop keeps handing out the sittings, and the reader reaches them from a phone over +the household's private network. Asked for in these words, the same day the question was opened: +*"have them run locally on this laptop but remember this laptop is setup on tailscale and can +access from phone."* + +Be clear about what settled it, because it was not a fresh weighing of three options. The one +thing A costs — the reader has to be somewhere that network reaches — was judged acceptable, +and the two costs that made A *feel* worse than it is turned out to be missing work rather than +properties of the arrangement: + +- **Reaching it from the phone at all was an incantation nobody had written down.** The server + listened only to the machine it ran on unless someone remembered a flag, and no script, + routine or document anywhere recorded how to start it. It now defaults to the private network, + and there are two named commands, and the routine has a sixth step that names them. +- **The machine answers to two spellings, and a browser treats them as two different sittings.** + Nothing said which to use, so the choice was made by whichever one got typed, and a reader who + came back at the other one was dropped at card one with an hour apparently gone. There is now + one address, derived rather than typed, printed on start with the others named as the ones not + to use — and the front door says so on screen when it notices it is being read at the wrong + spelling. + +Two further commitments came with the choice, because a sitting on a phone is a sitting on a +phone and both were broken: + +- **A front door that says where you left off.** It counts the marks out of the sittings + themselves, asks this machine what it has heard, and points at the one to carry on with. +- **Two taps in a row are two answers, not a zoom.** Pressing the same nudge twice magnified the + page instead of nudging it, which on a phone is most of what nudging is. + +B and C stay on the page unbeaten rather than beaten. Neither was tried; A was made good enough +that neither had to be. What would reopen this is unchanged and is listed below — most cheaply, +a sitting actually attempted away from home. + +## Why it is being asked now + +Because the work is about to get long, and only now is it clear how long. 1,710 marks remain, +in 16 sittings of about 106 — measured by `pnpm audit:sittings`, not estimated. At the pace of +the two sittings already done that is roughly 16 hours of one person's attention, every hour of +which currently needs `pnpm sittings:serve` running on one machine, in one house, with the +reader on the same network. + +Nothing is broken, and this is not a defect report. The arrangement was built deliberately — +`packages/etl/scripts/serve-sittings.mjs` replaced `python3 -m http.server` precisely because +answers were living only in one browser's store and going unbanked until the end, which cost a +reader an evening twice. The question is whether the arrangement that fixed *that* is the one +that gets the next sixteen hours done. + +## What happens if nobody decides + +The sittings carry on exactly as they are, and the cost is real but small: they happen when the +reader and the laptop are in the same house, and not otherwise. + +**Nothing is waiting behind this.** No feature is blocked, no other decision depends on it, and +the 578 statements already banked in `packages/etl/out/mark-answers.jsonl` are safe under every +option — the file is only ever appended to, and `packages/etl/scripts/lib/answered.mjs` is the +one reading of it that every consumer shares. + +So this can stay open, and saying so is part of the answer. If the remaining hours are going to +happen at a desk at home anyway, the honest choice is A and no work at all. + +## What it costs today, measured + +Every number below was counted on 2026-08-17 out of `packages/etl/out` by +`node scripts/build-sitting-hosting-options.mjs --extract`, and lives in +`docs/design/sitting-hosting.data.json` so the page rebuilds from committed bytes. + +- **1,710** marks unsat, **16** parts of about **106**, **167** already answered, **1,877** in + all. Confirmed independently by the auditor, which reads what each built part declares it was + built from rather than trusting the build log. +- **578 statements over 167 marks — 3.5 per mark.** This is the number that separates the + options. A reader nudges a rectangle a step, looks, nudges again, and each nudge is a separate + thing said. Anything that banks one at a time does so three or four times a mark, not once. + By kind: 360 placement, 201 wrong shape, 15 print defect, 2 looks right. About 193 bytes each. +- **1.3 MB is a whole sitting** — median across the sixteen, min 1.26 MB, max 1.43 MB, total + 21.4 MB — against a **16 MB** artifact ceiling. So publishing one is not a technical question, + which is what makes B and C possible at all. + +## What other people do about this + +**Nobody looked.** The session's web-search budget was exhausted before this page was written, +and no prior art was consulted. That is stated on the options page in the same words, because a +record silent about prior art reads as one that found none. + +The comparable question — how projects that ask many people to judge many pictures get those +judgements home safely — has a large literature behind it in crowd-judged science, in map +correction, and in image-labelling tooling. What is worth looking up specifically, if this is +going to be decided rather than left: how those tools handle a judgement given while the device +is offline, and whether they bank each judgement as it is given or in batches. That single +detail is what separates the three options, and somebody has certainly measured it. + +## What we have already decided that constrains it + +- **Mark placement is open, and this exists only to serve it.** The sittings are how that + question gets evidence. If it is answered on what is already in hand, the remaining 1,710 + marks stop being urgent and this closes unmade. +- **What a reader does inside the app never leaves their device.** That is settled, and it is + about *the app*. A sitting is not the app — it is a maintainer's instrument shown to one or + two people who know what they are agreeing to — so the rule does not forbid C. But the + instinct behind it should be brought to C deliberately rather than forgotten, and C is the + only option that has to answer for it. +- **This project ships static files on purpose.** That is what rules out the fourth option + below, and it is also why B and C are cheap: a sitting is already one self-contained file with + no external references, because it was built to be opened from `file://`. +- **An answer already given is never at risk.** Whatever wins, the settling run + (`pnpm sittings:settle`) reads the same append-only log, and `pnpm audit:sittings` will notice + if a rebuilt sitting stops agreeing with it. + +## What each option actually costs + +**A — the laptop.** Every answer is banked the moment it is given, so a closed lid costs at most +one statement, and the count of what is left falls as the reader works. Two costs. The first is +the one this question is about: the reader must be somewhere that network reaches. The second is +sharper and less obvious — restarting the server mints a fresh token, and every page a reader +already has open quietly stops banking. The answers keep appearing to send and stop arriving. +That is why `review-sitting` says not to restart the server to pick up a rebuild. + +**B — publish, hand over at the end.** This is what was in place before the laptop was taught to +receive, and it is known to fail: a browser's store belongs to one address, so the same machine +reached two ways is two memories of the same sitting and neither can see the other. Nothing is +banked until the end, so a cleared browser loses the hour, and the remaining count cannot fall +until a file comes back. It buys exactly one thing, and it is the thing being asked for: any +device, any place, nothing running at home. + +**C — publish, bank each answer.** Everything A gets with none of what A costs. It is also the +only option here that does not exist: it needs a table, a shape agreed for what one statement +looks like, a page that writes to it, and a step that pulls the rows back down into the same +log. And its failure mode is the one nobody has met. The connector is granted by the reader on +their own account, per reader, which is a real constraint and also the thing that keeps the +answers from being handed to anybody who finds the link. + +## What else could be considered, and why it is not an option here + +- **Publish the page, and have it post back to the laptop.** The obvious middle, and it cannot + work: a published page is served under a policy that blocks every external host. It would not + fail loudly — it would simply never arrive. +- **Put the sittings on the open web with a small service behind them.** A bill, an account, and + a thing to keep running, for an instrument two people will use for perhaps sixteen hours. +- **Send the sitting as a file and get it back as a file.** Already works, and it is what B + degrades to when nobody publishes anything. Not listed separately because the only difference + is whether the reader receives a link or an attachment, and everything after that point is + identical. +- **Expose the laptop to the open internet rather than the private network.** Deliberately not + drawn. It turns a household instrument into a public one for the sake of convenience, and the + private network already reaches every device the reader owns when they are at home — which is + where the problem is not. + +## What would change the answer + +- **A sitting attempted away from home.** If the reader tries and cannot, A has been measured + rather than assumed, and this stops being hypothetical. This is the cheapest thing on the list + and it should happen before anything is built. +- **1,710 still being 1,710 in a month.** Then the arrangement is the suspect, whatever anybody + thinks of it in principle. +- **A second reader.** Every option assumes one person answering. Two makes A awkward and C + obvious, because two readers on one laptop's network is not an arrangement anybody would + choose. +- **Mark placement being settled.** Closes this unmade. + +## What this is not settling + +Whether the rectangles are right — that is [mark placement](../design/mark-registration.md), and +it has its own page. What a hosted table would actually hold: C describes a shape that does not +exist, so choosing it is choosing to design it, not choosing a design. And nothing about the app +itself — no option here changes one thing a reader of the mus'haf would see. + +## Where the numbers come from + +`scripts/build-sitting-hosting-options.mjs` has two modes. `--extract` reads +`packages/etl/out` — the sixteen built parts and the answer log, neither of which is committed — +and writes `docs/design/sitting-hosting.data.json`, which is. The default mode renders the page +from that file and nothing else, so it rebuilds on a fresh clone. + +The specimen card on the page is one real question lifted from the first part, with the two +fields carrying Arabic dropped: the printed ink is outlined paths from the vendored print, and +the generator refuses to write a page containing an Arabic codepoint. The card's ink and paper +colours are pinned inside the card rather than inherited, because the page's own theme tokens +share those names and a dark theme would otherwise print black on black — the defect +`mark-registration.md` ⑪ records as already fixed once, in the instrument itself. diff --git a/docs/decisions/spread-zoom.md b/docs/decisions/spread-zoom.md new file mode 100644 index 00000000..aa348735 --- /dev/null +++ b/docs/decisions/spread-zoom.md @@ -0,0 +1,162 @@ +# When two pages are open, do the zoom buttons grow both together? + +**Status:** decided — both leaves grow together. +**Decided by:** omar, on 2026-09-01. +**In one line:** with the book open, pressing magnify now enlarges both pages at +once, to the same size. It used to do nothing at all while two pages showed. + +This reverses a finding made earlier, on purpose and by the person who made it. +Everything below is why the old answer was reasonable, why it was overturned, and +what the reversal does and does not settle. + +## A few words, once + +- **Mus'haf** — the printed Qur'an, the thing on screen. +- **Leaf** — one page of it. When two are shown side by side, each is a leaf. +- **The opening** — the two facing leaves seen together, the way a physical book + falls open. "Open the book" means show two leaves; "close it" means show one. +- **Magnify / the stepper** — the two buttons in the top bar, marked minus and + plus, that make the page larger or smaller by fixed steps. + +## What is being decided? + +When the book is open — two leaves side by side — and the reader presses the +magnify button, what should happen? Three answers were on the table: nothing (the +old behaviour); make the reader close to one page first; or grow both leaves +together. The reader chose the third. + +## Why is this being asked now? + +Because a reader hovered the magnify buttons with two pages open, found them dead, +and the cursor showed the "not allowed" sign. From the reader's chair there was no +sign of *why* they were off — the buttons looked broken, not withheld. The +question "why can't I make this bigger?" is the report that reopened it. + +## What did the app do before, and what did it cost? + +With two pages open the magnify buttons were switched off entirely. Hovering them +showed a blocked cursor and a tooltip that said, in effect, "magnifying needs one +page — switch to one page first." A reader who wanted a larger page had to first +change a *different* control (the one-page / two-page switch), then magnify, then +switch back if they wanted the opening again. Three actions for one wish, and the +first of the three was invisible until you went looking for it. + +That was not an accident. It was the answer to a real observation, recorded when +the magnify buttons were first built. + +## What did we already decide that touches this? + +One decision, closely: **the wheel navigates, buttons magnify** — the change that +took magnification off the mouse wheel and put it on a pair of buttons. That +decision carried a corollary: *the buttons are off while the book is open.* It +rested on two observations made at the time: + +1. Two enlarged pages side by side lose their outer edges off the screen and start + to read as one tall column of script, which an opening is not — a spread is + *two* pages, and the gap down the middle is part of what tells you so. +2. At the width where two pages first fit, each leaf is already limited by the + screen's *height*, not its width — so at that one narrow size, magnifying buys + very little reading size before the page runs off the top and bottom. + +Both were true. This decision does not dispute either. It disputes that they +should *forbid* the reader from magnifying — it makes them the reader's cost to +weigh, not the app's rule to enforce. The earlier record has been marked to point +here, so nobody reads its "disabled while open" section as still current. + +## What do other readers and other apps do about this? + +I did not survey other mus'haf apps for this specific behaviour before deciding, so +treat this as thin. The one strong reference point is the physical book the app is +imitating: a reader holding a large-print mus'haf does not see one page grow while +the facing page stays small — the whole opening is the same size, because it is one +sheet of paper. "Both leaves at one size" is the behaviour that matches the object. +The reason print does not answer the *edges* worry is that paper has no screen to +run off — a bound book can be as wide as it likes. That is exactly the constraint +observation (1) above is about, and it is why the drawn evidence, not the analogy, +is what the reader weighed. + +## The options, and what each costs + +**Leave it off (the old answer).** No work, and the two edge-and-height findings +are honoured by construction. The cost is the reported one: a control that reads as +broken, and a three-step detour for anyone who wants a bigger page while keeping +the opening. *Not chosen.* + +**Make the reader close to one page first.** Keep magnification a one-page feature, +but when the reader presses magnify on a spread, close the book for them and then +magnify. Cheaper to reason about — one leaf, one size — and it sidesteps the edge +worry entirely. The cost is that it silently overrides the reader's other choice: +they said "show me two pages", and the magnify button would quietly countermand it. +*Not chosen.* + +**Grow both leaves together (chosen).** The magnify buttons work with the book +open, and one press enlarges both leaves to the same size. The reader keeps their +opening and gets a larger one. The two pages grow *outward from the fold* — the +gap down the middle stays exactly where it is, and each page opens away from it +into the margin of empty desk on its outer side, the way a real book gets larger. +So a step or two of magnification clips nothing at all: it spends the desk margin +first. Only past that does the outermost edge of the opening begin to leave the +screen, and when it does it leaves symmetrically, both sides at once, which is what +zooming any large picture does. At the narrowest two-page width there is also +little height to spend before the top and bottom run off — that half of the old +finding still holds, and it is the reader's to weigh. + +## Why "together" and not "each leaf on its own"? + +Because two leaves at two different sizes is the *original* complaint that the +wheel-and-buttons decision was cleaning up in the first place — a book open with +one page big and one page small looks broken in a way neither size explains. The +whole reason this is safe to build is that there is exactly one magnification for +the opening: the live page owns it, and the facing page is told to match whatever +the live one lands at. One number, so there is nothing to fall out of step. + +## Does the opening grow from the middle, or from the fold? + +This is the part the first build got wrong, and it is worth stating plainly +because it is what a reader sees before they can say whether they like anything +else. The first attempt grew each page from its own centre. Two pages each +swelling from their own middle push *toward* each other: the inner margins meet +and crush the fold — the gap down the middle that tells you it is two pages and +not one — while the outer margins are shoved off both edges of the screen at once. +The reader's words for it were that the middle went weird and the sides were +clipped, and both were the same mistake seen from two ends. + +The opening grows from the **fold** instead. The middle of the book is held still +and both pages open outward from it, the way paper does when a book is made +larger — nothing near the fold moves, and the empty desk on the two outer sides is +what gets spent as the pages grow. That is why a normal step or two of +magnification now clips nothing: there is room to grow into before there is +anything to run off. It is the same one magnification for both leaves as before; +only the point they grow *from* changed, from two centres to the single fold they +share. + +## What would change the answer? + +- If readers report the magnified spread genuinely reads as one column and they + lose their place in it, the edge finding wins after all, and the fallback is the + second option — magnify by closing the book. The mechanism to do that already + exists; only the wiring would change. Growing from the fold was meant to answer + exactly this worry — the middle gap is preserved, so the two pages stay legibly + two — but the test is a reader's eye, not the drawing, and this is the thing to + watch for. +- If the app ever gives the facing page its own controls (a drawer on the opposite + leaf is already contemplated separately), the "one magnification for the opening" + rule would need re-examining, because a second surface is a second place a size + could be set. + +## What this is not settling + +- **Magnification is still dropped to fit when the opening first appears**, and + when a shared link or a page-turn lands with the book open. Both leaves start at + their natural size and the reader grows them from there. Whether magnification + should instead *survive* a page-turn or a jump inside a spread is a separate + question, left open. +- **Nothing about a phone.** Below the width where two pages fit, there is no + opening and no facing leaf; this decision does not exist there. +- **The height finding is not withdrawn.** At the narrowest width where two pages + first fit, each page is limited by the screen's height, so there magnifying buys + little before the top and bottom run off. That is real and the reader chose to + live with it. The *edges* half of the old finding is answered rather than + accepted: growing from the fold keeps the middle gap and spends the desk margin + before anything clips, so a modest magnification loses no edges at all. If either + proves worse in the hand than on the page, this is reopenable on that evidence. diff --git a/docs/decisions/tajweed-colours.md b/docs/decisions/tajweed-colours.md index d7ed43f7..40ddd537 100644 --- a/docs/decisions/tajweed-colours.md +++ b/docs/decisions/tajweed-colours.md @@ -1,15 +1,17 @@ # Whose colours are they? -**Status:** open. Nobody has chosen. +**Status:** decided — **option B, with C offered behind an advanced setting**, by omar on +2026-08-19. **Date opened:** 2026-08-08. **Asked as:** *"can we also enable a tajweed rule color coding page where readers can change the colors of tajweed rules"*. **The picture:** [`docs/design/tajweed-colours.html`](../design/tajweed-colours.html), built by `scripts/build-palette-options.mjs`, published at -. -**Constrains and is constrained by:** [mark granularity](../design/sub-word-marks.md) — the -question of whether the app paints a whole verse or the exact letter. That one decides how much -of this one is even visible, and it is also open. + (an earlier copy on another host: ). +**Constrains and is constrained by:** [mark granularity](mark-granularity.md) — the question of +whether the app paints a whole verse or the exact letter, decided the same day as this record, +as **B: colour the exact letter**. That is what turns option C below from incoherent into an +advanced surface rather than the default one; see "What decided it". ## The question @@ -113,11 +115,10 @@ readable the day the generator does. ## What we have already decided that constrains it -- **Mark granularity is open, and it dominates.** The app paints one tint per verse. Until that - changes, eighteen colours are mostly unpaintable and 620 verses have no answer. If that - question is settled as "colour the exact letter", C stops being a bad idea and becomes a - plausible one in a single change. Answering *this* record with C before that one is answered - would be building a picker for colours nobody can see. +- **Mark granularity is decided, as of this same day, as "colour the exact letter."** The app + still paints one tint per verse until `mark-C` on the task board ships that. Until it does, C + stays an advanced setting a reader can turn on without yet seeing much from it — the ordering + this record settles, not a reason to have waited for `mark-C` to land first. - **Loop 6a settled that the colouring can be turned off entirely.** That control stays; everything here is about what happens when it is on. - **Colour is never the only carrier** (WCAG 1.4.1). Every group has a dash pattern and an @@ -125,9 +126,36 @@ readable the day the generator does. two groups set to the identical colour and they are still separable, because the dash patterns and the marks are not part of what a reader would be editing. Whatever wins, they stay ours. +## What decided it + +Not a fifth option — B and C both, at different depths. A reader gets a picker for the seven +groups by default (B); the eighteen-rule picker (C) is there too, reached through an advanced +or expert setting rather than offered as the first thing shown. + +Two of the four arguments against C below survive the granularity decision unchanged: the +colours themselves do not fit — eighteen hues fanned as generously as is honest still produce +pairs closer together than the closest pair in today's seven — and eighteen rule names in two +languages is real weight to hand every reader by default. Those are why C is not the default. + +The other two do not survive it. "Most of them cannot be painted" and "620 verses have no +answer" were both a fact about painting at verse granularity, where a colour has to win an +entire ayah against whichever other rule is in it. Once the app paints at letter granularity — +decided the same day, in [mark-granularity.md](mark-granularity.md) — a rule's colour shows +wherever its own letter is, not wherever it happens to win a tie for the verse. The specific +per-rule reach numbers in the table above stop being the right measure of C once that ships; +they were never a measure of B, which is why B was never in question. + +So: B ships as the surface most readers see. C ships behind a setting most will never open, +for the reader who already knows what `idghaam_mutajanisayn` is and wants to see it named. That +reader exists — printed colour-coded mus'hafs teach a legend for exactly this — and hiding the +control costs them nothing an advanced setting can't answer, while showing it to everyone by +default would hand the majority eighteen names for a distinction most will never look for. +Lands as `tj-5` on the task board; the eighteen-rule picker itself is unscoped until then. + ## Why C is the one option this page argues against -Not by preference — by picture and by arithmetic: +Not by preference — by picture and by arithmetic. Written before mark granularity was decided; +"What decided it" above says which of these four survive that. 1. **The colours do not fit.** Fanning each family's hue in lightness and a little in hue, as generously as is honest, still produces **18 pairs closer together than the closest pair in @@ -157,9 +185,7 @@ wrong instrument for it until the app paints smaller than a verse. ## What would change the answer -- **Mark granularity settling as "colour the exact letter."** The big one — it is what makes C - coherent and what makes B's stakes higher. -- **A hafiz naming one of our seven as wrong.** Would settle B versus D faster than anything on +- **A hafiz naming one of our seven as wrong.** Would reopen B versus D faster than anything on the page. - **One report of a pair that genuinely cannot be separated.** That turns B from a preference into an accessibility fix, and then it should not wait behind anything. diff --git a/docs/decisions/wheel-and-zoom.md b/docs/decisions/wheel-and-zoom.md index 1042e5c2..968148d1 100644 --- a/docs/decisions/wheel-and-zoom.md +++ b/docs/decisions/wheel-and-zoom.md @@ -92,6 +92,13 @@ inherited and only the landings are pinned. ## The stepper is disabled while the book is open +> **Reversed on 2026-09-01.** The reader later chose to let the stepper work with the +> book open, magnifying both leaves together. The opening grows from the fold outward, so +> the *edges* worry this section raises is answered rather than paid — the middle gap is +> kept and the desk margin is spent before anything clips; only the height limit at the +> narrowest width still stands. The reasoning below is why it was disabled, and it is worth +> keeping for that — but the app no longer disables it. See [`spread-zoom.md`](spread-zoom.md). + This is where §8 ②'s answer survives its mechanism. That section rendered a magnified spread and found what it looks like: two pages under one transform lose their edges and read as one continuous column, which an opening is not. And §3 measured a leaf as **height**-bound at diff --git a/docs/design/ayah-box-sweep.html b/docs/design/ayah-box-sweep.html new file mode 100644 index 00000000..7e04f474 --- /dev/null +++ b/docs/design/ayah-box-sweep.html @@ -0,0 +1,204 @@ + + + + + +Ayah Box Sweep + + + +

    Which ayah boxes can the pen not draw as lines?

    +

    Built from the committed pages by scripts/build-box-sweep.mjs; the counts are held by gate:boxes. Rebuild when either moves.

    + +

    What is this page for?

    +

    When you tap a verse, the app lays an amber marker swipe along each line of it. It does that by reading the verse's hit box — a run of rectangles, one per line — and drawing one band per line. Three defects in that drawing were each found by a reader on one page; each was a whole class, visible across the book long before anyone met an instance. This page runs the app's own pen over every one of the 6,236 verse boxes and shows the ones it cannot draw as lines, on their real page, at about the size a desktop reader sees them.

    + +
    +
    verse
    an ayah — the unit a reader selects.
    +
    hit box
    the invisible shape under a verse that decides which verse a tap lands on. The print supplies one per verse, as a run of rectangles.
    +
    line grid
    the fifteen lines the print sets to a page; every rectangle is expected to be a whole number of them tall.
    +
    + +

    What did the sweep count?

    + + + + + + + +
    ClassCountWhat the reader sees
    Boxes the pen cannot read as lines8The older look: the raw shape filled and ringed. All on pages 1 and 2, whose decorated frame gives verses true polygon shapes.
    Rectangles off the line grid2A band thinner than its neighbours, sitting a little high. Drawn below.
    Rectangles spanning several lines (the pen splits them)582Nothing wrong any more — up to 13 lines fused by the print, each now drawn as its own swipe. This was the 2:249 defect.
    Rectangles too short for a band (drawn as a dot)99A one-word tail, inked as a single round dot — what a pen does when tapped.
    Rectangles in all12,350
    + +

    What does each flagged box look like?

    +
    the app's swipe, or its fallback washthe box's own outlinethe page's line grid
    + +
    +

    Page 1 · verse 1:4 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 1 · verse 1:5 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 1 · verse 1:6 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 1 · verse 1:7 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 2 · verse 2:2 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 2 · verse 2:3 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 2 · verse 2:4 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 2 · verse 2:5 · drawn as a box

    +

    The pen cannot read this box as a run of rectangles (it is a true polygon), so the app fills the raw shape and rings it — the older look, kept on purpose for a shape the pen does not know.

    +
    + + + + + + + +
    +
    + +
    +

    Page 564 · verse 68:3 · off the line grid

    +

    Rectangle 1 of this box is 27.9 units tall on a page whose lines are 36 — 0.77 of a line. The pen draws a band 77% as thick as its neighbours, and centred above the line's middle.

    +
    + + + + + + + +
    +
    + +
    +

    Page 602 · verse 107:2 · off the line grid

    +

    Rectangle 1 of this box is 28.3 units tall on a page whose lines are 36 — 0.79 of a line. The pen draws a band 79% as thick as its neighbours, and centred above the line's middle.

    +
    + + + + + + + +
    +
    + +

    What is this not settling?

    +

    Whether the two off-grid boxes should be repaired in the print's polygon layer (a declared repair, a re-vendor and a re-pin) or tolerated as they are — a reader would have to be looking for them. The plan carries that question as follow-up 17. The sweep also says nothing about a box that covers the wrong glyphs; that is what the page and word gates measure.

    + + diff --git a/docs/design/comparison-crop.html b/docs/design/comparison-crop.html new file mode 100644 index 00000000..f6aee31b --- /dev/null +++ b/docs/design/comparison-crop.html @@ -0,0 +1,587 @@ +What should the panel show around the ayah? + + + + +
    +
    +

    Hifth · open question

    +

    What should the panel show around the ayah?

    +

    When you open a pair of look-alike verses, the app now cuts both of them straight out of the printed page instead of retyping them. That fixed a real problem and introduced a smaller one: a verse almost never begins and ends at the edge of a line, so the rectangle cut around it brings its neighbours along for the ride.

    +

    Decided Option F, by omar on 2026-08-16. The other five stay below — they are why it was a choice.

    +
    + +
    +
    +

    The answerWhat was decided?

    +

    Option F: fade the neighbours, and mark the shared words green and the differing words yellow. It is C and D taken together — the neighbouring verses veiled so they stop competing, and both meaningful states marked so nothing is left to inference — with one further change that neither parent proposed.

    +

    The two colours change what they are about. Today they say which verse you are looking at: terracotta for the one you came from, verdigris for the one you hopped to. From here they say what the words are: green where the two verses agree, yellow where they part, identically on both halves of the panel.

    +

    That is the substantive part of the decision, and it is worth being plain about the trade. Which verse is which was already written above each crop in words, so the colour was spending itself on something the reader could already read. Whether a phrase is shared or divergent was written nowhere — and it is the one thing a hafiz is looking at the panel to learn.

    +
    + +
    +
    +
    البقرة · ٢:٤٨ · هنا
    البقرة · ٢:١٢٣
    +
    Fade the neighbours, mark the shared words green and the differing words yellow — enlarged so the two tints can be judged against each other. At true size it is the specimen under option F below.
    +
    +
    +

    What the reader is being told

    +
      +
    • Veiled — a different verse that happens to share the line. Still legible, no longer competing.
    • +
    • Green — the two verses agree here. This is the wording that makes the pair confusable.
    • +
    • Yellow — the two verses part here. This is the thing to memorise.
    • +
    +

    The same two colours appear on both halves of the panel, because they now describe the words rather than which verse you are looking at. Which verse is which is stated in the label above each crop.

    +
    +
    +
    +

    What this costs, stated rather than buried. The panel's two halves no longer differ by colour, so a reader glancing at one crop without its label has lost a cue. Yellow is the hardest colour to lay on cream and has been pushed to an ochre to survive at ten per cent over ink — a reader may fairly say that is not yellow. And this is the busiest of the six drawings: it inherits the objection to C, that a veil goes over printed Quran, and the objection to D, that most of the crop ends up under colour.

    +

    It also reopens something this page had listed as settled. The two wash colours were chosen elsewhere, and this changes not merely which they are but what they mean. That is recorded as part of the decision rather than done quietly, so the earlier choice is superseded on purpose and not by accident.

    +
    +
    + +
    +

    The words on this pageWhat do these words mean?

    +
    +
    ayah
    A verse.
    +
    mus'haf
    The printed Quran the app draws. Every page is the printer's own artwork, vendored as-is.
    +
    look-alikes
    Pairs of verses that read almost the same — the thing a hafiz has to keep apart. Opening one is called a hop.
    +
    the crop
    The rectangle the app cuts out of a printed page to show one verse.
    +
    the wash
    The translucent colour laid over the words the two verses do not share.
    +
    +
    + +
    +

    The questionWhat is being decided?

    +

    What the app draws around a verse when it shows it beside its look-alike — not what it marks. Which words get the wash is read off the pair itself and is correct; that part is not in question.

    +

    What is in question is everything else inside the frame. Because a verse starts and stops mid-line, the rectangle that holds it also holds whatever else is printed on those lines. The reader is given no way to tell the difference between this is the other half of the verse, and the two agree here and this is a different verse that happens to sit on the same line.

    +
    + +
    +

    TimingWhy is this being asked now?

    +

    Until this week the panel showed the two verses as retyped text. That was wrong on its own terms — the transcription used a plainer spelling than the printed page, so a reader comparing the panel against the page underneath saw one set of letters in each. It was also the larger of the only two places in this project holding running scripture, which is the reason it went.

    +

    The replacement cuts the verses out of the page itself, so there is exactly one set of letters now, and it covers 2,544 pairs where the retyped table covered twelve. The neighbours arrived with the ink. This question exists because the fix worked.

    +
    + +
    +

    The alternative to decidingWhat would have happened if nobody decided?

    +

    Option A would have stood, because option A was live. Nothing breaks, and the panel remains a clear improvement on what it replaced — which is exactly why this could have gone unanswered indefinitely.

    +

    The risk was quiet rather than loud: a reader takes an unmarked neighbour for shared wording and memorises a difference that is not there — in an app whose entire purpose is to stop exactly that. Nothing would have told anybody it was happening.

    +
    + +
    +
    +

    Measured, not estimatedWhat does the app do today, and what does that cost?

    +

    Every look-alike pair that the panel can draw was measured, both sides — 5,088 crops across 2,544 pairs. For each one: how much of the rectangle actually drawn belongs to the verse it names.

    +
    +
    +
    69.8%of the average crop is the verse it names
    +
    70.2%is the middle crop — half are worse
    +
    16.1%are more neighbour than verse (820 crops)
    +
    10.3%is the worst of them — 80:36 on page 585
    +
    11.5%sit on one line and have no neighbours at all
    +
    +
    053610720%20%40%60%80%100%share of the drawn rectangle that is the verse it names
    +
    +

    Two facts in that distribution matter more than the average. The first is that 584 crops — the verses that fit on a single line — are perfect already: the strip drawn for them contains nothing but the verse. Whatever is decided here should not disturb them.

    +

    The second is that the worst case is not the longest verse but the shortest multi-line one. Verses spanning two lines average just 60.7%, because both of their lines are partial and there is no full line in the middle to dilute the edges. 220 crops are under a third. At the very bottom sits 80:36 on page 585, where nine tenths of what the reader is shown is other verses.

    +

    The pair drawn on every specimen below is the app's own signature example, and it straddles the problem neatly: 2:48 is 80% its own verse, and 2:123 is 48%.

    +

    Look at what that means on the lower crop, in option A below. It is labelled 2:123, and its entire first line is 2:122 — the verse before it, ending in a printed verse number, none of it marked in any way. The verse the label names does not begin until the second line. A reader who takes plain ink for shared wording has just been told that a whole line the two verses do not have in common is a line they agree on.

    +

    A note on the arithmetic: a few crops score fractionally over 100% because two adjacent lines' strips overlap slightly where letters descend. They are counted as 100% rather than more.

    +
    +
    + +
    +

    Prior artWhat do other people do about this?

    +

    The whole thing, nobody. I did not find anyone cutting a verse out of a printed page and setting it beside its look-alike. The largest public library of Quran data publishes the two halves as separate downloads and has not joined them: printed-page layouts in one place, and look-alike phrase data in another — 5,277 look-alike phrase entries and 4,001 similar-verse links, alongside twenty approved page layouts. The most-used Quran reader shows no look-alike feature on a verse's own page at all.

    +

    The hard part of it, everybody — and they all do the same thing. Marking a run of text that wraps across several lines of a page is a solved problem, and in four independent traditions it is solved per line, never by drawing a box around the whole run.

    +
      +
    • Every text selection you have ever made. The specification that governs how a highlight is painted says it is not one shape over a range: there is a single overlay for the document, and Each box owns the piece of the overlay corresponding to the text directly inside it. Drag across three wrapped lines in any browser and you get three shapes cut to the words. CSS Pseudo-Elements 4, §3.4 Area of a Highlight
    • +
    • The web platform gives the two shapes different names. You can ask a range for its geometry as a list — one for each box fragment — or as a single rectangle, the smallest rectangle that includes all of the rectangles in list. Two methods, because they are two different answers to two different questions. The app is currently calling the second one. CSSOM View Module Level 1
    • +
    • The standard for annotating part of an image. Its ordinary rectangle selector can say only origin, width and height, and the spec is blunt about the limit — even a simple circular region of an image, or a diagonal line across it, are not possible — so it defines a second, shape-based selector and points you at it whenever the region is not a rectangle. W3C Web Annotation Data Model
    • +
    • The standard for serving a crop of a scanned page can cut only rectangles — the whole image, a square, or a box given in pixels or per cent. There is no multi-part or non-rectangular request in it. That is worth naming, because it explains why our crop is a rectangle: a rectangle is what the tooling hands you, not what the content is. Viewers that need a real shape draw it over the image instead of asking for it. IIIF Image API 3.0, §4.1 Region
    • +
    • The formats that describe scanned pages put the geometry on the line. Both of the widely used ones carry a box per text line, while logical units like a paragraph are containers that group lines rather than shapes of their own. One goes further and defines an element for exactly our case, documented as describing the bounding shape of a block, if it is not rectangular, holding a polygon. A verse crossing three lines is that block. hOCR 1.2 · ALTO 4.4 schema
    • +
    +

    What this changed. It moved option B from the boldest of the set to the conventional one — its only remaining objection being appearance, since none of the precedents above has to look like a page of a mus'haf. The decision went the other way, to F, and the reason is worth stating: every precedent here solves where to draw, and none of them solves what the drawing means. B removes the ambiguity by removing the neighbours; F removes it by naming all three states outright. On a page a reader is trying to memorise from, saying what a mark means beat inheriting a convention about its shape.

    +
    +

    What I could not confirm. I believe a multi-line highlight in a PDF is stored as several quadrilaterals rather than one rectangle — the same convention a fifth time — but both sources I tried for that specification returned nothing, and this session's search budget was spent, so it is not counted above.

    +

    I also could not open the layout or look-alike data files themselves to see whether they carry word positions. The finding about them rests on the library's own descriptions.

    +
    +
    + +
    +

    ConstraintsWhat have we already decided that limits the answer?

    +
      +
    • The printed page is never edited. Whatever the panel does, it does by drawing over the artwork. Moving, re-flowing, or re-setting the letters is not available — that was the whole point of cropping the page instead of retyping it.
    • +
    • The page is paper and stays paper. It is never re-themed, because the printer's ink is a fixed dark that would vanish on a dark ground. Every specimen below is on paper even if you are reading this at night, and that is not an oversight.
    • +
    • The wash must not hide the words it points at. It is translucent for that reason, and a solid fill is not on the table for anything drawn over the ink.
    • +
    • When the comparison cannot be drawn, nothing is drawn and the row keeps its plain note. Any option has to keep that fallback.
    • +
    • Whether a rectangle should line up with a printed line or with a whole page is already an open question here, asked about the marks the app draws over recitation rules. This is the same tension in a second place, and the two should not be answered in opposite directions without somebody saying why.
    • +
    +
    + +
    +
    +

    Side by sideHow do the six compare at a glance?

    +

    The hard side of the pair — 2:123, where a little under half the crop is other verses — drawn six ways at the panel's real width. Scroll sideways. Each one is the actual printed page with the actual measured geometry over it, and the last one is the one chosen.

    +
    + +
    +
    A Leave it as it is
    البقرة · ٢:١٢٣
    B Cut each line down to the verse's own words
    البقرة · ٢:١٢٣
    C Fade everything that is not this verse
    البقرة · ٢:١٢٣
    E Mark where the verse begins and ends
    البقرة · ٢:١٢٣
    D Mark the words the two share, as well as the ones they do not
    البقرة · ٢:١٢٣
    F Fade the neighbours, mark the shared words green and the differing words yellow
    البقرة · ٢:١٢٣
    +
    +
    + +
    +
    +

    The optionsWhat were the choices?

    +

    Six, grouped by what they do about the neighbours: take them away, push them back, leave them and say plainly what the marks mean — or, in the one that was chosen, do two of those at once. They were never exclusive, and F is the proof: it is C and D composed, plus a change to what the colours are about that neither parent asked for.

    +

    The five that lost stay here in full. They are the reason the choice was a choice, and anyone reopening this will want to see what was weighed rather than take the answer on trust.

    +
    + +
    +
    + A +
    +

    Change nothing

    +

    Leave it as it is

    +
    +
    +
    +
    +

    The rectangle stays as it is drawn today: every line the verse touches, edge to edge, whatever else is printed on those lines.

    +
    +
    +

    What it gets you

    +
    • It ships. It is already a large improvement on what it replaced, and it is the only option with no work behind it.
    • The context is not worthless — seeing what runs before and after is how a hafiz places a verse on the page in the first place.
    • The verse numbers are printed in the artwork, so the boundaries are visible to anyone who looks for them.
    +
    +
    +

    What it costs

    +
    • Unmarked does not mean shared, and the panel gives the reader no way to know that.
    • On the lower crop here, the whole first line is the previous verse — the labelled one does not start until line two, and nothing says so.
    • On one crop in six, more of what is drawn belongs to other verses than to the one being named.
    • It is the one option with no precedent behind it: nothing else that marks a run of text on a page marks it this way.
    +
    +
    +
    +
    +
    البقرة · ٢:٤٨ · هنا
    البقرة · ٢:١٢٣
    +
    Drawn on 2:48 and 2:123, at the width the panel really is on a phone. This is a picture of the live app.
    +
    +
    +
    +
    +
    + B +
    +

    Take the neighbours away

    +

    Cut each line down to the verse's own words

    +
    +
    +
    +
    +

    Instead of one rectangle over all the lines, one strip per line, each cut to exactly the words of this verse — kept in its true place, so the shape of the verse on the page survives.

    +
    +
    +

    What it gets you

    +
    • The problem disappears by construction. Nothing is drawn that is not the verse, so unmarked can only mean shared.
    • It is what everything else already does. Selecting three wrapped lines of text in any browser gives three shapes cut to the words, never a rectangle around them — and the formats that describe scanned pages put their geometry on the line for the same reason.
    • It keeps the printer's own line breaks and the verse's real position, so it still looks like the page it came from.
    • It costs nothing to read: no new colour, no new mark, no legend.
    +
    +
    +

    What it costs

    +
    • It is ragged. A verse that opens with three words at the end of a line gets a stub floating above a full line, which looks broken until you understand why.
    • None of the precedents above has to look like a page of a mus'haf — a selection is transient and a scanned newspaper is not a place of reverence.
    • It removes the context argument entirely — including for the readers who were using it.
    • It is the largest change of the five: the crop stops being one picture and becomes several.
    +
    +
    +
    +
    +
    البقرة · ٢:٤٨ · هنا
    البقرة · ٢:١٢٣
    +
    Drawn on 2:48 and 2:123, at the width the panel really is on a phone. Everything here is the printed page — only what is drawn over it changes.
    +
    +
    +
    +
    +
    + C +
    +

    Push the neighbours back

    +

    Fade everything that is not this verse

    +
    +
    +
    +
    +

    One rectangle still, but a translucent veil over every part of it that belongs to another verse, so this one sits forward and the rest recedes.

    +
    +
    +

    What it gets you

    +
    • Context survives and stops competing. You can still read what runs before and after, but only if you go looking.
    • It is a small change — the same single picture, one shape drawn over it.
    • It answers the actual confusion directly: faded is visibly a different category from washed.
    +
    +
    +

    What it costs

    +
    • It puts a veil over printed Quran, which is a heavier thing to do than tinting a few words, and it does it to most of the crop.
    • Three visual states now — faded, plain, washed — where there were two.
    • On a small screen a faint veil may not read as a veil at all; it may just look like uneven printing.
    +
    +
    +
    +
    +
    البقرة · ٢:٤٨ · هنا
    البقرة · ٢:١٢٣
    +
    Drawn on 2:48 and 2:123, at the width the panel really is on a phone. Everything here is the printed page — only what is drawn over it changes.
    +
    +
    +
    +
    +
    + E +
    +

    Push the neighbours back

    +

    Mark where the verse begins and ends

    +
    +
    +
    +
    +

    The rectangle is untouched. Two small brackets are drawn instead — one at the first word, one at the last — so the verse's extent is stated without anything being hidden or dimmed.

    +
    +
    +

    What it gets you

    +
    • The lightest possible intervention: two marks, no colour over the ink, nothing obscured.
    • It states the boundary exactly, which is the fact the reader is missing.
    • It reads the same at any size, unlike a faint veil.
    +
    +
    +

    What it costs

    +
    • It tells you where the verse ends without making the neighbours any quieter — a reader skimming still sees one block of ink.
    • Two more marks in a panel that already carries a coloured wash, on a surface where restraint is the whole aesthetic.
    • A bracket is a convention, and conventions have to be learnt.
    +
    +
    +
    +
    +
    البقرة · ٢:٤٨ · هنا
    البقرة · ٢:١٢٣
    +
    Drawn on 2:48 and 2:123, at the width the panel really is on a phone. Everything here is the printed page — only what is drawn over it changes.
    +
    +
    +
    +
    +
    + D +
    +

    Say what the marks mean

    +

    Mark the words the two share, as well as the ones they do not

    +
    +
    +
    +
    +

    Nothing is removed or dimmed. A third, neutral tint goes over the shared opening — so the two states that carry meaning are both stated, and anything left plain is simply not part of the comparison.

    +
    +
    +

    What it gets you

    +
    • It fixes the actual error rather than its cause: the reader's wrong inference was that plain means shared. Here, shared is marked, so plain means nothing at all.
    • The neighbours keep whatever context value they have, at no cost.
    • It is the smallest change to what is drawn — one more tint, using machinery already there.
    +
    +
    +

    What it costs

    +
    • Almost the whole verse is now under colour, which is close to the opposite of the panel's intent — the wash exists so the eye lands on a few words.
    • Three tints on one crop needs a legend, and there is no room for a legend under a row in a list.
    • It is the only option that makes the crop busier rather than quieter.
    +
    +
    +
    +
    +
    البقرة · ٢:٤٨ · هنا
    البقرة · ٢:١٢٣
    +
    Drawn on 2:48 and 2:123, at the width the panel really is on a phone. Everything here is the printed page — only what is drawn over it changes.
    +
    +
    +
    +
    +
    + F +
    +

    Do both, and change what colour means chosen

    +

    Fade the neighbours, mark the shared words green and the differing words yellow

    +
    +
    +
    +
    +

    C and D at once — the neighbours veiled, the shared opening and the divergent ending both marked — and the two colours spent on what the words are rather than on which verse you are looking at. Green where the two agree, yellow where they part, the same on both halves of the panel.

    +
    +
    +

    What it gets you

    +
    • Every part of the crop now says what it is. Veiled means not this verse, green means the two agree here, yellow means they part here — and nothing is left for the reader to infer.
    • The colour finally carries the fact the panel exists to teach. Which of the two verses you are looking at is already written above each crop in words; whether a phrase is shared or divergent is not written anywhere, and it is the thing a hafiz is trying to learn.
    • Green for agreement and yellow for caution are read the same way by most people before anything explains them, which is the closest this panel gets to needing no legend.
    • The same two colours on both halves means the eye can compare the halves directly — the yellow on top and the yellow below are the same claim, where terracotta and verdigris are two.
    +
    +
    +

    What it costs

    +
    • It gives up the colour that says which verse is which. Today terracotta is the verse you came from and verdigris the one you hopped to, and that distinction now rests on the label alone.
    • It reopens a settled decision. The two wash colours were chosen elsewhere and this changes what they mean, not just which they are — that has to be decided deliberately, not as a side effect of this page.
    • Yellow is the hardest colour to put on cream paper. It has been pushed to an ochre here to be visible at all, and a reader may not accept that as yellow.
    • It is the busiest of the six: a veil and two tints on one small crop, and it inherits every objection to C and to D at once.
    +
    +
    +
    +
    +
    البقرة · ٢:٤٨ · هنا
    البقرة · ٢:١٢٣
    +
    Drawn on 2:48 and 2:123, at the width the panel really is on a phone. Everything here is the printed page — only what is drawn over it changes.
    +
    +
    +
    +
    + +
    +

    RejectedWhat else was considered, and why is it not here?

    +
      +
    • Show the whole page and point at the verse. The panel is a strip underneath a row in a list. A whole page at that size is unreadable, and the reader is already looking at one.
    • +
    • Re-flow the verse's words onto a line of their own. It would move the printer's letters, and showing the reader letters arranged differently from the page is the exact defect that cropping the page was brought in to fix.
    • +
    • Blur the neighbours' ink. Same objection as re-flowing, and a blur laid over printed Quran is a harder thing to defend than a translucent tint of the same strength.
    • +
    • Draw only the words that differ. A difference with no context is not memorable. The shared opening is the reason the pair is confusable in the first place — remove it and the panel stops answering its own question.
    • +
    +
    + +
    +

    SensitivityWhat would reopen this?

    +
      +
    • A hafiz reading the panel and saying the veil gets in the way. Everything against option A rested on a guess about someone else's habit — that the neighbouring verses are a distraction rather than an aid — and F acts on that guess by veiling them. It has still not been tested on a single person who memorises, and that is the assumption most likely to be wrong.
    • +
    • Green and yellow failing on the paper rather than on the screen. Yellow at ten per cent over ink on cream is the hardest thing on this page, and it has only been judged at the sizes shown here. Seen on a phone in daylight it may not hold.
    • +
    • Anyone reading green and yellow as right and wrong. They are meant as the same and not the same. If readers take yellow for an error rather than a difference, the colours are wrong even though the structure is right, and the fix is the palette rather than the drawing.
    • +
    • The panel moving out of a list row. F is busy because the crop is small. On a full screen there is room for the veil to be lighter and the tints weaker, and the balance chosen here would want revisiting.
    • +
    • Any change to how the page is themed. The veil is drawn in the printed page's own paper colour; if that ground ever moves, the veil moves with it.
    • +
    +
    + +
    +

    ScopeWhat is this not settling?

    +
      +
    • Which words get marked. Read off the pair itself, verified, and correct. F changes what the marks look like and what they mean, never which words get them.
    • +
    • The colours anywhere else in the app. Terracotta and verdigris keep their jobs everywhere they already have one. What changed is confined to this panel, where they were saying something the label already said.
    • +
    • The exact green and the exact yellow. Chosen here to be legible at ten per cent over ink on cream, and drawn so they can be argued with. Moving them does not reopen the decision.
    • +
    • Whether the panel should exist at all. It should.
    • +
    • Anything about the printed page. It is vendored unmodified apart from three declared transforms, and nothing here touches that.
    • +
    +
    +
    diff --git a/docs/design/confusion-map-export.data.json b/docs/design/confusion-map-export.data.json new file mode 100644 index 00000000..2b541a69 --- /dev/null +++ b/docs/design/confusion-map-export.data.json @@ -0,0 +1,63 @@ +{ + "$comment": [ + "Committed data for the confusion-map-export options page. Hand-authored, not a", + "build product: the feature is not built yet, so there is no telemetry to extract.", + "What IS real here is (a) the shape of a confusion map and its backup file, drawn", + "from the confusion-points data model, and (b) the size that shape works out to,", + "computed below rather than guessed. The seam list is illustrative and says so on", + "the page; it carries verse references only, never Qur'an text, so the rendered", + "page holds zero Arabic codepoints by construction.", + "", + "Rebuild the page: node scripts/build-confusion-export-options.mjs", + "Reasons: docs/decisions/confusion-map-export.md" + ], + "question": "Should a reader be able to copy their confusion map off the phone — and if so, by which means?", + "recommendation": "B", + "sample": { + "note": "An illustrative slice of one reader's map — verse references only, the way the backup file itself stores them. Real maps grow to dozens of seams over a year.", + "seams": [ + { "from": "2:48", "to": "2:123", "kind": "slip", "state": "every-pass", "count": 5, "last": "last week" }, + { "from": "6:141", "to": "6:151", "kind": "jump", "state": "sometimes", "count": 3, "last": "3 days ago" }, + { "from": "2:281", "to": "2:48", "kind": "slip", "state": "sometimes", "count": 1, "last": "2 weeks ago" }, + { "from": "3:7", "to": "3:29", "kind": "slip", "state": "retired", "count": 4, "last": "quiet 6 weeks" } + ], + "typicalSeamsPerYear": 40, + "bytesPerSeam": 140, + "fileSizeComparison": "smaller than a single photo" + }, + "options": [ + { + "key": "A", + "name": "Keep it on the phone only", + "gist": "Change nothing. The map lives in the phone's private storage and never leaves it. To make it safer, install the app to the home screen.", + "takes": "Nothing — this is what the app does today.", + "gets": "The strongest possible privacy: the map cannot leak, because it never exists anywhere but the one phone.", + "costs": "It can still be lost — to a wipe, a broken phone, a new phone — and when it is, it goes silently and completely.", + "mock": "on-device" + }, + { + "key": "B", + "name": "A file the reader saves and restores", + "gist": "Two plain actions: Save a backup hands the reader one file to keep wherever they like; Restore from a backup reads it back. Nothing leaves unless the reader saves the file, and only where they put it.", + "takes": "A modest, self-contained piece of work — turn the map into a file and back — with no server, no account, and no change to the app's stay-on-the-phone nature.", + "gets": "The map survives a lost phone and moves to a new one, while the reader keeps full control of the one copy that ever leaves.", + "costs": "A saved file is a file — anything the reader lets near it can read it — and a reader who never saves one is no safer than under Option A.", + "mock": "file", + "recommended": true + }, + { + "key": "C", + "name": "A cloud-sync switch the reader controls", + "gist": "The reader turns on a sync switch; from then on the map keeps a copy in the personal cloud built into their phone — surviving a lost phone and appearing on every device they own — with tooling to see and clear what is stored, and the copy reachable through the phone's own Files.", + "takes": "An installed phone app that does not yet exist and is blocked on a licensing question the project is waiting on advice for — plus the switch, the management tooling, and the Files access that keep it under the reader's control. Not reachable this year.", + "gets": "Once switched on, the map is safe without the reader remembering to do anything, and follows them across devices — the strongest protection against loss.", + "costs": "While the switch is on, an intimate record lives continuously on someone else's servers, not only when the reader reaches for it. Making it a switch the reader controls — not something on by default — is what keeps it inside the 'only in the reader's interest' line.", + "mock": "cloud" + } + ], + "changesTheAnswer": [ + "Prior art we have not yet gathered — how faith and intimate-health apps handle export — coming back against even a reader-saved file.", + "The licensing question that blocks the phone app being answered in the app's favour, which promotes Option C from ‘not this year’ to a live choice.", + "Real use showing readers rarely clear their phone's data or lose the map, which shrinks the cost of doing nothing." + ] +} diff --git a/docs/design/confusion-map-export.html b/docs/design/confusion-map-export.html new file mode 100644 index 00000000..b1b2a553 --- /dev/null +++ b/docs/design/confusion-map-export.html @@ -0,0 +1,303 @@ + + + + + +Copy your confusion map off the phone? + + + +
    +

    A decision, still open

    +

    Should a reader be able to copy their confusion map off the phone?

    +

    The confusion map is the private record of exactly where one reader's memory slips from a verse onto a similar-sounding one — built up, slip by slip, over months. Today it lives on the phone and nowhere else. Should there be a way to copy it off, and if so, how?

    + Open · nobody has chosen yet + +
    +

    Why is this being asked now?

    +

    Reviewing the feature, the app's owner asked that the map survive clearing the phone's browser data, "with the option of backing up to a downloaded file and re-uploading" — and, one day, saved to the cloud. Answering that sharpened the app's own rule. The rule had been read as nothing leaves the phone; the truer form is nothing leaves unless it is in the reader's interest, and under their control. So the question is not whether to break a promise — it is which ways of copying the map off actually serve the reader, and at what cost to them.

    +
    + +
    +

    What happens if nobody decides?

    +

    The map stays on the phone only. That works — but the phone can wipe it silently: on one common phone, a web app left unopened for about a week can have its stored data cleared with no warning and no way back. The map is months of a reader's work, and losing it silently is the exact harm the feature exists to avoid. Nothing else is blocked behind this, so it can wait — but every week it waits is a week a map could vanish.

    +
    + +
    +

    What does the map look like, and how big is it?

    +

    Small. The map stores where each slip goes and how it is doing — references and a few words, never the verses themselves. A backup file is plain text a reader could open and read:

    +
    {
    +  "kind": "hifth-confusion-map",
    +  "saved": "2026-08-29",
    +  "seams": [
    +    {
    +      "from": "2:48",
    +      "to": "2:123",
    +      "kind": "slip",
    +      "state": "every-pass",
    +      "count": 5
    +    },
    +    {
    +      "from": "6:141",
    +      "to": "6:151",
    +      "kind": "jump",
    +      "state": "sometimes",
    +      "count": 3
    +    },
    +    {
    +      "from": "2:281",
    +      "to": "2:48",
    +      "kind": "slip",
    +      "state": "sometimes",
    +      "count": 1
    +    },
    +    {
    +      "from": "3:7",
    +      "to": "3:29",
    +      "kind": "slip",
    +      "state": "retired",
    +      "count": 4
    +    }
    +  ]
    +}
    +

    An illustrative slice — a real map grows to dozens of these over a year. About 5 KB for a year of revision — smaller than a single photo.

    +
    + +

    The options

    +

    Each drawn at phone size, showing what the reader would actually see and do.

    +
    + +
    +
    + A +

    Keep it on the phone only

    + +
    +
    +
    +
    +
    Your confusion map
    +
    + +
    + 2:48 → 2:123 + every pass +
    +
    + 6:141 → 6:151 + sometimes +
    +
    + 2:281 → 2:48 + sometimes +
    +
    🔒 Stays on this phone. Nothing to send.
    +
    Install to the home screen to make a silent wipe far less likely.
    +
    +
    +
    +

    Change nothing. The map lives in the phone's private storage and never leaves it. To make it safer, install the app to the home screen.

    +
    +
    Takes
    Nothing — this is what the app does today.
    +
    Gets
    The strongest possible privacy: the map cannot leak, because it never exists anywhere but the one phone.
    +
    Costs
    It can still be lost — to a wipe, a broken phone, a new phone — and when it is, it goes silently and completely.
    +
    +
    +
    +
    + +
    +
    + B +

    A file the reader saves and restores

    + the owner's lean +
    +
    +
    +
    +
    Back up your map
    +
    + + +
    +
    📄 hifth-confusion-map.json
    +
    About 5 KB for a year of revision — smaller than a single photo.
    +
    +
    The file goes only where you put it — your downloads, your own cloud drive, a memory stick.
    +
    +
    +
    +

    Two plain actions: Save a backup hands the reader one file to keep wherever they like; Restore from a backup reads it back. Nothing leaves unless the reader saves the file, and only where they put it.

    +
    +
    Takes
    A modest, self-contained piece of work — turn the map into a file and back — with no server, no account, and no change to the app's stay-on-the-phone nature.
    +
    Gets
    The map survives a lost phone and moves to a new one, while the reader keeps full control of the one copy that ever leaves.
    +
    Costs
    A saved file is a file — anything the reader lets near it can read it — and a reader who never saves one is no safer than under Option A.
    +
    +
    +
    +
    + +
    +
    + C +

    A cloud-sync switch the reader controls

    + +
    +
    +
    +
    +
    Sync
    +
    +
    Keep my map in iCloudon
    +
    ☁ Copied automatically · on all your devices
    +
    Needs the phone app — not available this year.
    +
    Your map lives on the cloud provider's servers all the time, not only when you choose.
    +
    +
    +
    +

    The reader turns on a sync switch; from then on the map keeps a copy in the personal cloud built into their phone — surviving a lost phone and appearing on every device they own — with tooling to see and clear what is stored, and the copy reachable through the phone's own Files.

    +
    +
    Takes
    An installed phone app that does not yet exist and is blocked on a licensing question the project is waiting on advice for — plus the switch, the management tooling, and the Files access that keep it under the reader's control. Not reachable this year.
    +
    Gets
    Once switched on, the map is safe without the reader remembering to do anything, and follows them across devices — the strongest protection against loss.
    +
    Costs
    While the switch is on, an intimate record lives continuously on someone else's servers, not only when the reader reaches for it. Making it a switch the reader controls — not something on by default — is what keeps it inside the 'only in the reader's interest' line.
    +
    +
    +
    +
    +
    + +
    +

    What would change the answer?

    +
      +
    • Prior art we have not yet gathered — how faith and intimate-health apps handle export — coming back against even a reader-saved file.
    • +
    • The licensing question that blocks the phone app being answered in the app's favour, which promotes Option C from ‘not this year’ to a live choice.
    • +
    • Real use showing readers rarely clear their phone's data or lose the map, which shrinks the cost of doing nothing.
    • +
    +
    + +
    +

    What is this not settling?

    +

    It is not settling whether a teacher could ever see a student's map — that is sending the map to another person, a larger question that deserves its own page. It is not settling the shape of the backup file, whether it is locked with a password, or what it is called — those are the building of the file option, once chosen. And it fixes the cloud option's timing no more precisely than "not this year," because that timing belongs to a different, still-unanswered question about the phone app.

    +
    + +

    This page is drawn from committed data and can be rebuilt from scratch; the reasons behind it, in full, live in the project's decision record for this question. Nothing here is chosen — it is put in front of a person so they can choose.

    +
    + + diff --git a/docs/design/confusion-points.md b/docs/design/confusion-points.md new file mode 100644 index 00000000..b5f70440 --- /dev/null +++ b/docs/design/confusion-points.md @@ -0,0 +1,599 @@ +# The confusion points a hafiz keeps for themselves + +**Status:** design only. Nothing here is built. This document is what a later session reads +before turning it into an action plan; it is expected to change as the feature does. + +`docs/design/` holds the document you read *before* touching a feature. +`docs/decisions/loop-*.md` are the retrospective records of what a finished loop settled. +This file is the former. + +--- + +## A few words, defined once + +- **Ayah** — a verse. Named by its surah and number, e.g. *the 48th ayah of surah 2*. +- **Mus'haf** — the printed Qur'an; the actual pages the app draws. +- **Mutashabihat** (المتشابهات) — verses that resemble each other across the Qur'an. Two + passages a page apart or twenty juz apart can open with the same words and then part. They + are the reason a memoriser's tongue takes the wrong turn. +- **Slip** — an *involuntary* wrong turn: you finish one phrase and your memory continues into + the *wrong* verse, because somewhere else in the book a near-identical passage begins the + same way. Resemblance pulled your tongue. This is the error the feature warns you about. +- **Jump** — when your memory *leaps* from a word to another place in the book, usually where + the **same** phrase recurs. A jump can be an error (you skipped ahead) or just a connection + you noticed — it is the neutral sibling of a slip, and the person gets to say which it was. +- **The seam word** — the exact word where two passages part: the last word they share, or the + first word that differs. It is where a slip or a jump actually happens, and marking it there + instead of at the whole ayah is what makes the capture precise. +- **A confusion point** — one recorded slip *or* jump: *from here, my memory went there.* It + has a place it started (down to the seam word, when marked there), usually a place it went, + and which of the two kinds it was. +- **The state of a seam** — how alive a confusion point is *right now*, which changes as you + drill it: one you catch **every pass**, one that catches you **sometimes**, one you **used + to** slip on and have since beaten, and one you have **dismissed** as not really a problem. + The state is a note you own — *watch this one*, *you've got this now* — not a score the app + computes about you. + +--- + +## What is being designed, and why now + +Today the app can tell a hafiz which verses *resemble* the one they are looking at — it reads +that off a corpus of mutashabihat that ships with the app. That is a fact about the Qur'an, +true for everyone. It is not a fact about **this reader**. + +The thing a hafiz actually needs is narrower and entirely personal: not *which verses could +be confused*, but *which verses I keep confusing* — the three or four seams in the whole book +where my own tongue takes the wrong turn, every time, and the correction never quite sticks. +That set is small, it is different for every person, and no dataset can contain it because it +is a fact about one memory. + +An app that only ever shows the shared, public resemblances is a reference. An app that learns +where **you** slip and warns you before you get there is an instrument you come to rely on — +the difference between a dictionary and a coach who has heard you recite. That reliance is the +whole point of building it: a hafiz revising alone, at night, has no teacher to say "careful, +this is the one you always miss." This feature is that sentence, said by the app, at the +moment it matters, because the reader themselves put it there weeks ago. + +It is asked now because everything it needs already exists in the app — a way to select an +ayah, a graph of resembling verses to draw candidate slip-targets from, and a proven pattern +for keeping a private per-reader log on the device. What is missing is small and additive. + +## What it is not + +It is **not** a notebook. Hifth is a navigation instrument, not a place to write. A confusion +point is not a paragraph of reflection; it is two coordinates and a direction — *from here, +toward there* — captured in one gesture and read back as a warning on a page. If a screen in +this feature ever invites the reader to *write*, the design has drifted into an app this one +deliberately is not. + +It is **not** a second mutashabihat dataset. It does not try to be complete, and it makes no +claim that the pairs a reader records are the "real" look-alikes. It records exactly what one +person hit, nothing more — and that narrowness is the value, not a shortfall. + +It is **not** a score. There is no accuracy percentage, no streak, no "verses mastered." A +number about someone's worship that the data cannot actually support is a mistake this +codebase has already named and refused elsewhere, and it would be the same mistake here. The +per-seam *state* below (*every pass*, *used to*) is the near-miss to watch: it stays a note the +reader owns about one seam, never a total added up across seams — the moment it becomes a count, +it has become the score. + +--- + +## What the experience is + +Three moments: catching a slip the instant it happens, being offered the right target so you +rarely have to hunt for it, and being warned near your own known slips ever after. + +### How do you record a slip without breaking your recitation? + +The rule the whole capture rests on: **it must cost one gesture from the ayah you are already +on, and it must never make you type a verse number.** A hafiz mid-revision who has to stop, +open a menu, and key in "37:130" will do it twice and never again. So the capture is built +*out of the slip itself*. + +Here is the moment. You are reciting; you were on an ayah; your memory just jumped the rails. +That ayah is already selected — selecting it is how you were reading. A single control rides +with the selection: + +``` + ┌─────────────────────────────────────────────┐ + │ … the page, with your ayah lit in amber … │ + │ │ + │ ┌──────────────────────┐ │ + │ selected ayah ▓▓▓ │ ↻ متشابهات (4) │ │ ← the hop rail, already here + │ ├──────────────────────┤ │ + │ │ ⚠ التبستُ هنا │ │ ← the one new control: "I slipped here" + │ └──────────────────────┘ │ + └─────────────────────────────────────────────┘ +``` + +One tap on *«التبستُ هنا»* (*I slipped here*) opens the capture sheet. It arrives already +knowing where you started, so the only question left is where you *went*: + +``` + ┌─────────────────────────────────────────────┐ + │ التبستُ عند ٢ : ٤٨ [ source ] │ + │ ذهب حفظي نحو… ? (my memory ran toward…) │ + │ ───────────────────────────────────────────│ + │ ↻ ٢ : ١٢٣ …same wording, context differs│ ← candidates, drawn from + │ وَٱتَّقُواْ يَوْمًا … ▟ shared words shown │ the app's resemblance graph + │ ≈ ٢ : ٢٨١ …opens the same, then parts │ + │ ↻ ٣ : ٩ … │ + │ ───────────────────────────────────────────│ + │ ✎ آية أخرى… (another verse — pick it) │ ← escape hatch: the jumper + │ ? لستُ متأكدًا بعد (not sure yet) │ ← capture source-only, resolve later + └─────────────────────────────────────────────┘ +``` + +In the common case the verse you slipped toward is **already in the list**, because the verse +you slip toward is, almost by definition, one that resembles the one you were on — which is +exactly what the resemblance graph already knows. So the whole capture is: one tap to open, +one tap on the right candidate. Two taps, no typing, no leaving the page. You are back +reciting in the time it took to notice you were wrong. + +Two exits handle the rest. *«آية أخرى»* hands you the existing surah-and-ayah jumper for the +times your slip landed somewhere the graph did not predict. *«لستُ متأكدًا بعد»* records the +source alone — a confusion point with a known start and an open end — so a slip is never lost +just because you could not name where it went in the half-second you had; you resolve it later +from the review list. + +And if the same slip happens again next week, capturing it a second time does not make a +duplicate — it deepens the one you have, so the review side can tell a slip you hit once from +a seam you hit every single pass. + +### What if I know the exact word — not just the verse? + +The ayah-level control above is the fast path: it costs one gesture and asks nothing but *which +verse*. But often you know more — you know the exact word where your tongue took the wrong turn, +because that word *is* the moment it happened. There is a second, more precise way in for that +case, and it costs the app nothing new: **hard-press the word itself.** + +The app already has a hold gesture that drops to word granularity — it is the reason the +ayah-level control was deliberately built *not* to collide with it. A firm press on a single +word hangs off that existing hold, which also means the app is already intercepting the touch, +so it sidesteps the phone's own text-selection menu that would otherwise fight for the same +gesture. The press raises a small menu anchored to the word, and the menu is where *slip* and +*jump* part ways: + +``` + ┌─────────────────────────────────────────────┐ + │ … وَٱتَّقُواْ [يَوْمًا] لَّا تَجْزِى … │ ← hard-press one word + │ ┌──────────────────────────┐ │ + │ │ ⚠ التبستُ هنا slip │ │ ← pulled onto a look-alike (a warning, later) + │ │ ↷ قفزتُ من هنا jump │ │ ← leapt to the same phrase elsewhere + │ │ ✎ علّمها… mark │ │ ← just flag the word, decide which later + │ └──────────────────────────┘ │ + └─────────────────────────────────────────────┘ +``` + +Two things the whole-ayah path cannot give come for free here. First, the pressed word is the +**seam word** — the precise point of divergence — so the capture records not just *from this +verse* but *from this word*, and the candidate list can rank by which verses share the opening +*up to and including that word*. That is exactly the opening-word signal open question ③ worries +about having to *build* an index for: when the reader marks the word, the app is handed the seam +instead of having to infer it. Second, the menu is where the reader **names the kind**: + +- ***I slipped* (التبستُ)** — an involuntary wrong turn. This is the one that becomes a private + *warning* on the page later, and its likely target is a verse that merely *resembles* this one, + so the sheet ranks resembling verses first. +- ***I jumped* (قفزتُ)** — a leap to another place, usually where the *identical* phrase recurs. + It may be an error or just a connection worth keeping; its likely target is the word-for-word + **twin** the graph already flags, so the sheet surfaces twins first. Because a jump is a real + path the reader's memory takes — not only a mistake — it is the natural candidate to become a + *navigable* link rather than only a warning (open question ④). +- ***Just mark it*** — flag the word now, choose slip-or-jump and the target later from the + review list, for the half-second when you cannot say more. + +The verb the reader picks is not cosmetic: it decides how the candidate list is ranked, and +which read surface the point feeds — a warning for a slip, a possible edge for a jump. The two +capture paths coexist by design: the visible ayah-level control stays the discoverable default +and the "I only know the verse" path; the hard-press is the precision path for "I know the word, +and I know whether I slipped or jumped." + +### Where do the suggested targets come from? + +The candidate list is the sharpest part of the experience, so it is worth being exact about +its source. The app already carries, for most ayahs, a set of typed links to verses that +resemble them — the same links the app's core navigation is built on. When you open the +capture sheet on an ayah, those links *are* the candidates. Nothing new is fetched or +computed for the common case; the feature reuses what is already on the device and offline. + +The ranking, though, wants to be different from the ranking navigation uses, because a slip is +**directional and happens at a seam**. A memoriser reciting forward slips at the *opening* of +the next phrase — the wrong continuation begins with the same words as the right one. So the +best slip-candidate is not merely the verse that shares the most words *somewhere*; it is the +verse whose *opening* matches, and above all the verse that is **word-for-word identical** to +the one you were on (the graph already flags these — they are the cruelest slips and belong at +the very top of the list). + +There is an honest gap here, and the design names it rather than papering over it. The +resemblance graph the app ships is **curated and deliberately not exhaustive** — it covers +roughly a quarter of the book's ayahs, not all of them. For an ayah the graph says nothing +about, the capture sheet would open with an empty candidate list and fall back to the jumper — +workable, but not the one-tap experience. Closing that gap needs a similarity signal the app +does not compute today: an index of **shared opening words** (the first few words of each +ayah's likely continuation), or shared *rasm* (the bare consonant skeleton, which is what the +eye and tongue actually confuse), so that even an un-curated ayah can offer candidates. That +signal is new work, described in *What is it built on* below, and it is the one part of the +smart-suggestion that is not free. + +### What is the captured set *for*? + +Capturing is worthless if nothing reads it back. The point of writing these down is that *we +are now aware of them* — so the reading side is the feature, and the capture merely feeds it. +Three faces, in rough order of value: + +**A quiet marker at the seam — details when you want them.** This is the payoff, and it must +not shout. An earlier draft popped an alert onto the page the instant you landed on a logged +ayah; a reviser found it too intrusive, and they were right — an interruption on every pass is +the fastest way to get a feature switched off. Instead, a small **breakpoint-style marker** rests +right after the word you slip on — like a debugger's breakpoint at the exact line where execution +derails. It sits there quietly, coloured by the seam's state, and interrupts nothing: + +``` + ┌─────────────────────────────────────────────┐ + │ … وَٱتَّقُوا۟ يَوْمًا ● لَّا تَجْزِى نَفْسٌ … │ ← the marker rests after the seam word + └─────────────────────────────────────────────┘ + tap the marker ↓ (only when you want to dig in) + ┌─────────────────────────────────────────────┐ + │ recorded slip every pass │ + │ you slip at يَوْمًا toward 2:123 │ + │ 5 times · last time last week │ + │ [ ↔ compare the seams ] [ ✓ beat it ] │ + └─────────────────────────────────────────────┘ +``` + +Nothing opens until you tap the marker. Then the recorded slippage: where your memory goes, how +often and how recently, the seam's state, and two actions — hop to the verse you confuse it with +so you can compare the two seams deliberately (the correction a teacher would walk you through), +or mark it *beaten* the day you stop slipping on it. The marker's colour alone already carries the +one bit that matters — how live the seam is — so even unopened it earns its place. This is the +reliance argument made concrete: the app knows your weak point and marks it, without ever getting +in your way. + +**A list to review.** Every confusion point, browsable — sorted by how often you hit it (the +seams you slip on *every* pass float to the top) and by how recently. Each row names both ends +and jumps to either. This is the pre-revision glance: *these are my known weak points; let me +drill them before I start.* + +**A map of your own weak seams.** A picture of the mus'haf coloured by where your slips +cluster — the personal twin of the resemblance map the app already draws for everyone. It +tells a hafiz something no dataset can: *my confusions are all in the last third*, or *they +cluster around the stories that repeat*. This is the most speculative of the three and the +last to build; the warning and the list carry the feature on their own. + +### Not every seam is equally alive — the state a confusion point moves through + +A flat list of everywhere you ever slipped is the wrong thing to read back. What a reviser +needs to know is which seams are still live — and, just as much, which ones they have *beaten*, +because that is the progress the feature exists to earn. So every confusion point carries a +**state**, and the state changes as the reader works the seam: + +- **Every pass** — this one catches you each time. It warns loudest, sorts to the top of the + list, and burns hottest on the map. It is the *this is the one you always miss* the feature + was built to say. +- **Sometimes** — it catches you on some passes and not others. A quieter warning, mid-weight + on the list and map. Most seams live here. +- **Used to** — you slipped here, and you have since beaten it. It stops nagging: no warning, or + at most a faint *you beat this one — still solid?* It stays visible on the list and the map, + in the calm colour, because a seam you conquered is a thing worth seeing. This is the + encouraging half of the whole feature — the app remembers your wins, not only your weak spots. +- **Dismissed** — you flagged it (or a single stray capture did) and it is not, on reflection, a + real problem. It goes quiet and drops off the list. Distinct from *used to*: nothing was + beaten, it simply was not a seam. (The true "I never slip here" is the seam you never logged + at all — this state only exists to retire a false flag so it stops nagging.) + +The state moves along a natural arc. A new capture is born **sometimes**. Capturing it again and +again may nudge it toward **every pass**. And the moment that matters most is the good one: you +land on a seam, the warning fires, and this time you *don't* slip — a single tap on *«beat it»* +retires it to **used to**. A flag you decide is noise gets **dismissed**. The three read +surfaces all read the state: the warning changes weight by it, the list groups by it (live seams +to drill up top, beaten seams below), and the map colours by it, adding a calm fourth colour for +the seams you have conquered. + +Two guards, because this is exactly the shape that decays into a score. First, **the reader owns +the state.** *Every pass* / *used to* are notes a person sets about themselves, the way a teacher +writes *watch this* or *you've got this now* in a margin — not a rate the app computes, which it +could not honestly compute anyway, since it sees your *captures*, never your recitation. The app +may at most **nudge** — *nothing logged here in six weeks; retire it?* — and never assert. Second, +**the states are never added up.** There is no "seams beaten: 12", no percentage, no streak +across seams; each state describes one seam's own standing and stops there. The instant a state +becomes a number about the reader, it has become the score this feature refused. + +--- + +## What is it built on? + +*(This section names files and symbols; it is the one section that does. Everything above and +below is meant to be followed by someone who has never opened the repository.)* + +Most of this feature is assembly of parts the app already has. The honest split: + +### Reused, essentially as-is + +- **Selecting the ayah you slipped on, and pressing the seam word.** `packages/core/src/resolver.ts` + (`class Resolver`) turns a point on the page into an ayah key like `quran/hafs-kfqc/2:48`; + `pointerIntent` in `packages/core/src/gestures.ts` already classifies taps, holds, and + marquees. Both capture paths reuse this and add no gesture: the ayah-level control hangs off + the existing selection, and the word-level hard-press reuses the **hold that already drops to + word granularity** — the menu is new UI on an existing gesture, not a new gesture, and because + `pointerIntent` already claims that hold, the word menu does not fight the phone's native + text-selection callout. +- **The candidate slip-targets.** `packages/core/src/adjacency.ts` is the reuse that makes the + smart suggestion nearly free. Its `Edge` interface already carries everything the capture + sheet needs: `type` (`"mutashabih" | "related-meaning" | "shared-root"`), `to` (the target + key), `page`, a precomputed `dir`, an optional `span`/`toSpan` (which words overlap), and — + crucially — `twin` (identical wording, the sharpest slip). The per-ayah shards live at + `apps/web/public/assets/adj/hafs-kfqc/`, already on the device and offline. `bucketEdges` + and especially `orderForHifz` are the starting point for the slip-candidate ranking. +- **Showing which words two look-alike ayahs share.** `packages/core/src/verse-diff.ts` + (`wordDiff`) already computes the diff as arithmetic on word indices — no scripture text in + the bundle. The capture sheet's "shared words shown" line and the warning's compare-hop reuse + it directly. +- **The escape-hatch jumper.** The `wayfinding` feature's surah/ayah jumper is the "another + verse…" path. +- **The persistence pattern, wholesale.** `apps/web/src/revision-store.ts` + + `packages/core/src/revision.ts` are the exact template: a **pure, clockless core module** + and a single **impure store** over IndexedDB, with a `since` stamp written when the store is + first opened, a once-per-day prune, and a privacy gate (`gate:revision-privacy`) that holds a + closed allow-list of modules permitted to import it and forbids any `fetch`/URL construction + inside it. `apps/web/src/storage.ts` already owns the durability/eviction story this store + inherits. + +### New, and honestly costed + +- **The personal confusion store.** A new pure module (say `packages/core/src/confusion.ts`) + and its store (`apps/web/src/confusion-store.ts`), following revision-record beat for beat. A + confusion point is small — a *from* key, an optional *to* key, an optional **seam-word span** + (filled when captured by hard-press on a word, empty when captured at the ayah), a **`kind`** + of `"slip" | "jump"` (the verb the reader chose, which routes ranking and read-surface), a + first-seen and last-seen stamp, a hit `count`, and a reader-owned **`state`** of + `"sometimes" | "every-pass" | "retired" | "dismissed"` (default `"sometimes"` on first + capture) that drives warning weight, list grouping and map colour; keyed by the *(from, to, + kind)* triple so re-capturing the same slip increments `count` rather than duplicating, while a + slip and a jump between the same pair stay distinct. The `state` is set by the reader, never + computed — the store may surface a nudge from `count` + last-seen but writes `state` only on an + explicit act. Plus its own privacy gate, `gate:confusion-privacy`, the same shape as + revision's. +- **The slip-candidate ranking.** New, but small: an ordering over existing `Edge`s that puts + `twin` first, then opening-word overlap, then longest shared run. It lives beside + `orderForHifz`, not inside the store. +- **The opening-word / rasm similarity signal** — the only genuinely new *data* work, and only + needed to give candidates for ayahs the curated corpus does not cover. Options range from a + build-time index of each ayah's opening n-gram (ETL, beside `build-adjacency.mjs`) to a + runtime rasm comparison. Deferrable: phase 1 ships with corpus-only candidates and the jumper + fallback, and this signal is what later makes the empty-list case rare. +- **The three read surfaces** — the at-selection warning (a new chip near the selection, + wired in `apps/web/src/App.tsx` the way the revision `since` line is), the review list, and + the personal map (a near-clone of `apps/web/src/components/RevisionMap.tsx`). + +--- + +## What we already know that constrains this + +- **The resemblance corpus is deliberately partial.** It is curated, covers roughly a quarter + of the book's ayahs, and its maintainers chose selectivity on purpose. So corpus-only + candidates will sometimes come up empty, and the design must degrade to the jumper without + apology — the same way the app already degrades a hop whose target page is not vendored. +- **On-device storage is not permanent on iOS.** iOS deletes script-writable storage after + seven days without interaction, and `navigator.storage.persist()` is not documented to stop + it. The revision record met this by stamping `since` so an emptied log reads as *young* + rather than as *you have revised nothing*. A confusion map is **more** painful to lose than a + glance log — it is weeks of hard-won self-knowledge — which sharpens rather than settles the + open question about durability below. On iOS the real defence is Home-Screen install, which + the app already treats as a feature. +- **A private log must stay private by construction, not by good intentions.** The revision + record's privacy is enforced by a gate, not a doc comment, precisely because "add it to the + share sheet" is always one convenient import away. This feature inherits that stance + unchanged: a record of where a particular person's memory fails is at least as sensitive as a + record of when they read. +- **An annotation layer must not ride along inside a shared link.** A past loop already settled + that a beta annotation layer stays *out* of the URL parameters, so that opening someone else's + shared link can never silently switch a personal layer on for a reader who never chose it + (`docs/decisions/loop-6a.md`). A confusion map is exactly such a layer, and it inherits that + rule: the warning, the list and the map are reached from the app's own chrome, never encoded + in a link. + +## What people outside this project do about it — and a caveat + +**I did not do a fresh external scan for this design.** Known mutashabihat study tools exist +(the problem is old — there are printed books and a handful of apps organised around it), and +the app's own vendored resemblance corpus, itself a Qari's hand-curated confusion list, is +prior art for *the public half* of the problem. There is also relevant prior art **already in +this repo**: an earlier decision looked at a larger external mutashabihat collection and at the +W3C Web Annotation Data Model as a way to structure annotations +(`docs/decisions/comparison-crop.md`), which the personal-note shape here should be checked +against before it is fixed. What I did *not* survey is how existing tools handle the +*personal-capture* half — one-tap logging, and warning at the point of risk — so that is owed a +proper look before the plan is costed, and the review-list and map designs above should be +checked against whatever it turns up. Flagging it rather than pretending I looked. + +## Deliberately out of scope + +- **Free-text notes of any length.** At most a short optional label; never a journal. See + *What it is not.* +- **Any score, streak, or completion metric.** The data cannot see recitation, so a number + over it would be a claim about worship the record does not support. +- **Turning your personal slips into shared navigation edges** — i.e. feeding captured pairs + back into the hop rail everyone sees. That blurs a private log into the public routing table + and is a privacy and provenance decision, not a default. Listed as an open question, not + built. + +## Open questions, and what would answer each + +Every design doc in this repo ends under this heading, and each item is an +`### ⓝ … · **status**` row so the issues gate can read it. The status vocabulary is defined in +`docs/issues.json`. + +### ① How durable must a confusion map be, given iOS wipes it? · **open** + +Following revision-record's pattern gives IndexedDB plus a `since` stamp — enough to keep an +emptied log from lying about itself, not enough to keep it from emptying. For a glance log that +is an accepted cost. A confusion map is different in weight: it is the distilled product of +months of revision, and losing it silently is a real harm to the exact reliance this feature +is built to earn. **What would answer it:** a human's call on whether `since` + Home-Screen +install is a sufficient defence, or whether this feature is the one that finally justifies an +export/backup path (see ②) — and if the latter, that changes the privacy stance below. This is +the sharpest question and the one most worth settling before code. + +**Owner's direction (2026-08-29):** reviewing the walkthrough, the owner asked that the map be +*durable storage* on the web that survives a cache flush, *with the option of backing up to a +downloaded file and re-uploading it*, and — longer term — saved to iCloud once there is a mobile +app. That is a clear lean toward "yes, this justifies a backup path," which moves the answer here +and hands the privacy question in ② its leading option. Captured, not yet settled: the file-backup +half is now being opened as a full decision (see ②), and the iCloud/sync half rides on the mobile +track, which is gated elsewhere. + +### ② Should a confusion map ever leave the device? · **open** + +Revision-record's settled stance is that anything off the device is a *deliberate* act, guarded +by a gate — no export, no sync. Two real pulls argue the other way here: a hafiz wants to back +up something this precious (question ①), and a **teacher** wants to see a student's confusion +map to drill them on it, which is a genuinely valuable use this app's navigation nature does +not otherwise serve. Against both: a record of where a person's memory of the Qur'an fails is +intimate, and "export" is the first step of every privacy erosion. **What would answer it:** a +decision, and because it is a genuine either/or with a privacy cost, it wants a decision record +and a published options page — see the register note below. It is not settled here. + +**Owner's direction (2026-08-29), and the two mechanisms it names.** The owner leans toward +letting the map leave the device, and named two distinct ways of doing it, which this question +must keep apart because they carry very different privacy costs: + +- **A user-controlled file backup** — export to a downloaded file, re-upload to restore. Nothing + leaves the device unless the reader deliberately saves the file, and it goes only where they put + it. This is the lighter-cost mechanism and the leading option; it is the half now being opened as + a full decision. +- **Automatic cloud/iCloud sync** — the map copies itself to a third-party cloud so it survives a + lost phone and follows the reader across devices. Materially heavier: an intimate record now lives + on someone else's server continuously, not only when the reader chooses. On the web this is not + directly reachable at all (a browser app cannot sync to iCloud); native iCloud/CloudKit needs the + **mobile-app wrap**, which is roadmap Track B and gated on the app-store licensing question + (`gpl-and-the-app-store`). So this mechanism is not a near-term option and is tracked as riding on + that track, not opened here. + +The teacher-sharing pull above is a *third* mechanism again and stays part of this question. The +decision being opened covers the file-backup option first; sync and sharing are named in it as the +heavier options it is deliberately not taking yet. + +### ③ How are slip-candidates ranked, and is the opening-word signal worth building? · **open** + +Phase 1 can ship with corpus edges ranked `twin` → shared-run and the jumper as the fallback +for un-covered ayahs. Whether that is *good enough*, or whether the opening-word / rasm signal +is needed to make the common case truly one-tap, is a judgment about how often real slips land +on ayahs the curated corpus omits. **What would answer it:** a hafiz trying capture on their +own real slips and reporting how often the right target was absent from the list — a +validation-ledger candidate, not something to settle from the renderer. The ranking order +itself (does opening-word overlap beat longest-shared-run for *slip* prediction?) wants the +same human judgment. Note that hard-press capture *changes the shape of this question*: when the +reader marks the seam word, they hand the app the opening-word signal directly, so the built +opening-word / rasm index is needed only for ayahs captured at the *whole-verse* level — which +shrinks, but does not remove, the case for building it. + +### ④ Does a captured slip become navigable like a corpus edge? · **open** + +Should your own confusion points appear in the hop rail as a fourth kind of link — *your* +edges, alongside the corpus's — so the app you navigate is shaped by your own history? It is +appealing and it is a privacy/provenance question: it mixes a private log into the surface that +draws public, sourced links, and the store's whole privacy gate is built to keep those apart. +**What would answer it:** a decision on whether the warning-at-selection surface (which keeps +the log private) already delivers this value without merging the two, which is the current +design's bet. The slip/jump distinction narrows the question usefully: a **slip** is an error +and belongs on the warning surface, where the current bet keeps it; a **jump** is a real path +the reader's memory takes, which is a far more natural thing to make navigable. So the sharper +form of this question is *whether jumps — not all confusion points — become edges*, and slips +stay warnings regardless. + +### ⑤ Who sets a seam's state — the reader, or the app's guess from the data? · **open** + +Each confusion point carries a state — *every pass*, *sometimes*, *used to*, *dismissed* — and +the question is who writes it. The design's bet is **the reader owns it**, because the store sees +*captures*, not recitation: silence at a seam could mean *I beat it* or *I stopped logging*, and +the app cannot tell which, so a state it computed would be the very score this feature refused. +The counter-pull is friction: a reader who never taps *«beat it»* leaves conquered seams warning +forever, and a gentle inference (*nothing here in six weeks — retire it?*) would spare them. **What +would answer it:** a human's call on how strong the nudge may be — whether the app is allowed to +*suggest* a state change from `count` + recency (and how loudly), while the write itself stays an +explicit act — or whether even a suggestion leans too far toward measuring worship. Leaning +reader-owned-with-a-quiet-nudge, but the line between *nudge* and *assertion* is the thing to +settle, and it echoes the durability and score stances above. + +### ⑥ Should a student be able to hand their confusion map to a teacher? · **open** + +A hafiz revises with a teacher, and the teacher's whole job is to drill the student exactly +where they are weak — which is precisely what the confusion map records. So there is a real +pull to let a student *share* their map with a teacher, and the owner has said yes to it as a +thing worth having, and asked that it be its own feature rather than folded into anything else. +It is kept apart from the backup question on purpose: a backup is a reader keeping their own +copy safe, while sharing sends an intimate record — where *this person's* memory of the Qur'an +fails — to *another person*, which is a larger privacy step and a different consent. **What +would answer it:** its own decision, drawn like any other — who initiates the share and who can +undo it, whether the teacher sees a live map or a snapshot the student chose, and whether this +rides on the file-backup mechanism (a student sends a teacher the file they already can save) +or wants something built for it. It should not borrow the export decision's answer; that +decision deliberately does not settle this. + +### ⑦ Should the app learn where *most* readers slip, not just where you do? · **open** + +Every reader's map is a record of the places the same few look-alike verses trip people up, +and across many readers those places would cluster — there are verses the whole tradition warns +about. The owner has asked whether the app could do that analysis: pool what many readers slip +on to find where people most commonly slip. The value is real — it could seed a new reader's +warnings before they have slipped even once, and it could tell us which verses the resemblance +model most needs to cover. But it is a sharp turn away from everything else this feature is: +every other part keeps one reader's map on one reader's phone and lets nothing leave without a +deliberate act, and this asks to gather many readers' maps in one place. That is not an +extension of the private design; it is close to its opposite, and it carries the heaviest +privacy question the project has yet raised — an intimate record, pooled, off-device, by +default. **What would answer it:** a decision that treats the pooling itself as the thing being +weighed — whether it can be done only with data a reader knowingly contributes, whether what is +pooled can be stripped of anything identifying and still be useful, and whether the app needs a +server at all to get the benefit, or whether the common slip points are already knowable from +the corpus without touching anyone's map. Named here so it is not lost; built by nobody until +that decision is made. + +--- + +## A rough plan, in order — not yet costed + +An implementer would likely proceed in these phases. Ordered by dependency, not committed to +dates; each phase is useful shipped alone. + +1. **Settle the stance (a decision, not code).** Answer open questions ① and ② at least + provisionally, because they decide whether the store needs an export path and how strict the + privacy gate is. Register the feature and open the export/share decision (see below). +2. **Capture, corpus-only.** The pure confusion model and its IndexedDB store, the privacy + gate, the `since` stamp and daily prune — all cloned from revision-record. The one-tap + *«التبستُ هنا»* control on the selection, and the capture sheet drawing candidates straight + from the existing resemblance edges with the `twin`-first ranking. Jumper fallback for + empty-candidate ayahs. This is a whole, useful feature on its own: slips get recorded. +3. **The warning at the point of risk, and the review list.** Wire the at-selection warning + chip in the app shell, reusing the verse-diff for the compare-hop; build the review list + with jump-to-either-end and the hit-count sort. This is where capture starts paying back — + arguably the real v1. +4. **The personal confusion map.** The picture, a near-clone of the revision map, coloured by + slip clusters. Highest polish, lowest urgency. +5. **(Gated on question ③) The opening-word / rasm similarity signal.** Only if phase-2 + validation shows corpus-only candidates come up empty too often. New ETL or runtime work to + give candidates for un-curated ayahs. +6. **(Gated on question ② saying yes) Export / teacher-share.** Only if the decision goes that + way, and built as the deliberate, gated act the privacy stance demands. + +## Where a row would go (recommendation only — nothing edited here) + +- **`docs/map.json`** — a new `features` row, `confusion-points`, layer `L1 core`, pointing at + the new pure module, its store, the ranking, and the read surfaces, in the same shape as the + `revision-record` row. +- **`docs/issues.json`** — the seven open questions above become rows: ①, ②, ⑤, ⑥ and ⑦ as + `question` severity (owner likely `user`), ③ as a validation-ledger-linked `risk`, ④ as a + `question`. Two of them, ② and ⑥, are the kind that want a full drawn decision, not just a + row; ⑦ is heavier still and is flagged as the project's sharpest privacy question to date. +- **`docs/decisions.json`** — open question ② (does a confusion map leave the device?) is a + genuine either/or with a privacy cost and is the one that wants a full decision record. Note + the gate's requirement: an *open* decision row must carry both a published `artifact` page and + a checked-in `page`, drawn on a real mus'haf, or the decisions gate fails — so opening this row + means producing that options page, not just adding a line. +- **`docs/use-cases.json`** — a new hafiz case, roughly *"I keep slipping from this ayah into + that one — let me record it so the app warns me next time,"* which is the personal companion to + the existing `hop-to-something-that-resembles-it` case. + diff --git a/docs/design/decision-board.html b/docs/design/decision-board.html new file mode 100644 index 00000000..b1141966 --- /dev/null +++ b/docs/design/decision-board.html @@ -0,0 +1,584 @@ +Hifth Decision Board + + +
    +
    +

    Hifth · the decision board

    +

    What has been decided here, and what is still waiting on somebody?

    +

    Every question this project has had to answer, on one page: the ones that + are settled, the ones nobody has chosen yet, and the lines between them that mean two + answers have to agree. Each card carries the question in plain words, the options as they + were actually put, and a way through to the drawing and to the reasons. The last section is + the shelf: every page this project has published for somebody to look at, including the ones + that are not attached to any decision at all.

    + +
      +
    • 3still open
    • +
    • 15decided
    • +
    • 1never finished
    • +
    • 15constraints, across 13 of them
    • +
    • 55.4kwords of reasons behind it
    • +
    • 9pages published, 4 with no copy
    • +
    + + +
    + + + + + Should the app colour tajweed one colour a verse, or should it colour the exact letter a rule lands on?Should the app line up what it draws one page at a time, or one printed line at a time?When the app shows a verse cut out of the printed page, what should it do about the neighbouring verses that come with it? — decided 16 August 2026Should a reader be able to change the colours the app uses for recitation rules, and how many colours are there to change?When should a wide window behave differently from a phone?Should rolling the mouse wheel magnify the page? — decided 6 August 2026When a reader picks out a few words, what should the app go looking for? — decided 5 August 2026Should a reader be able to pick single words, or only whole verses? — decided 4 August 2026How does the app keep a page it was told to save without storing it twice? — decided 4 August 2026How does the app's own wording get translated, and what keeps the two languages in step? — decided 30 July 2026Can a real page of the mus'haf be put on a phone, installed, and opened again offline? — decided 20 July 2026Does touching a verse on a phone answer fast enough to build the rest on? — decided 20 July 2026Can a reader jump from a verse to one that sounds like it, and find the way back? — decided 23 July 2026Can a teacher send a link that opens exactly what they were looking at? — decided 25 July 2026Where does the list of look-alike verses come from, and is it the same list every time it is built? — decided 25 July 2026Can the app carry the whole mus'haf instead of three pages? — decided 3 August 2026Can a reader mark a stretch of more than one verse and ask about all of it at once? — decided 25 July 2026Can a reader turn the colouring off, jump anywhere in the book, and reopen it with no signal? — decided 25 July 2026Can a reader keep one thirtieth of the book on their phone for good? — decided 4 August 2026 + JulyAugust + no date — still live + +
    + Each dot is a decision, on the day it was made. A line between two of them means one + holds the other up: change either and somebody has to look at both. The four to the right + of the break have no date because they have no answer yet — three nobody has chosen, and + one that is never finished by design. +
    +
    +
    + +
    +
    +

    Waiting on youWhat is still open?

    +

    These are the only ones anybody can still act on. Each has its options drawn on a real + page of the mus'haf, at the size they would actually be used, because a wash you cannot + see at that size is an answer and it is one no paragraph would have given you.

    +

    Nothing here breaks while it stays open — an unanswered question is the normal state of + a live project. What it costs is written on each one's own page.

    +
    +
    +
    +
    Open · nobody has chosen4,392 words of reasons
    +

    Should the app colour tajweed one colour a verse, or should it colour the exact letter a rule lands on?

    +

    Written up asSub-word marks: the level below a word, and the print that already named it

    +
    1. ALeave it as it is — one colour a verse
    2. BColour the exact letter or mark
    3. CPrepare the ground, colour nothing yet
    + + +
    +
    +
    Open · nobody has chosen25,505 words of reasons
    +

    Should the app line up what it draws one page at a time, or one printed line at a time?

    +

    Written up asAre the marks in the right place, and how would we know?

    +
    1. ALeave the rectangles where they are
    2. BLine each page up as a whole
    3. FLine each printed line up on its own, and let it tilt
    4. GLine the marks up, and leave the words where they are
    5. ILine each printed line up on its own, and let it bend
    6. HPut each mark where its own ink is, and line the rest up
    + + +
    +
    +
    Open · nobody has chosen1,639 words of reasons
    +

    Should a reader be able to change the colours the app uses for recitation rules, and how many colours are there to change?

    +

    Written up asWhose colours are they?

    +
    1. ALeave the colours fixed
    2. BA reader recolours the seven groups
    3. CA reader recolours all eighteen rules
    4. DOffer a few ready-made schemes, no picker
    + + +
    +
    + +
    +
    +

    Never finishedWhat gets added to rather than answered?

    +

    One record is a running list by design. Every time a choice goes differently on a wide + window than on a phone, a row goes here with the reason for the difference — and the reason + is the column it exists for. A table of two behaviours with no reasons is a list of + inconsistencies.

    +
    +
    +
    +
    Never finished · added to as it happens2,884 words of reasons
    +

    When should a wide window behave differently from a phone?

    +

    The answer, in one lineDesktop vs mobile — the running record

    + +
    +

    Held up by, and holding up

    + +
    + +
    +
    + +
    +
    +

    SettledWhat has already been answered?

    +

    Newest first. The line under each question is the record's own title, which is the + answer stated in one sentence — read at build time from the record itself rather than + copied here, so it cannot quietly stop being true.

    +
    +
    +
    +
    Decided 16 August 2026 · omar2,091 words of reasons
    +

    When the app shows a verse cut out of the printed page, what should it do about the neighbouring verses that come with it?

    +

    The answer, in one lineWhat should the panel show around the ayah?

    +
    1. ALeave the crop as it is
    2. BCut each line down to the verse's own words
    3. CFade everything that is not this verse
    4. EMark where the verse begins and ends
    5. DMark the words the two verses share, as well as the ones they do not
    6. FFade the neighbours, mark the shared words green and the differing words yellowchosen
    + + +
    + + + +
    +
    Decided 4 August 2026 · omar1,430 words of reasons
    +

    How does the app keep a page it was told to save without storing it twice?

    +

    The answer, in one lineOne bit of provenance, and the line it does not cross

    + +
    +

    Held up by, and holding up

    + +
    + +
    + + +
    +
    Decided 30 July 2026 · omar962 words of reasons
    +

    How does the app's own wording get translated, and what keeps the two languages in step?

    +

    The answer, in one lineThe chrome's language layer moved to a message compiler

    + + + +
    +
    +
    Decided 25 July 2026 · omar1,748 words of reasons
    +

    Can a teacher send a link that opens exactly what they were looking at?

    +

    The answer, in one lineLoop 3 — Diffs, share links, a11y pass

    + + + +
    + +
    +
    Decided 25 July 2026 · omar1,731 words of reasons
    +

    Can a reader mark a stretch of more than one verse and ask about all of it at once?

    +

    The answer, in one lineLoop 5 — Highlight gesture + root lens

    + + + +
    + +
    +
    Decided 23 July 2026 · omar1,138 words of reasons
    +

    Can a reader jump from a verse to one that sounds like it, and find the way back?

    +

    The answer, in one lineLoop 2 — The hop

    + + + +
    +
    +
    Decided 20 July 2026 · omar666 words of reasons
    +

    Can a real page of the mus'haf be put on a phone, installed, and opened again offline?

    +

    The answer, in one lineLoop 0 — Skeleton on a phone

    + + + +
    +
    +
    Decided 20 July 2026 · omar928 words of reasons
    +

    Does touching a verse on a phone answer fast enough to build the rest on?

    +

    The answer, in one lineLoop 1 — Select + the performance question

    + + + +
    +
    + +
    +
    +

    The shelfWhat has been published, and can anybody still see it?

    +

    Every page this project has put on the web, in one place. Of the 9, + 4 are the drawings the decisions above were made from; the + rest were made to settle something in the moment — a diagnosis, a comparison, a finding, a + plan — and were never attached to anything.

    +

    And 4 of them exist nowhere but at the address on the card. They were drawn + in a working folder, published, talked about, and the folder was later emptied. Nobody chose + that; it is what happens when the last step of publishing something is remembering to. They + come first here for the same reason the open questions do — they are the rows that need a + person.

    +
    +
    +
    +
    + No copy — the link is all there is + 13 August 2026 +
    +

    Registered on the Ornaments

    +

    Why the boxes drawn around tajweed marks sit off the ink: what was measured, the same mistake found named in medical imaging and in typesetting, and the plan that came out of it.

    +

    No copy anywhere. It is the diagnosis that the still-open question about lining up the rectangles grew out of, and it holds two things that question's own page does not: the count showing not one mark in three hundred thousand is named wrongly, and the reason the alignment can report itself excellent while being a whole mark-height out. When that question is settled, its record should absorb both, and this row should point at a page instead of at nothing.

    +
    +
    +
    + No copy — the link is all there is + 7 August 2026 +
    +

    Two spellings, one word

    +

    Why two machine readings of the same printed page disagree about ten verses, and why a reader looking at the page would see nothing different.

    +

    No copy anywhere. It settles a question — the disagreement is in the two readings, not in the print — but it was never written down as a decision, so the finding survives only at this address. It belongs in the record that covers where the two readings of the text come from.

    +
    +
    +
    + No copy — the link is all there is + 4 August 2026 +
    +

    Two segmentations, one text — PLAN ⑬

    +

    Two different ways of cutting the same text into words, how far the two agree, and where that agreement ought to be kept.

    +

    No copy anywhere, and its title is written in this project's own shorthand, which is the other half of the same problem: a page nobody outside can name is one nobody outside can use. It predates the rule that a decision page must be checked in.

    +
    +
    +
    + No copy — the link is all there is + 4 August 2026 +
    +

    Should the two leaves pan and zoom together?

    +

    Three screenshots of the running app comparing what zooming into one leaf does to a two-page spread, with a recommendation.

    +

    No copy anywhere. This one is a decision in everything but filing: the title is the question, the page puts options, and it carries a recommendation. What is missing is the record — the reasons, who chose, and when — so it should become a row in the decision register rather than stay here.

    +
    +
    +
    +
    + You are reading it + 17 August 2026 +
    +

    Hifth Decision Board

    +

    Every question this project has had to answer, on one time line, with the constraints running between them and every page it has published listed underneath.

    + +
    + + + +
    +
    + +

    Drawn from this project's decision register on every build. The questions, the +options, the dates, the arcs and the counts are all read from it — nothing on this page is +typed a second time, so if it ever disagrees with the register, the register is right and this +page has not been rebuilt. The shelf is read the same way from its own register. Rebuild with +node scripts/build-decision-board.mjs.

    +
    diff --git a/docs/design/etl-pipeline.md b/docs/design/etl-pipeline.md index 256c0539..5ca1c042 100644 --- a/docs/design/etl-pipeline.md +++ b/docs/design/etl-pipeline.md @@ -236,13 +236,21 @@ The pattern is the same each time: the failure was not in the check, it was in t ## ④ The one-way boundary -The repo's load-bearing rule, stated once in `morphology.mjs` and enforced twice: +The repo's load-bearing rule, stated once in `morphology.mjs`: -> There is no Quran text in this repo and there will not be. +> Nothing this project vendors, and nothing it ships, is Quran text. It survives contact with an ETL whose whole job is comparing texts, and the way it survives is the most interesting structural fact in the pipeline. +**It used to be written unscoped** — *"there is no Quran text in this repo and there will not +be"* — and about the repository as a whole it was false. Twelve verses were typed into a source +file and shipped in the bundle; a pipeline test held a four-word phrase. Neither crossed this +boundary, which is exactly why the boundary did not catch them: they arrived by being typed. +The rule about the pipeline was true the whole time and is what the diagram below draws. The +rule about the tree needed a check of its own, and now has one — `gate:scripture`, which fails +on any run of three consecutive fully-vowelled words in any source file. + ```mermaid flowchart LR classDef out fill:#f6f3ec,stroke:#b8ab8f,color:#3a3428 diff --git a/docs/design/mark-labels.md b/docs/design/mark-labels.md index 5eb1b8f2..ea9d7bce 100644 --- a/docs/design/mark-labels.md +++ b/docs/design/mark-labels.md @@ -336,8 +336,9 @@ has to defend, that search still needs doing. - **Measurements are not gates until a person has agreed with them.** This one exits with a failure code when a threshold is breached so that it *could* become a check, and it is wired into nothing. -- The companion document's Option B — one recorded displacement per page — is unaffected by - anything here, which is the useful consequence of the two being independent. +- Whichever correction the companion document adopts is unaffected by anything here, which is + the useful consequence of the two being independent. Its recommendation has since moved from + one displacement per page to one per printed line, and nothing on this page moved with it. ## ⑦ What are the options? diff --git a/docs/design/mark-placement.data.json b/docs/design/mark-placement.data.json new file mode 100644 index 00000000..48c1bfb1 --- /dev/null +++ b/docs/design/mark-placement.data.json @@ -0,0 +1,25123 @@ +{ + "$comment": [ + "Generated by scripts/build-placement-options.mjs --extract. Geometry only — no Quran text.", + "Coordinates are the page frame the word shards use. `b` is the rectangle the app ships", + "today; `o` is how far each option would move it, and `ink` how far it would have to move", + "to sit on the printer's own ink. Mark names are Latin; words are referred to by the", + "print's own word index and never by their text." + ], + "ran": "2026-08-15", + "page": 179, + "viewBox": "0 0 345 550", + "far": 0.75, + "pin": { + "markers": 9, + "residual": 0.087, + "words": 156 + }, + "marks": [ + { + "n": "fatha", + "l": 1, + "b": [ + 323.7, + 15.9, + 6.3, + 3.4 + ], + "ink": [ + -0.812, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.885, + -0.933 + ], + "curve": [ + -0.77, + -0.945 + ], + "mark": [ + -0.812, + -1 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 316.1, + 15.1, + 6.3, + 3.4 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.872, + -0.928 + ], + "curve": [ + -0.775, + -0.938 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 1, + "b": [ + 312.7, + 23.1, + 4.4, + 3 + ], + "ink": [ + -1, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.865, + -0.925 + ], + "curve": [ + -0.778, + -0.934 + ], + "mark": [ + -1, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 303.2, + 15.7, + 6.3, + 3.4 + ], + "ink": [ + -0.687, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.851, + -0.919 + ], + "curve": [ + -0.782, + -0.926 + ], + "mark": [ + -0.687, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 1, + "b": [ + 297.5, + 16.9, + 4.4, + 3 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.84, + -0.915 + ], + "curve": [ + -0.784, + -0.92 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "damma", + "l": 1, + "b": [ + 288.1, + 15.8, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.825, + -0.909 + ], + "curve": [ + -0.786, + -0.913 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "damma", + "l": 1, + "b": [ + 280.7, + 12.3, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.813, + -0.904 + ], + "curve": [ + -0.787, + -0.906 + ], + "mark": [ + -0.625, + -0.75 + ] + } + }, + { + "n": "damma", + "l": 1, + "b": [ + 267.2, + 16.7, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.791, + -0.894 + ], + "curve": [ + -0.786, + -0.895 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 1, + "b": [ + 257.4, + 22, + 4.4, + 3 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.774, + -0.887 + ], + "curve": [ + -0.784, + -0.886 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 245.2, + 20.8, + 6.3, + 3.4 + ], + "ink": [ + -0.562, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.755, + -0.88 + ], + "curve": [ + -0.779, + -0.877 + ], + "mark": [ + -0.562, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 237.9, + 14.2, + 6.3, + 3.4 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.743, + -0.875 + ], + "curve": [ + -0.775, + -0.871 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "superscript alef", + "l": 1, + "b": [ + 237, + 19.2, + 2.1, + 6 + ], + "ink": [ + -1, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.738, + -0.873 + ], + "curve": [ + -0.773, + -0.869 + ], + "mark": [ + -1, + -0.875 + ] + } + }, + { + "n": "kasra", + "l": 1, + "b": [ + 217, + 35.3, + 7, + 3.5 + ], + "ink": [ + -0.437, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.709, + -0.861 + ], + "curve": [ + -0.759, + -0.856 + ], + "mark": [ + -0.437, + -0.75 + ] + } + }, + { + "n": "shadda", + "l": 1, + "b": [ + 204.4, + 20.6, + 4.3, + 3.3 + ], + "ink": [ + -1, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.686, + -0.851 + ], + "curve": [ + -0.745, + -0.845 + ], + "mark": [ + -1, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 203.5, + 16.7, + 6.3, + 3.4 + ], + "ink": [ + -1.062, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.687, + -0.851 + ], + "curve": [ + -0.745, + -0.845 + ], + "mark": [ + -1.062, + -0.812 + ] + } + }, + { + "n": "wasla", + "l": 1, + "b": [ + 193.8, + 12.1, + 5.3, + 4.3 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.67, + -0.844 + ], + "curve": [ + -0.732, + -0.838 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "shadda", + "l": 1, + "b": [ + 187.7, + 18.7, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.659, + -0.84 + ], + "curve": [ + -0.723, + -0.833 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 185.5, + 14.1, + 7, + 3.6 + ], + "ink": [ + -0.687, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.657, + -0.839 + ], + "curve": [ + -0.722, + -0.833 + ], + "mark": [ + -0.687, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 179.8, + 20.2, + 5.6, + 3.1 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.647, + -0.835 + ], + "curve": [ + -0.713, + -0.828 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 168.1, + 14.9, + 7, + 3.5 + ], + "ink": [ + -0.687, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.629, + -0.827 + ], + "curve": [ + -0.695, + -0.821 + ], + "mark": [ + -0.687, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 158.4, + 15.4, + 8.5, + 3.9 + ], + "ink": [ + -0.687, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.614, + -0.821 + ], + "curve": [ + -0.679, + -0.815 + ], + "mark": [ + -0.687, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 150.1, + 14.8, + 7, + 3.6 + ], + "ink": [ + -0.687, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.599, + -0.815 + ], + "curve": [ + -0.662, + -0.809 + ], + "mark": [ + -0.687, + -0.75 + ] + } + }, + { + "n": "damma", + "l": 1, + "b": [ + 142.4, + 16.8, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.585, + -0.809 + ], + "curve": [ + -0.645, + -0.803 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "sukun", + "l": 1, + "b": [ + 133.4, + 21.9, + 4.4, + 3 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.569, + -0.803 + ], + "curve": [ + -0.624, + -0.797 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 120.6, + 20.4, + 7, + 3.6 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.55, + -0.795 + ], + "curve": [ + -0.598, + -0.79 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 110.8, + 18.2, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.534, + -0.788 + ], + "curve": [ + -0.573, + -0.784 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 97, + 19.6, + 6.3, + 3.4 + ], + "ink": [ + -0.375, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.511, + -0.779 + ], + "curve": [ + -0.535, + -0.776 + ], + "mark": [ + -0.375, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 88.7, + 17.8, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.498, + -0.773 + ], + "curve": [ + -0.513, + -0.772 + ], + "mark": [ + -0.562, + -0.562 + ] + } + }, + { + "n": "sukun", + "l": 1, + "b": [ + 82.4, + 19.7, + 4.4, + 3 + ], + "ink": [ + -0.437, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.485, + -0.768 + ], + "curve": [ + -0.49, + -0.768 + ], + "mark": [ + -0.437, + -0.625 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 71.6, + 18.9, + 6.3, + 3.4 + ], + "ink": [ + -0.437, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.469, + -0.761 + ], + "curve": [ + -0.46, + -0.762 + ], + "mark": [ + -0.437, + -0.687 + ] + } + }, + { + "n": "hamza", + "l": 1, + "b": [ + 61.3, + 34.4, + 4, + 3.9 + ], + "ink": [ + -0.25, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.45, + -0.754 + ], + "curve": [ + -0.422, + -0.757 + ], + "mark": [ + -0.25, + -0.687 + ] + } + }, + { + "n": "kasra", + "l": 1, + "b": [ + 61.3, + 37.8, + 4.9, + 2.6 + ], + "ink": [ + -0.25, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.451, + -0.754 + ], + "curve": [ + -0.424, + -0.757 + ], + "mark": [ + -0.25, + -0.625 + ] + } + }, + { + "n": "sukun", + "l": 1, + "b": [ + 52.7, + 15.3, + 4.4, + 3 + ], + "ink": [ + -0.25, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.436, + -0.748 + ], + "curve": [ + -0.393, + -0.752 + ], + "mark": [ + -0.25, + -0.625 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 42.3, + 18.3, + 7, + 3.6 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.421, + -0.742 + ], + "curve": [ + -0.361, + -0.748 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 32.8, + 17.4, + 8.5, + 3.9 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.407, + -0.736 + ], + "curve": [ + -0.329, + -0.744 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 1, + "b": [ + 25, + 19, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.39, + -0.729 + ], + "curve": [ + -0.291, + -0.739 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 1, + "b": [ + 11.8, + 17.7, + 7.8, + 3.7 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.371, + -0.721 + ], + "curve": [ + -0.245, + -0.734 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 324.8, + 55.7, + 7, + 3.5 + ], + "ink": [ + -1, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.041, + -1.439 + ], + "curve": [ + -1.31, + -0.994 + ], + "mark": [ + -1, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 316.9, + 48, + 6.3, + 3.4 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.038, + -1.436 + ], + "curve": [ + -1.264, + -1.06 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "superscript alef", + "l": 2, + "b": [ + 315.9, + 54.4, + 2.1, + 6 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.036, + -1.435 + ], + "curve": [ + -1.248, + -1.084 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 2, + "b": [ + 292, + 69.8, + 7, + 3.6 + ], + "ink": [ + -1, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.027, + -1.427 + ], + "curve": [ + -1.144, + -1.233 + ], + "mark": [ + -1, + -0.875 + ] + } + }, + { + "n": "shadda", + "l": 2, + "b": [ + 279.9, + 55.4, + 4.3, + 3.3 + ], + "ink": [ + -0.875, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.021, + -1.421 + ], + "curve": [ + -1.086, + -1.313 + ], + "mark": [ + -0.875, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 278.4, + 51.1, + 6.3, + 3.4 + ], + "ink": [ + -1.125, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.021, + -1.421 + ], + "curve": [ + -1.084, + -1.316 + ], + "mark": [ + -1.125, + -1 + ] + } + }, + { + "n": "wasla", + "l": 2, + "b": [ + 268.5, + 47.6, + 5.3, + 4.3 + ], + "ink": [ + -1.375, + -1.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.016, + -1.417 + ], + "curve": [ + -1.044, + -1.371 + ], + "mark": [ + -1.375, + -1.875 + ] + } + }, + { + "n": "shadda", + "l": 2, + "b": [ + 262.2, + 53.8, + 4.3, + 3.3 + ], + "ink": [ + -1.562, + -2.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.013, + -1.414 + ], + "curve": [ + -1.02, + -1.404 + ], + "mark": [ + -1.562, + -2.062 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 260, + 48.9, + 7, + 3.6 + ], + "ink": [ + -1.437, + -1.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.013, + -1.414 + ], + "curve": [ + -1.017, + -1.407 + ], + "mark": [ + -1.437, + -1.812 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 254.1, + 56, + 5.6, + 3.1 + ], + "ink": [ + -1.437, + -2.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.01, + -1.411 + ], + "curve": [ + -0.995, + -1.436 + ], + "mark": [ + -1.437, + -2.187 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 246.5, + 54, + 6.3, + 3.4 + ], + "ink": [ + -1.437, + -1.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.007, + -1.408 + ], + "curve": [ + -0.973, + -1.465 + ], + "mark": [ + -1.437, + -1.687 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 239.1, + 53.5, + 7, + 3.6 + ], + "ink": [ + -1.312, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.004, + -1.406 + ], + "curve": [ + -0.953, + -1.491 + ], + "mark": [ + -1.312, + -1.562 + ] + } + }, + { + "n": "superscript alef", + "l": 2, + "b": [ + 234.7, + 57.9, + 2.1, + 6 + ], + "ink": [ + -1.375, + -1.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.001, + -1.403 + ], + "curve": [ + -0.935, + -1.512 + ], + "mark": [ + -1.375, + -1.812 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 220.7, + 55.2, + 7, + 3.5 + ], + "ink": [ + -1.187, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.996, + -1.398 + ], + "curve": [ + -0.908, + -1.543 + ], + "mark": [ + -1.187, + -1.562 + ] + } + }, + { + "n": "kasra", + "l": 2, + "b": [ + 216.5, + 67.5, + 5.6, + 3.1 + ], + "ink": [ + -1.062, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.994, + -1.397 + ], + "curve": [ + -0.899, + -1.554 + ], + "mark": [ + -1.062, + -1.562 + ] + } + }, + { + "n": "damma", + "l": 2, + "b": [ + 206.8, + 55, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.989, + -1.393 + ], + "curve": [ + -0.881, + -1.572 + ], + "mark": [ + -0.75, + -1.562 + ] + } + }, + { + "n": "sukun", + "l": 2, + "b": [ + 198.3, + 57.4, + 4.4, + 3 + ], + "ink": [ + -0.937, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.985, + -1.389 + ], + "curve": [ + -0.868, + -1.583 + ], + "mark": [ + -0.937, + -1.562 + ] + } + }, + { + "n": "kasra", + "l": 2, + "b": [ + 194.5, + 75.3, + 5.6, + 3.1 + ], + "ink": [ + -1.187, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.984, + -1.388 + ], + "curve": [ + -0.864, + -1.586 + ], + "mark": [ + -1.187, + -1.562 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 184.7, + 59.8, + 6.3, + 3.4 + ], + "ink": [ + -0.937, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.98, + -1.384 + ], + "curve": [ + -0.855, + -1.592 + ], + "mark": [ + -0.937, + -1.562 + ] + } + }, + { + "n": "wasla", + "l": 2, + "b": [ + 176.5, + 48.1, + 5.3, + 4.3 + ], + "ink": [ + -0.5, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.976, + -1.381 + ], + "curve": [ + -0.848, + -1.593 + ], + "mark": [ + -0.5, + -1.562 + ] + } + }, + { + "n": "sukun", + "l": 2, + "b": [ + 171.1, + 47.6, + 4.4, + 3 + ], + "ink": [ + -0.562, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.974, + -1.378 + ], + "curve": [ + -0.846, + -1.591 + ], + "mark": [ + -0.562, + -1.562 + ] + } + }, + { + "n": "damma", + "l": 2, + "b": [ + 165.2, + 54.1, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.971, + -1.376 + ], + "curve": [ + -0.844, + -1.587 + ], + "mark": [ + -0.625, + -1.562 + ] + } + }, + { + "n": "sukun", + "l": 2, + "b": [ + 158.5, + 53.8, + 4.4, + 3 + ], + "ink": [ + -0.625, + -1.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.968, + -1.373 + ], + "curve": [ + -0.844, + -1.58 + ], + "mark": [ + -0.625, + -1.625 + ] + } + }, + { + "n": "hamza", + "l": 2, + "b": [ + 159.1, + 59, + 4, + 3.9 + ], + "ink": [ + -0.625, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.968, + -1.374 + ], + "curve": [ + -0.844, + -1.58 + ], + "mark": [ + -0.625, + -1.562 + ] + } + }, + { + "n": "kasra", + "l": 2, + "b": [ + 151.5, + 69, + 6.3, + 3.4 + ], + "ink": [ + -0.187, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.965, + -1.371 + ], + "curve": [ + -0.845, + -1.571 + ], + "mark": [ + -0.187, + -0.875 + ] + } + }, + { + "n": "kasra", + "l": 2, + "b": [ + 141.7, + 70.6, + 6.3, + 3.4 + ], + "ink": [ + -0.812, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.961, + -1.367 + ], + "curve": [ + -0.849, + -1.554 + ], + "mark": [ + -0.812, + -1.562 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 125.8, + 53.4, + 7, + 3.6 + ], + "ink": [ + -0.5, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.954, + -1.361 + ], + "curve": [ + -0.862, + -1.514 + ], + "mark": [ + -0.5, + -1.562 + ] + } + }, + { + "n": "kasra", + "l": 2, + "b": [ + 115.9, + 69.2, + 7, + 3.6 + ], + "ink": [ + -0.5, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.95, + -1.357 + ], + "curve": [ + -0.875, + -1.482 + ], + "mark": [ + -0.5, + -1.437 + ] + } + }, + { + "n": "sukun", + "l": 2, + "b": [ + 106.5, + 55.5, + 4.4, + 3 + ], + "ink": [ + -0.687, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.945, + -1.353 + ], + "curve": [ + -0.892, + -1.441 + ], + "mark": [ + -0.687, + -1.437 + ] + } + }, + { + "n": "damma", + "l": 2, + "b": [ + 97.4, + 51.7, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.941, + -1.35 + ], + "curve": [ + -0.909, + -1.404 + ], + "mark": [ + -0.5, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 83.4, + 54.1, + 8.5, + 3.9 + ], + "ink": [ + -1.5, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.936, + -1.345 + ], + "curve": [ + -0.938, + -1.342 + ], + "mark": [ + -1.5, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 69.7, + 50.2, + 7.8, + 3.7 + ], + "ink": [ + -1.625, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.93, + -1.339 + ], + "curve": [ + -0.976, + -1.262 + ], + "mark": [ + -1.625, + -1.062 + ] + } + }, + { + "n": "maddah", + "l": 2, + "b": [ + 61, + 50.8, + 6.6, + 3.1 + ], + "ink": [ + -1.437, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.926, + -1.336 + ], + "curve": [ + -1.006, + -1.203 + ], + "mark": [ + -1.437, + -1.187 + ] + } + }, + { + "n": "fathatan", + "l": 2, + "b": [ + 56, + 57.4, + 6.1, + 4.9 + ], + "ink": [ + -0.375, + -1.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.924, + -1.333 + ], + "curve": [ + -1.024, + -1.168 + ], + "mark": [ + -0.375, + -1.25 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 38.4, + 53.6, + 8.5, + 3.9 + ], + "ink": [ + -1.25, + -1.125 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.916, + -1.327 + ], + "curve": [ + -1.085, + -1.047 + ], + "mark": [ + -1.25, + -1.125 + ] + } + }, + { + "n": "fatha", + "l": 2, + "b": [ + 27.6, + 54.4, + 8.5, + 3.9 + ], + "ink": [ + -1.312, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.912, + -1.323 + ], + "curve": [ + -1.131, + -0.959 + ], + "mark": [ + -1.312, + -1.062 + ] + } + }, + { + "n": "fathatan", + "l": 2, + "b": [ + 10.1, + 52.9, + 6, + 5 + ], + "ink": [ + -1.25, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.903, + -1.315 + ], + "curve": [ + -1.22, + -0.791 + ], + "mark": [ + -1.25, + -0.937 + ] + } + }, + { + "n": "hamza", + "l": 3, + "b": [ + 327, + 105.5, + 4, + 3.9 + ], + "ink": [ + -0.75, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.806, + -0.933 + ], + "curve": [ + -0.778, + -0.915 + ], + "mark": [ + -0.75, + -0.75 + ] + } + }, + { + "n": "kasra", + "l": 3, + "b": [ + 326.9, + 109.2, + 4.9, + 2.6 + ], + "ink": [ + -0.75, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.807, + -0.933 + ], + "curve": [ + -0.778, + -0.915 + ], + "mark": [ + -0.75, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 3, + "b": [ + 317.5, + 88.8, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.792, + -0.934 + ], + "curve": [ + -0.771, + -0.92 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 315.4, + 85.1, + 7, + 3.6 + ], + "ink": [ + -0.812, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.791, + -0.934 + ], + "curve": [ + -0.77, + -0.92 + ], + "mark": [ + -0.812, + -0.812 + ] + } + }, + { + "n": "wasla", + "l": 3, + "b": [ + 306.5, + 82.3, + 5.3, + 4.3 + ], + "ink": [ + -0.687, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.777, + -0.935 + ], + "curve": [ + -0.763, + -0.925 + ], + "mark": [ + -0.687, + -1.062 + ] + } + }, + { + "n": "shadda", + "l": 3, + "b": [ + 300.2, + 89, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.767, + -0.935 + ], + "curve": [ + -0.757, + -0.929 + ], + "mark": [ + -0.75, + -1 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 297.8, + 84, + 7.8, + 3.7 + ], + "ink": [ + -0.75, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.766, + -0.935 + ], + "curve": [ + -0.756, + -0.929 + ], + "mark": [ + -0.75, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 291.2, + 90.6, + 6.3, + 3.4 + ], + "ink": [ + -0.812, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.756, + -0.936 + ], + "curve": [ + -0.75, + -0.932 + ], + "mark": [ + -0.812, + -1 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 278.2, + 89.6, + 8.5, + 3.9 + ], + "ink": [ + -0.75, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.738, + -0.937 + ], + "curve": [ + -0.739, + -0.937 + ], + "mark": [ + -0.75, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 3, + "b": [ + 263.7, + 107, + 7, + 3.6 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.716, + -0.938 + ], + "curve": [ + -0.724, + -0.943 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "dammatan", + "l": 3, + "b": [ + 247.5, + 87.5, + 5.9, + 8.7 + ], + "ink": [ + -0.75, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.692, + -0.939 + ], + "curve": [ + -0.706, + -0.948 + ], + "mark": [ + -0.75, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 234.8, + 89.4, + 6.3, + 3.4 + ], + "ink": [ + -0.687, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.673, + -0.94 + ], + "curve": [ + -0.691, + -0.952 + ], + "mark": [ + -0.687, + -0.875 + ] + } + }, + { + "n": "kasra", + "l": 3, + "b": [ + 228.9, + 109.3, + 6.3, + 3.4 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.665, + -0.941 + ], + "curve": [ + -0.684, + -0.953 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "successive dammatan", + "l": 3, + "b": [ + 208.1, + 89.2, + 8.2, + 6.4 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.636, + -0.942 + ], + "curve": [ + -0.659, + -0.957 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 176.2, + 86.4, + 5.6, + 3.1 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.587, + -0.945 + ], + "curve": [ + -0.613, + -0.961 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "superscript alef", + "l": 3, + "b": [ + 174.7, + 93.5, + 2.1, + 6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.583, + -0.945 + ], + "curve": [ + -0.608, + -0.961 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 3, + "b": [ + 168.6, + 106.5, + 7, + 3.6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.577, + -0.945 + ], + "curve": [ + -0.603, + -0.962 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 3, + "b": [ + 141.2, + 87, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.536, + -0.947 + ], + "curve": [ + -0.558, + -0.962 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 3, + "b": [ + 134.9, + 91.1, + 4.4, + 3 + ], + "ink": [ + -0.687, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.526, + -0.948 + ], + "curve": [ + -0.547, + -0.961 + ], + "mark": [ + -0.687, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 121.1, + 90.8, + 7, + 3.6 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.508, + -0.949 + ], + "curve": [ + -0.525, + -0.96 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "hamza", + "l": 3, + "b": [ + 114.7, + 84.3, + 4, + 3.9 + ], + "ink": [ + -0.625, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.496, + -0.949 + ], + "curve": [ + -0.511, + -0.959 + ], + "mark": [ + -0.625, + -1 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 112.8, + 80.5, + 6.3, + 3.4 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.495, + -0.949 + ], + "curve": [ + -0.51, + -0.959 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 3, + "b": [ + 106.5, + 87.1, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.485, + -0.95 + ], + "curve": [ + -0.497, + -0.958 + ], + "mark": [ + -0.625, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 105.1, + 81.4, + 6.3, + 3.4 + ], + "ink": [ + -0.562, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.484, + -0.95 + ], + "curve": [ + -0.496, + -0.958 + ], + "mark": [ + -0.562, + -1 + ] + } + }, + { + "n": "wasla", + "l": 3, + "b": [ + 95.6, + 83.3, + 5.3, + 4.3 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.469, + -0.951 + ], + "curve": [ + -0.477, + -0.956 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 3, + "b": [ + 89.1, + 88.7, + 4.3, + 3.3 + ], + "ink": [ + -0.375, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.459, + -0.951 + ], + "curve": [ + -0.464, + -0.954 + ], + "mark": [ + -0.375, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 87.3, + 83.5, + 7, + 3.6 + ], + "ink": [ + -0.312, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.459, + -0.951 + ], + "curve": [ + -0.463, + -0.954 + ], + "mark": [ + -0.312, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 80.4, + 89.8, + 6.3, + 3.4 + ], + "ink": [ + -0.437, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.448, + -0.952 + ], + "curve": [ + -0.449, + -0.953 + ], + "mark": [ + -0.437, + -0.875 + ] + } + }, + { + "n": "damma", + "l": 3, + "b": [ + 70.7, + 88.2, + 5.3, + 6.6 + ], + "ink": [ + -0.375, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.433, + -0.953 + ], + "curve": [ + -0.429, + -0.95 + ], + "mark": [ + -0.375, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 3, + "b": [ + 55.9, + 104.9, + 7, + 3.6 + ], + "ink": [ + -0.25, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.413, + -0.954 + ], + "curve": [ + -0.401, + -0.946 + ], + "mark": [ + -0.25, + -0.875 + ] + } + }, + { + "n": "damma", + "l": 3, + "b": [ + 43.4, + 89, + 5.3, + 6.6 + ], + "ink": [ + -0.375, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.393, + -0.955 + ], + "curve": [ + -0.372, + -0.941 + ], + "mark": [ + -0.375, + -1 + ] + } + }, + { + "n": "fatha", + "l": 3, + "b": [ + 28.6, + 83.2, + 7, + 3.6 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.373, + -0.956 + ], + "curve": [ + -0.342, + -0.936 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "sukun", + "l": 3, + "b": [ + 23.4, + 95.1, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.363, + -0.956 + ], + "curve": [ + -0.328, + -0.933 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 3, + "b": [ + 8, + 106.5, + 6.3, + 3.4 + ], + "ink": [ + -0.312, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.342, + -0.957 + ], + "curve": [ + -0.295, + -0.926 + ], + "mark": [ + -0.312, + -1 + ] + } + }, + { + "n": "wasla", + "l": 4, + "b": [ + 326.6, + 120.5, + 5.3, + 4.3 + ], + "ink": [ + -0.875, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.445, + -1.107 + ], + "curve": [ + -0.728, + -1.415 + ], + "mark": [ + -0.875, + -1.562 + ] + } + }, + { + "n": "sukun", + "l": 4, + "b": [ + 321, + 120, + 4.4, + 3 + ], + "ink": [ + -0.875, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.465, + -1.117 + ], + "curve": [ + -0.714, + -1.389 + ], + "mark": [ + -0.875, + -1.562 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 296.8, + 122.5, + 7, + 3.6 + ], + "ink": [ + -0.875, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.539, + -1.154 + ], + "curve": [ + -0.674, + -1.301 + ], + "mark": [ + -0.875, + -1.562 + ] + } + }, + { + "n": "superscript alef", + "l": 4, + "b": [ + 293.3, + 126.6, + 2.1, + 6 + ], + "ink": [ + -0.187, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.558, + -1.163 + ], + "curve": [ + -0.667, + -1.282 + ], + "mark": [ + -0.187, + -0.75 + ] + } + }, + { + "n": "kasra", + "l": 4, + "b": [ + 287.9, + 141.9, + 6.3, + 3.4 + ], + "ink": [ + -0.125, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.569, + -1.168 + ], + "curve": [ + -0.664, + -1.271 + ], + "mark": [ + -0.125, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 4, + "b": [ + 278.7, + 146.8, + 6.3, + 3.4 + ], + "ink": [ + -0.312, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.599, + -1.183 + ], + "curve": [ + -0.656, + -1.245 + ], + "mark": [ + -0.312, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 266.2, + 126.2, + 7, + 3.6 + ], + "ink": [ + -0.187, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.639, + -1.203 + ], + "curve": [ + -0.651, + -1.216 + ], + "mark": [ + -0.187, + -0.812 + ] + } + }, + { + "n": "hamza", + "l": 4, + "b": [ + 238.9, + 142.2, + 4, + 3.9 + ], + "ink": [ + -0.937, + -1.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.733, + -1.249 + ], + "curve": [ + -0.659, + -1.168 + ], + "mark": [ + -0.937, + -1.687 + ] + } + }, + { + "n": "kasra", + "l": 4, + "b": [ + 238.8, + 145.9, + 4.9, + 2.6 + ], + "ink": [ + -0.875, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.731, + -1.248 + ], + "curve": [ + -0.659, + -1.169 + ], + "mark": [ + -0.875, + -1.562 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 216, + 121.5, + 7.8, + 3.7 + ], + "ink": [ + -1, + -1.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.801, + -1.282 + ], + "curve": [ + -0.684, + -1.154 + ], + "mark": [ + -1, + -1.25 + ] + } + }, + { + "n": "sukun", + "l": 4, + "b": [ + 211.9, + 128.6, + 4.4, + 3 + ], + "ink": [ + -1.25, + -1.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.82, + -1.291 + ], + "curve": [ + -0.693, + -1.154 + ], + "mark": [ + -1.25, + -1.625 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 195.9, + 124, + 7.8, + 3.7 + ], + "ink": [ + -0.937, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.867, + -1.314 + ], + "curve": [ + -0.722, + -1.157 + ], + "mark": [ + -0.937, + -1.437 + ] + } + }, + { + "n": "sukun", + "l": 4, + "b": [ + 189.9, + 124.1, + 4.4, + 3 + ], + "ink": [ + -1.312, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.892, + -1.327 + ], + "curve": [ + -0.741, + -1.162 + ], + "mark": [ + -1.312, + -1.437 + ] + } + }, + { + "n": "kasra", + "l": 4, + "b": [ + 180.9, + 142.5, + 7.8, + 3.7 + ], + "ink": [ + -1.312, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.915, + -1.338 + ], + "curve": [ + -0.76, + -1.169 + ], + "mark": [ + -1.312, + -1.562 + ] + } + }, + { + "n": "damma", + "l": 4, + "b": [ + 168.2, + 123.4, + 5.3, + 6.6 + ], + "ink": [ + -1, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.961, + -1.361 + ], + "curve": [ + -0.803, + -1.189 + ], + "mark": [ + -1, + -1.312 + ] + } + }, + { + "n": "rounded zero", + "l": 4, + "b": [ + 157.3, + 119.9, + 3.2, + 4.1 + ], + "ink": [ + -0.937, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1, + -1.38 + ], + "curve": [ + -0.845, + -1.211 + ], + "mark": [ + -0.937, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 149.2, + 122.4, + 6.3, + 3.4 + ], + "ink": [ + -0.75, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.021, + -1.39 + ], + "curve": [ + -0.87, + -1.226 + ], + "mark": [ + -0.75, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 141.5, + 122.7, + 7, + 3.6 + ], + "ink": [ + -0.75, + -1.125 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.045, + -1.402 + ], + "curve": [ + -0.901, + -1.244 + ], + "mark": [ + -0.75, + -1.125 + ] + } + }, + { + "n": "sukun", + "l": 4, + "b": [ + 135, + 125, + 4.4, + 3 + ], + "ink": [ + -0.937, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.07, + -1.414 + ], + "curve": [ + -0.935, + -1.266 + ], + "mark": [ + -0.937, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 118.7, + 126.1, + 7.8, + 3.7 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.118, + -1.438 + ], + "curve": [ + -1.004, + -1.314 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "maddah", + "l": 4, + "b": [ + 112.4, + 121.5, + 6.6, + 3.1 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.141, + -1.449 + ], + "curve": [ + -1.04, + -1.339 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 107.5, + 127.2, + 6.3, + 3.4 + ], + "ink": [ + -0.687, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.157, + -1.457 + ], + "curve": [ + -1.067, + -1.359 + ], + "mark": [ + -0.687, + -1 + ] + } + }, + { + "n": "damma", + "l": 4, + "b": [ + 84.6, + 122.1, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.233, + -1.494 + ], + "curve": [ + -1.205, + -1.463 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "damma", + "l": 4, + "b": [ + 74.2, + 126.2, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.267, + -1.511 + ], + "curve": [ + -1.272, + -1.516 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "wasla", + "l": 4, + "b": [ + 65.8, + 119.4, + 5.3, + 4.3 + ], + "ink": [ + -2.625, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.294, + -1.524 + ], + "curve": [ + -1.329, + -1.562 + ], + "mark": [ + -2.625, + -2.625 + ] + } + }, + { + "n": "sukun", + "l": 4, + "b": [ + 60.3, + 119.6, + 4.4, + 3 + ], + "ink": [ + -2.687, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.314, + -1.534 + ], + "curve": [ + -1.372, + -1.597 + ], + "mark": [ + -2.687, + -2.625 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 52.5, + 124.6, + 7.8, + 3.7 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.334, + -1.544 + ], + "curve": [ + -1.416, + -1.633 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 4, + "b": [ + 44.2, + 123.9, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.366, + -1.56 + ], + "curve": [ + -1.492, + -1.696 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 4, + "b": [ + 35.6, + 124.5, + 5.3, + 6.6 + ], + "ink": [ + -0.375, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.393, + -1.573 + ], + "curve": [ + -1.556, + -1.751 + ], + "mark": [ + -0.375, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 4, + "b": [ + 23.3, + 126.4, + 6.3, + 3.4 + ], + "ink": [ + -2.625, + -2.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.431, + -1.591 + ], + "curve": [ + -1.654, + -1.834 + ], + "mark": [ + -2.625, + -2.687 + ] + } + }, + { + "n": "hamza", + "l": 4, + "b": [ + 18.3, + 136.5, + 4, + 3.9 + ], + "ink": [ + -2.625, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.451, + -1.601 + ], + "curve": [ + -1.707, + -1.879 + ], + "mark": [ + -2.625, + -2.625 + ] + } + }, + { + "n": "kasra", + "l": 4, + "b": [ + 18.4, + 140.1, + 4.9, + 2.6 + ], + "ink": [ + -2.812, + -2.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.45, + -1.6 + ], + "curve": [ + -1.702, + -1.875 + ], + "mark": [ + -2.812, + -2.562 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 323.4, + 159.6, + 8.5, + 3.9 + ], + "ink": [ + -2.125, + -2.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -2.209, + -2.884 + ], + "curve": [ + -2.136, + -2.723 + ], + "mark": [ + -2.125, + -2.75 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 310.9, + 159.4, + 7.8, + 3.7 + ], + "ink": [ + -2, + -2.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -2.139, + -2.834 + ], + "curve": [ + -2.085, + -2.714 + ], + "mark": [ + -2, + -2.687 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 303.8, + 160.1, + 5.3, + 6.6 + ], + "ink": [ + -2.312, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -2.094, + -2.802 + ], + "curve": [ + -2.051, + -2.706 + ], + "mark": [ + -2.312, + -2.625 + ] + } + }, + { + "n": "rounded zero", + "l": 5, + "b": [ + 293.3, + 157.4, + 3.2, + 4.1 + ], + "ink": [ + -2.062, + -2.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -2.032, + -2.757 + ], + "curve": [ + -2.003, + -2.693 + ], + "mark": [ + -2.062, + -2.687 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 283.7, + 157.3, + 7, + 3.5 + ], + "ink": [ + -1.687, + -2.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.99, + -2.727 + ], + "curve": [ + -1.971, + -2.683 + ], + "mark": [ + -1.687, + -2.687 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 276.6, + 159.8, + 5.3, + 6.6 + ], + "ink": [ + -2, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.948, + -2.696 + ], + "curve": [ + -1.936, + -2.671 + ], + "mark": [ + -2, + -2.625 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 269.4, + 161.8, + 7, + 3.6 + ], + "ink": [ + -2, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.913, + -2.671 + ], + "curve": [ + -1.908, + -2.66 + ], + "mark": [ + -2, + -2.625 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 255.8, + 160.4, + 7, + 3.6 + ], + "ink": [ + -1.687, + -2.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.84, + -2.618 + ], + "curve": [ + -1.847, + -2.635 + ], + "mark": [ + -1.687, + -2.687 + ] + } + }, + { + "n": "sukun", + "l": 5, + "b": [ + 250, + 164.3, + 4.4, + 3 + ], + "ink": [ + -1.812, + -2.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.801, + -2.591 + ], + "curve": [ + -1.815, + -2.62 + ], + "mark": [ + -1.812, + -2.812 + ] + } + }, + { + "n": "successive dammatan", + "l": 5, + "b": [ + 241, + 161.1, + 8.2, + 6.4 + ], + "ink": [ + -1.812, + -2.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.763, + -2.563 + ], + "curve": [ + -1.782, + -2.604 + ], + "mark": [ + -1.812, + -2.812 + ] + } + }, + { + "n": "shadda", + "l": 5, + "b": [ + 234.8, + 155.6, + 4.3, + 3.3 + ], + "ink": [ + -1.687, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.719, + -2.532 + ], + "curve": [ + -1.743, + -2.585 + ], + "mark": [ + -1.687, + -2.625 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 232.2, + 152.2, + 7.8, + 3.7 + ], + "ink": [ + -1.562, + -2.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.715, + -2.528 + ], + "curve": [ + -1.739, + -2.583 + ], + "mark": [ + -1.562, + -2.562 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 210.2, + 159.3, + 5.3, + 6.6 + ], + "ink": [ + -1.812, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.589, + -2.438 + ], + "curve": [ + -1.626, + -2.519 + ], + "mark": [ + -1.812, + -2.625 + ] + } + }, + { + "n": "sukun", + "l": 5, + "b": [ + 202.9, + 165.2, + 4.4, + 3 + ], + "ink": [ + -1.875, + -2.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.547, + -2.408 + ], + "curve": [ + -1.587, + -2.495 + ], + "mark": [ + -1.875, + -2.625 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 190.3, + 161.3, + 6.3, + 3.4 + ], + "ink": [ + -1.437, + -2.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.484, + -2.362 + ], + "curve": [ + -1.527, + -2.457 + ], + "mark": [ + -1.437, + -2.437 + ] + } + }, + { + "n": "hamza", + "l": 5, + "b": [ + 186.7, + 171.6, + 4, + 3.9 + ], + "ink": [ + -1.562, + -2.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.459, + -2.344 + ], + "curve": [ + -1.502, + -2.44 + ], + "mark": [ + -1.562, + -2.25 + ] + } + }, + { + "n": "kasra", + "l": 5, + "b": [ + 186.7, + 175.2, + 4.9, + 2.6 + ], + "ink": [ + -1.5, + -2.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.461, + -2.346 + ], + "curve": [ + -1.504, + -2.442 + ], + "mark": [ + -1.5, + -2.312 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 161.9, + 158.4, + 8.5, + 3.9 + ], + "ink": [ + -1.312, + -2.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.337, + -2.256 + ], + "curve": [ + -1.382, + -2.356 + ], + "mark": [ + -1.312, + -2.25 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 151.9, + 159.8, + 5.3, + 6.6 + ], + "ink": [ + -1.437, + -2.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.274, + -2.211 + ], + "curve": [ + -1.318, + -2.308 + ], + "mark": [ + -1.437, + -2.375 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 139.9, + 159.7, + 5.3, + 6.6 + ], + "ink": [ + -1.437, + -2.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.209, + -2.165 + ], + "curve": [ + -1.25, + -2.255 + ], + "mark": [ + -1.437, + -2.375 + ] + } + }, + { + "n": "rounded zero", + "l": 5, + "b": [ + 128.4, + 156.6, + 3.2, + 4.1 + ], + "ink": [ + -1.25, + -2.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.142, + -2.116 + ], + "curve": [ + -1.178, + -2.197 + ], + "mark": [ + -1.25, + -2.25 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 118.9, + 159.3, + 7, + 3.6 + ], + "ink": [ + -1, + -2.125 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.101, + -2.086 + ], + "curve": [ + -1.134, + -2.16 + ], + "mark": [ + -1, + -2.125 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 111.5, + 159.3, + 5.3, + 6.6 + ], + "ink": [ + -1, + -2.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.056, + -2.054 + ], + "curve": [ + -1.085, + -2.119 + ], + "mark": [ + -1, + -2.25 + ] + } + }, + { + "n": "sukun", + "l": 5, + "b": [ + 103.4, + 161.6, + 4.4, + 3 + ], + "ink": [ + -1.187, + -2.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.01, + -2.021 + ], + "curve": [ + -1.034, + -2.074 + ], + "mark": [ + -1.187, + -2.25 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 91.7, + 162.9, + 6.3, + 3.4 + ], + "ink": [ + -0.937, + -2 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.952, + -1.979 + ], + "curve": [ + -0.969, + -2.016 + ], + "mark": [ + -0.937, + -2 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 83.5, + 156.9, + 7, + 3.5 + ], + "ink": [ + -0.937, + -1.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.91, + -1.949 + ], + "curve": [ + -0.92, + -1.972 + ], + "mark": [ + -0.937, + -1.937 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 67.6, + 157.2, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -1.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.819, + -1.884 + ], + "curve": [ + -0.815, + -1.874 + ], + "mark": [ + -0.625, + -1.687 + ] + } + }, + { + "n": "sukun", + "l": 5, + "b": [ + 62.6, + 160.3, + 4.4, + 3 + ], + "ink": [ + -0.625, + -1.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.79, + -1.862 + ], + "curve": [ + -0.78, + -1.841 + ], + "mark": [ + -0.625, + -1.75 + ] + } + }, + { + "n": "kasra", + "l": 5, + "b": [ + 56.2, + 182.7, + 6.3, + 3.4 + ], + "ink": [ + -0.875, + -1.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.76, + -1.841 + ], + "curve": [ + -0.745, + -1.807 + ], + "mark": [ + -0.875, + -1.812 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 48.4, + 163.5, + 7, + 3.6 + ], + "ink": [ + -0.625, + -1.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.72, + -1.812 + ], + "curve": [ + -0.696, + -1.76 + ], + "mark": [ + -0.625, + -1.687 + ] + } + }, + { + "n": "fatha", + "l": 5, + "b": [ + 35.5, + 161.1, + 7, + 3.6 + ], + "ink": [ + -0.5, + -1.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.65, + -1.762 + ], + "curve": [ + -0.611, + -1.675 + ], + "mark": [ + -0.5, + -1.5 + ] + } + }, + { + "n": "damma", + "l": 5, + "b": [ + 12.1, + 153.8, + 5.3, + 6.6 + ], + "ink": [ + -0.375, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.519, + -1.668 + ], + "curve": [ + -0.447, + -1.506 + ], + "mark": [ + -0.375, + -1.562 + ] + } + }, + { + "n": "sukun", + "l": 5, + "b": [ + 9.6, + 169.9, + 4.4, + 3 + ], + "ink": [ + -0.687, + -1.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.504, + -1.656 + ], + "curve": [ + -0.426, + -1.484 + ], + "mark": [ + -0.687, + -1.687 + ] + } + }, + { + "n": "kasra", + "l": 6, + "b": [ + 326.2, + 213.8, + 5.6, + 3.1 + ], + "ink": [ + -0.187, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.796, + -1.663 + ], + "curve": [ + -0.64, + -1.642 + ], + "mark": [ + -0.187, + -1.562 + ] + } + }, + { + "n": "hamza", + "l": 6, + "b": [ + 320.4, + 204.1, + 4, + 3.9 + ], + "ink": [ + -0.187, + -1.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.8, + -1.647 + ], + "curve": [ + -0.667, + -1.629 + ], + "mark": [ + -0.187, + -1.625 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 317.1, + 197.4, + 7.8, + 3.7 + ], + "ink": [ + -0.937, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.801, + -1.644 + ], + "curve": [ + -0.673, + -1.627 + ], + "mark": [ + -0.937, + -1.562 + ] + } + }, + { + "n": "damma", + "l": 6, + "b": [ + 307.5, + 195.5, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -1.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.807, + -1.618 + ], + "curve": [ + -0.715, + -1.606 + ], + "mark": [ + -0.875, + -1.625 + ] + } + }, + { + "n": "damma", + "l": 6, + "b": [ + 296.2, + 190.5, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.814, + -1.592 + ], + "curve": [ + -0.755, + -1.584 + ], + "mark": [ + -0.875, + -1.562 + ] + } + }, + { + "n": "sukun", + "l": 6, + "b": [ + 293.7, + 206.2, + 4.4, + 3 + ], + "ink": [ + -0.25, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.816, + -1.585 + ], + "curve": [ + -0.765, + -1.578 + ], + "mark": [ + -0.25, + -1.562 + ] + } + }, + { + "n": "sukun", + "l": 6, + "b": [ + 268.9, + 201.9, + 4.4, + 3 + ], + "ink": [ + -1.125, + -1.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.831, + -1.527 + ], + "curve": [ + -0.841, + -1.528 + ], + "mark": [ + -1.125, + -1.625 + ] + } + }, + { + "n": "hamza", + "l": 6, + "b": [ + 262.2, + 204.8, + 4, + 3.9 + ], + "ink": [ + -0.75, + -1.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.835, + -1.51 + ], + "curve": [ + -0.86, + -1.514 + ], + "mark": [ + -0.75, + -1.625 + ] + } + }, + { + "n": "successive fathatan", + "l": 6, + "b": [ + 260, + 197.2, + 7.4, + 3.8 + ], + "ink": [ + -1.125, + -1.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.835, + -1.509 + ], + "curve": [ + -0.861, + -1.513 + ], + "mark": [ + -1.125, + -1.562 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 277.9, + 195.2, + 7.8, + 3.7 + ], + "ink": [ + -1.062, + -1.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.824, + -1.552 + ], + "curve": [ + -0.81, + -1.55 + ], + "mark": [ + -1.062, + -1.5 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 247.3, + 199, + 6.3, + 3.4 + ], + "ink": [ + -1.25, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.843, + -1.478 + ], + "curve": [ + -0.893, + -1.485 + ], + "mark": [ + -1.25, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 238.3, + 192.3, + 7, + 3.6 + ], + "ink": [ + -1, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.849, + -1.458 + ], + "curve": [ + -0.911, + -1.466 + ], + "mark": [ + -1, + -1.437 + ] + } + }, + { + "n": "sukun", + "l": 6, + "b": [ + 236, + 201, + 4.4, + 3 + ], + "ink": [ + -1.187, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.851, + -1.449 + ], + "curve": [ + -0.918, + -1.458 + ], + "mark": [ + -1.187, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 208.5, + 195.9, + 7, + 3.6 + ], + "ink": [ + -1.125, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.867, + -1.388 + ], + "curve": [ + -0.959, + -1.4 + ], + "mark": [ + -1.125, + -1.437 + ] + } + }, + { + "n": "damma", + "l": 6, + "b": [ + 199.4, + 189.8, + 5.3, + 6.6 + ], + "ink": [ + -1, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.873, + -1.365 + ], + "curve": [ + -0.971, + -1.378 + ], + "mark": [ + -1, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 194.1, + 198.4, + 5.6, + 3.1 + ], + "ink": [ + -1.25, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.876, + -1.353 + ], + "curve": [ + -0.975, + -1.366 + ], + "mark": [ + -1.25, + -1.312 + ] + } + }, + { + "n": "sukun", + "l": 6, + "b": [ + 185.5, + 196.9, + 4.4, + 3 + ], + "ink": [ + -1, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.882, + -1.331 + ], + "curve": [ + -0.982, + -1.344 + ], + "mark": [ + -1, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 170.3, + 199.1, + 7, + 3.6 + ], + "ink": [ + -1.062, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.89, + -1.298 + ], + "curve": [ + -0.989, + -1.311 + ], + "mark": [ + -1.062, + -1.437 + ] + } + }, + { + "n": "hamza", + "l": 6, + "b": [ + 162.9, + 191.6, + 4, + 3.9 + ], + "ink": [ + -0.875, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.895, + -1.277 + ], + "curve": [ + -0.99, + -1.29 + ], + "mark": [ + -0.875, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 160.4, + 188.4, + 7, + 3.6 + ], + "ink": [ + -0.875, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.896, + -1.275 + ], + "curve": [ + -0.99, + -1.288 + ], + "mark": [ + -0.875, + -1.312 + ] + } + }, + { + "n": "shadda", + "l": 6, + "b": [ + 149.7, + 199.7, + 4.3, + 3.3 + ], + "ink": [ + -1, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.903, + -1.247 + ], + "curve": [ + -0.989, + -1.258 + ], + "mark": [ + -1, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 146, + 195.5, + 8.5, + 3.9 + ], + "ink": [ + -0.812, + -1.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.904, + -1.243 + ], + "curve": [ + -0.988, + -1.254 + ], + "mark": [ + -0.812, + -1.5 + ] + } + }, + { + "n": "wasla", + "l": 6, + "b": [ + 128.8, + 191.8, + 5.3, + 4.3 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.916, + -1.199 + ], + "curve": [ + -0.978, + -1.207 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 6, + "b": [ + 121.8, + 196.6, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.92, + -1.181 + ], + "curve": [ + -0.971, + -1.188 + ], + "mark": [ + -0.75, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 119.7, + 192, + 7, + 3.6 + ], + "ink": [ + -0.687, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.921, + -1.18 + ], + "curve": [ + -0.97, + -1.186 + ], + "mark": [ + -0.687, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 113, + 198.4, + 7, + 3.6 + ], + "ink": [ + -0.562, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.925, + -1.164 + ], + "curve": [ + -0.963, + -1.169 + ], + "mark": [ + -0.562, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 102.3, + 197.9, + 7.8, + 3.7 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.931, + -1.14 + ], + "curve": [ + -0.95, + -1.142 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 94.3, + 197.6, + 7, + 3.6 + ], + "ink": [ + -0.75, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.936, + -1.12 + ], + "curve": [ + -0.937, + -1.12 + ], + "mark": [ + -0.75, + -0.812 + ] + } + }, + { + "n": "wasla", + "l": 6, + "b": [ + 86.2, + 191.1, + 5.3, + 4.3 + ], + "ink": [ + -1.812, + -1.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.942, + -1.099 + ], + "curve": [ + -0.921, + -1.096 + ], + "mark": [ + -1.812, + -1.375 + ] + } + }, + { + "n": "sukun", + "l": 6, + "b": [ + 80.4, + 191.9, + 4.4, + 3 + ], + "ink": [ + -1.875, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.945, + -1.084 + ], + "curve": [ + -0.909, + -1.079 + ], + "mark": [ + -1.875, + -1.312 + ] + } + }, + { + "n": "damma", + "l": 6, + "b": [ + 74.5, + 197.6, + 5.3, + 6.6 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.949, + -1.072 + ], + "curve": [ + -0.897, + -1.065 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 6, + "b": [ + 67.6, + 197.1, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.953, + -1.054 + ], + "curve": [ + -0.881, + -1.045 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "hamza", + "l": 6, + "b": [ + 68, + 202.4, + 4, + 3.9 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.953, + -1.055 + ], + "curve": [ + -0.881, + -1.045 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 6, + "b": [ + 60.4, + 211.8, + 7, + 3.6 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.957, + -1.04 + ], + "curve": [ + -0.866, + -1.028 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 6, + "b": [ + 49, + 213.8, + 7, + 3.5 + ], + "ink": [ + -0.625, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.964, + -1.014 + ], + "curve": [ + -0.836, + -0.997 + ], + "mark": [ + -0.625, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 6, + "b": [ + 28.1, + 196.1, + 7, + 3.6 + ], + "ink": [ + -1.812, + -1.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.976, + -0.965 + ], + "curve": [ + -0.772, + -0.938 + ], + "mark": [ + -1.812, + -1.25 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 325.7, + 233.9, + 5.6, + 3.1 + ], + "ink": [ + -1.687, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.289, + -1.287 + ], + "curve": [ + -1.64, + -1.331 + ], + "mark": [ + -1.687, + -1.312 + ] + } + }, + { + "n": "superscript alef", + "l": 7, + "b": [ + 321.4, + 235.9, + 2.1, + 6.1 + ], + "ink": [ + -1.812, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.271, + -1.277 + ], + "curve": [ + -1.574, + -1.314 + ], + "mark": [ + -1.812, + -1.312 + ] + } + }, + { + "n": "maddah", + "l": 7, + "b": [ + 318.7, + 232.3, + 6.6, + 3.1 + ], + "ink": [ + -1.625, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.269, + -1.276 + ], + "curve": [ + -1.569, + -1.313 + ], + "mark": [ + -1.625, + -1.312 + ] + } + }, + { + "n": "hamza", + "l": 7, + "b": [ + 313.6, + 226.2, + 4, + 3.9 + ], + "ink": [ + -1.375, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.251, + -1.264 + ], + "curve": [ + -1.502, + -1.295 + ], + "mark": [ + -1.375, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 311.7, + 222.5, + 6.3, + 3.4 + ], + "ink": [ + -1.312, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.248, + -1.263 + ], + "curve": [ + -1.494, + -1.293 + ], + "mark": [ + -1.312, + -1.187 + ] + } + }, + { + "n": "shadda", + "l": 7, + "b": [ + 306.2, + 230.8, + 4.3, + 3.3 + ], + "ink": [ + -1.5, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.229, + -1.251 + ], + "curve": [ + -1.428, + -1.276 + ], + "mark": [ + -1.5, + -1.312 + ] + } + }, + { + "n": "damma", + "l": 7, + "b": [ + 303.8, + 224.8, + 5.3, + 6.6 + ], + "ink": [ + -1.375, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.224, + -1.248 + ], + "curve": [ + -1.409, + -1.271 + ], + "mark": [ + -1.375, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 298.3, + 227.6, + 6.3, + 3.4 + ], + "ink": [ + -1.437, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.209, + -1.239 + ], + "curve": [ + -1.36, + -1.258 + ], + "mark": [ + -1.437, + -1.312 + ] + } + }, + { + "n": "wasla", + "l": 7, + "b": [ + 286.6, + 224.8, + 5.3, + 4.3 + ], + "ink": [ + -1.125, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.173, + -1.218 + ], + "curve": [ + -1.247, + -1.227 + ], + "mark": [ + -1.125, + -1.062 + ] + } + }, + { + "n": "shadda", + "l": 7, + "b": [ + 281.4, + 224.8, + 4.3, + 3.3 + ], + "ink": [ + -1.125, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.157, + -1.207 + ], + "curve": [ + -1.197, + -1.212 + ], + "mark": [ + -1.125, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 279.3, + 220.8, + 7, + 3.6 + ], + "ink": [ + -1, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.154, + -1.206 + ], + "curve": [ + -1.19, + -1.211 + ], + "mark": [ + -1, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 7, + "b": [ + 276.9, + 249.4, + 7, + 3.6 + ], + "ink": [ + -1.187, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.147, + -1.202 + ], + "curve": [ + -1.17, + -1.205 + ], + "mark": [ + -1.187, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 254.9, + 236.3, + 8.5, + 3.9 + ], + "ink": [ + -1.125, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.085, + -1.164 + ], + "curve": [ + -1.001, + -1.154 + ], + "mark": [ + -1.125, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 236.9, + 234.3, + 6.3, + 3.4 + ], + "ink": [ + -0.875, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.029, + -1.13 + ], + "curve": [ + -0.87, + -1.11 + ], + "mark": [ + -0.875, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 225.6, + 231.6, + 6.3, + 3.4 + ], + "ink": [ + -0.75, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.996, + -1.11 + ], + "curve": [ + -0.802, + -1.086 + ], + "mark": [ + -0.75, + -1.187 + ] + } + }, + { + "n": "damma", + "l": 7, + "b": [ + 218.4, + 230, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.973, + -1.096 + ], + "curve": [ + -0.76, + -1.07 + ], + "mark": [ + -0.75, + -1.187 + ] + } + }, + { + "n": "maddah", + "l": 7, + "b": [ + 212.4, + 232.3, + 6.6, + 3.1 + ], + "ink": [ + -0.812, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.958, + -1.087 + ], + "curve": [ + -0.732, + -1.059 + ], + "mark": [ + -0.812, + -1.187 + ] + } + }, + { + "n": "rounded zero", + "l": 7, + "b": [ + 208.2, + 226.6, + 3.2, + 4.1 + ], + "ink": [ + -0.812, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.94, + -1.076 + ], + "curve": [ + -0.703, + -1.047 + ], + "mark": [ + -0.812, + -1.187 + ] + } + }, + { + "n": "hamza", + "l": 7, + "b": [ + 199.9, + 226.7, + 4, + 3.9 + ], + "ink": [ + -0.625, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.917, + -1.062 + ], + "curve": [ + -0.668, + -1.031 + ], + "mark": [ + -0.625, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 197.9, + 223.1, + 6.3, + 3.4 + ], + "ink": [ + -0.687, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.915, + -1.061 + ], + "curve": [ + -0.664, + -1.03 + ], + "mark": [ + -0.687, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 7, + "b": [ + 192.3, + 248.2, + 7, + 3.5 + ], + "ink": [ + -0.625, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.899, + -1.051 + ], + "curve": [ + -0.642, + -1.02 + ], + "mark": [ + -0.625, + -1.187 + ] + } + }, + { + "n": "damma", + "l": 7, + "b": [ + 175.8, + 231.6, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.848, + -1.021 + ], + "curve": [ + -0.581, + -0.988 + ], + "mark": [ + -0.625, + -1.187 + ] + } + }, + { + "n": "rounded zero", + "l": 7, + "b": [ + 164.4, + 226.8, + 3.2, + 4.1 + ], + "ink": [ + -0.625, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.812, + -0.999 + ], + "curve": [ + -0.548, + -0.966 + ], + "mark": [ + -0.625, + -1.187 + ] + } + }, + { + "n": "wasla", + "l": 7, + "b": [ + 156.8, + 226.1, + 5.3, + 4.3 + ], + "ink": [ + -0.375, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.793, + -0.987 + ], + "curve": [ + -0.533, + -0.955 + ], + "mark": [ + -0.375, + -1.062 + ] + } + }, + { + "n": "shadda", + "l": 7, + "b": [ + 150, + 232.4, + 4.3, + 3.3 + ], + "ink": [ + -0.375, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.771, + -0.974 + ], + "curve": [ + -0.52, + -0.943 + ], + "mark": [ + -0.375, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 148.1, + 227.7, + 7, + 3.6 + ], + "ink": [ + -0.375, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.77, + -0.973 + ], + "curve": [ + -0.519, + -0.942 + ], + "mark": [ + -0.375, + -1 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 141.6, + 234.1, + 6.3, + 3.4 + ], + "ink": [ + -0.5, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.749, + -0.961 + ], + "curve": [ + -0.509, + -0.931 + ], + "mark": [ + -0.5, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 133.3, + 234.2, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.726, + -0.947 + ], + "curve": [ + -0.501, + -0.919 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 122.8, + 234.7, + 7, + 3.5 + ], + "ink": [ + -0.75, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.695, + -0.928 + ], + "curve": [ + -0.495, + -0.903 + ], + "mark": [ + -0.75, + -0.5 + ] + } + }, + { + "n": "damma", + "l": 7, + "b": [ + 113.3, + 231.2, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.665, + -0.91 + ], + "curve": [ + -0.496, + -0.889 + ], + "mark": [ + -0.75, + -0.5 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 97.4, + 227.4, + 7, + 3.6 + ], + "ink": [ + -0.687, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.621, + -0.883 + ], + "curve": [ + -0.506, + -0.869 + ], + "mark": [ + -0.687, + -0.5 + ] + } + }, + { + "n": "damma", + "l": 7, + "b": [ + 92, + 231.6, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.602, + -0.872 + ], + "curve": [ + -0.514, + -0.861 + ], + "mark": [ + -0.75, + -0.5 + ] + } + }, + { + "n": "small waw", + "l": 7, + "b": [ + 86.3, + 243, + 5.5, + 5.7 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.586, + -0.862 + ], + "curve": [ + -0.523, + -0.854 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 78.8, + 234.5, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.566, + -0.85 + ], + "curve": [ + -0.536, + -0.846 + ], + "mark": [ + -0.562, + -0.5 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 70.5, + 225.9, + 7, + 3.5 + ], + "ink": [ + -0.5, + -0.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.542, + -0.835 + ], + "curve": [ + -0.555, + -0.837 + ], + "mark": [ + -0.5, + -0.562 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 57.2, + 229.3, + 6.3, + 3.4 + ], + "ink": [ + -0.5, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.502, + -0.811 + ], + "curve": [ + -0.595, + -0.822 + ], + "mark": [ + -0.5, + -1 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 53.3, + 238.1, + 5.6, + 3.1 + ], + "ink": [ + -0.562, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.489, + -0.803 + ], + "curve": [ + -0.609, + -0.818 + ], + "mark": [ + -0.562, + -0.75 + ] + } + }, + { + "n": "shadda", + "l": 7, + "b": [ + 47.3, + 225.9, + 4.3, + 3.3 + ], + "ink": [ + -0.375, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.47, + -0.791 + ], + "curve": [ + -0.633, + -0.812 + ], + "mark": [ + -0.375, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 45.4, + 222.3, + 7, + 3.6 + ], + "ink": [ + -0.312, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.468, + -0.79 + ], + "curve": [ + -0.636, + -0.811 + ], + "mark": [ + -0.312, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 7, + "b": [ + 43.4, + 234.6, + 4.4, + 3 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.459, + -0.785 + ], + "curve": [ + -0.649, + -0.808 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "rounded zero", + "l": 7, + "b": [ + 38, + 227.1, + 3.2, + 4.1 + ], + "ink": [ + -0.375, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.441, + -0.774 + ], + "curve": [ + -0.674, + -0.803 + ], + "mark": [ + -0.375, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 7, + "b": [ + 25.8, + 231.4, + 7, + 3.6 + ], + "ink": [ + -1, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.411, + -0.756 + ], + "curve": [ + -0.722, + -0.794 + ], + "mark": [ + -1, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 7, + "b": [ + 19.2, + 232.5, + 4.4, + 3 + ], + "ink": [ + -1.062, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.388, + -0.742 + ], + "curve": [ + -0.762, + -0.788 + ], + "mark": [ + -1.062, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 7, + "b": [ + 7.8, + 229.6, + 5.3, + 6.6 + ], + "ink": [ + -1, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.355, + -0.722 + ], + "curve": [ + -0.824, + -0.78 + ], + "mark": [ + -1, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 325.6, + 271.7, + 6.3, + 3.4 + ], + "ink": [ + -0.937, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.554, + -0.917 + ], + "curve": [ + -0.713, + -0.694 + ], + "mark": [ + -0.937, + -0.75 + ] + } + }, + { + "n": "hamza", + "l": 8, + "b": [ + 319.6, + 262.7, + 4, + 3.9 + ], + "ink": [ + -0.437, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.564, + -0.912 + ], + "curve": [ + -0.697, + -0.725 + ], + "mark": [ + -0.437, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 317.1, + 259.4, + 7, + 3.6 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.565, + -0.911 + ], + "curve": [ + -0.695, + -0.729 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 8, + "b": [ + 306.7, + 266.9, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.58, + -0.903 + ], + "curve": [ + -0.673, + -0.772 + ], + "mark": [ + -0.5, + -0.75 + ] + } + }, + { + "n": "sukun", + "l": 8, + "b": [ + 300.3, + 270.7, + 4.4, + 3 + ], + "ink": [ + -0.625, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.589, + -0.898 + ], + "curve": [ + -0.66, + -0.797 + ], + "mark": [ + -0.625, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 287.6, + 264.7, + 7.8, + 3.7 + ], + "ink": [ + -1, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.603, + -0.889 + ], + "curve": [ + -0.643, + -0.833 + ], + "mark": [ + -1, + -0.875 + ] + } + }, + { + "n": "sukun", + "l": 8, + "b": [ + 284.3, + 272.2, + 4.4, + 3 + ], + "ink": [ + -0.187, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.61, + -0.886 + ], + "curve": [ + -0.637, + -0.848 + ], + "mark": [ + -0.187, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 266.2, + 270, + 8.5, + 3.9 + ], + "ink": [ + -0.25, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.631, + -0.874 + ], + "curve": [ + -0.619, + -0.892 + ], + "mark": [ + -0.25, + -0.687 + ] + } + }, + { + "n": "damma", + "l": 8, + "b": [ + 258.1, + 268.3, + 5.3, + 6.6 + ], + "ink": [ + -1, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.644, + -0.867 + ], + "curve": [ + -0.611, + -0.913 + ], + "mark": [ + -1, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 242, + 267.7, + 7, + 3.5 + ], + "ink": [ + -0.937, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.664, + -0.856 + ], + "curve": [ + -0.603, + -0.942 + ], + "mark": [ + -0.937, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 212.9, + 271.1, + 6.3, + 3.4 + ], + "ink": [ + -1, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.703, + -0.834 + ], + "curve": [ + -0.603, + -0.975 + ], + "mark": [ + -1, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 205, + 262.4, + 7, + 3.6 + ], + "ink": [ + -1.187, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.713, + -0.828 + ], + "curve": [ + -0.606, + -0.978 + ], + "mark": [ + -1.187, + -1 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 189.2, + 268.3, + 7.8, + 3.7 + ], + "ink": [ + -0.812, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.734, + -0.817 + ], + "curve": [ + -0.617, + -0.98 + ], + "mark": [ + -0.812, + -1 + ] + } + }, + { + "n": "damma", + "l": 8, + "b": [ + 176.5, + 262, + 5.3, + 6.6 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.752, + -0.807 + ], + "curve": [ + -0.632, + -0.975 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 8, + "b": [ + 165.9, + 264.8, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.766, + -0.799 + ], + "curve": [ + -0.646, + -0.967 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "rounded zero", + "l": 8, + "b": [ + 159.2, + 263.5, + 3.2, + 4.1 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.776, + -0.793 + ], + "curve": [ + -0.658, + -0.959 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 132.5, + 268.1, + 7.8, + 3.7 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.809, + -0.775 + ], + "curve": [ + -0.705, + -0.92 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "wasla", + "l": 8, + "b": [ + 126.1, + 264.2, + 5.3, + 4.3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.819, + -0.769 + ], + "curve": [ + -0.723, + -0.904 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 8, + "b": [ + 120.8, + 264.1, + 4.3, + 3.3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.826, + -0.765 + ], + "curve": [ + -0.737, + -0.891 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 119, + 260.4, + 6.3, + 3.4 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.827, + -0.765 + ], + "curve": [ + -0.739, + -0.889 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 8, + "b": [ + 116, + 287.2, + 6.3, + 3.4 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.831, + -0.762 + ], + "curve": [ + -0.747, + -0.881 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 101.9, + 268.9, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.85, + -0.752 + ], + "curve": [ + -0.785, + -0.843 + ], + "mark": [ + -0.562, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 91, + 266.3, + 6.3, + 3.4 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.865, + -0.744 + ], + "curve": [ + -0.82, + -0.807 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 8, + "b": [ + 80.2, + 261.3, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.879, + -0.736 + ], + "curve": [ + -0.858, + -0.766 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "rounded zero", + "l": 8, + "b": [ + 72.8, + 263.2, + 3.2, + 4.1 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.891, + -0.729 + ], + "curve": [ + -0.888, + -0.734 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 61.6, + 269, + 8.5, + 3.9 + ], + "ink": [ + -1.437, + -0.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.902, + -0.723 + ], + "curve": [ + -0.92, + -0.698 + ], + "mark": [ + -1.437, + -0.375 + ] + } + }, + { + "n": "kasra", + "l": 8, + "b": [ + 50.9, + 285.6, + 7.8, + 3.7 + ], + "ink": [ + -0.312, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.917, + -0.715 + ], + "curve": [ + -0.964, + -0.648 + ], + "mark": [ + -0.312, + -0.875 + ] + } + }, + { + "n": "sukun", + "l": 8, + "b": [ + 44.9, + 271.7, + 4.4, + 3 + ], + "ink": [ + -0.437, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.927, + -0.709 + ], + "curve": [ + -0.997, + -0.611 + ], + "mark": [ + -0.437, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 34.6, + 268.9, + 8.5, + 3.9 + ], + "ink": [ + -1.5, + -0.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.938, + -0.703 + ], + "curve": [ + -1.033, + -0.569 + ], + "mark": [ + -1.5, + -0.375 + ] + } + }, + { + "n": "fatha", + "l": 8, + "b": [ + 22.5, + 270.2, + 6.3, + 3.4 + ], + "ink": [ + -1.562, + -0.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.955, + -0.693 + ], + "curve": [ + -1.094, + -0.498 + ], + "mark": [ + -1.562, + -0.312 + ] + } + }, + { + "n": "damma", + "l": 8, + "b": [ + 14.6, + 265.5, + 5.3, + 6.6 + ], + "ink": [ + -1.437, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.966, + -0.687 + ], + "curve": [ + -1.136, + -0.449 + ], + "mark": [ + -1.437, + -0.5 + ] + } + }, + { + "n": "sukun", + "l": 8, + "b": [ + 8.9, + 276.7, + 4.4, + 3 + ], + "ink": [ + -1.437, + -0.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.974, + -0.683 + ], + "curve": [ + -1.167, + -0.412 + ], + "mark": [ + -1.437, + -0.375 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 324.8, + 298.3, + 7, + 3.6 + ], + "ink": [ + -1.437, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -1.12, + -0.353 + ], + "curve": [ + -1.343, + -0.31 + ], + "mark": [ + -1.437, + -0.5 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 310.2, + 302.6, + 7, + 3.6 + ], + "ink": [ + -0.75, + -0.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -1.081, + -0.381 + ], + "curve": [ + -1.247, + -0.349 + ], + "mark": [ + -0.75, + -0.375 + ] + } + }, + { + "n": "sukun", + "l": 9, + "b": [ + 307.4, + 308.8, + 4.4, + 3 + ], + "ink": [ + -1.437, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -1.07, + -0.389 + ], + "curve": [ + -1.221, + -0.359 + ], + "mark": [ + -1.437, + -0.5 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 292.6, + 306.6, + 7.8, + 3.7 + ], + "ink": [ + -1.25, + -0.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -1.035, + -0.414 + ], + "curve": [ + -1.141, + -0.393 + ], + "mark": [ + -1.25, + -0.437 + ] + } + }, + { + "n": "damma", + "l": 9, + "b": [ + 285, + 304.5, + 5.3, + 6.6 + ], + "ink": [ + -1.125, + -0.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -1.011, + -0.431 + ], + "curve": [ + -1.089, + -0.415 + ], + "mark": [ + -1.125, + -0.375 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 269.1, + 303.1, + 6.3, + 3.4 + ], + "ink": [ + -1.125, + -0.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.97, + -0.46 + ], + "curve": [ + -1.004, + -0.453 + ], + "mark": [ + -1.125, + -0.312 + ] + } + }, + { + "n": "hamza", + "l": 9, + "b": [ + 235, + 320.3, + 4, + 3.9 + ], + "ink": [ + -0.75, + -0.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.876, + -0.527 + ], + "curve": [ + -0.833, + -0.535 + ], + "mark": [ + -0.75, + -0.375 + ] + } + }, + { + "n": "kasra", + "l": 9, + "b": [ + 235, + 324, + 4.9, + 2.6 + ], + "ink": [ + -1, + -0.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.877, + -0.526 + ], + "curve": [ + -0.835, + -0.534 + ], + "mark": [ + -1, + -0.312 + ] + } + }, + { + "n": "shadda", + "l": 9, + "b": [ + 224.8, + 305.6, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.849, + -0.546 + ], + "curve": [ + -0.79, + -0.558 + ], + "mark": [ + -0.75, + -0.5 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 222.7, + 301.7, + 7.8, + 3.7 + ], + "ink": [ + -0.687, + -0.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.848, + -0.547 + ], + "curve": [ + -0.789, + -0.558 + ], + "mark": [ + -0.687, + -0.562 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 207.3, + 301.5, + 7.8, + 3.7 + ], + "ink": [ + -0.75, + -0.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.807, + -0.576 + ], + "curve": [ + -0.729, + -0.592 + ], + "mark": [ + -0.75, + -0.562 + ] + } + }, + { + "n": "shadda", + "l": 9, + "b": [ + 199.2, + 309.5, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.562 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.78, + -0.595 + ], + "curve": [ + -0.693, + -0.612 + ], + "mark": [ + -0.75, + -0.562 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 196.9, + 305.4, + 7.8, + 3.7 + ], + "ink": [ + -0.75, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.779, + -0.596 + ], + "curve": [ + -0.691, + -0.613 + ], + "mark": [ + -0.75, + -0.5 + ] + } + }, + { + "n": "wasla", + "l": 9, + "b": [ + 190, + 299.3, + 5.3, + 4.3 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.757, + -0.612 + ], + "curve": [ + -0.664, + -0.63 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 9, + "b": [ + 180.8, + 304.3, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.731, + -0.63 + ], + "curve": [ + -0.634, + -0.649 + ], + "mark": [ + -0.625, + -0.5 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 178.1, + 300, + 7.8, + 3.7 + ], + "ink": [ + -0.437, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.729, + -0.632 + ], + "curve": [ + -0.631, + -0.651 + ], + "mark": [ + -0.437, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 169, + 304.5, + 7, + 3.6 + ], + "ink": [ + -0.687, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.703, + -0.65 + ], + "curve": [ + -0.604, + -0.67 + ], + "mark": [ + -0.687, + -0.5 + ] + } + }, + { + "n": "maddah", + "l": 9, + "b": [ + 162.6, + 299.4, + 6.6, + 3.1 + ], + "ink": [ + -0.437, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.685, + -0.663 + ], + "curve": [ + -0.586, + -0.682 + ], + "mark": [ + -0.437, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 9, + "b": [ + 155.4, + 308, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.663, + -0.679 + ], + "curve": [ + -0.566, + -0.698 + ], + "mark": [ + -0.75, + -0.5 + ] + } + }, + { + "n": "kasra", + "l": 9, + "b": [ + 155.3, + 323.9, + 7, + 3.6 + ], + "ink": [ + -0.75, + -0.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.666, + -0.676 + ], + "curve": [ + -0.569, + -0.695 + ], + "mark": [ + -0.75, + -0.5 + ] + } + }, + { + "n": "kasra", + "l": 9, + "b": [ + 140.6, + 320, + 7, + 3.6 + ], + "ink": [ + -0.5, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.627, + -0.705 + ], + "curve": [ + -0.537, + -0.722 + ], + "mark": [ + -0.5, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 122.3, + 303.9, + 7, + 3.6 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.578, + -0.739 + ], + "curve": [ + -0.505, + -0.754 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "wasla", + "l": 9, + "b": [ + 112.4, + 298.5, + 5.3, + 4.3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.549, + -0.76 + ], + "curve": [ + -0.49, + -0.772 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 9, + "b": [ + 105.9, + 303.6, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.531, + -0.773 + ], + "curve": [ + -0.482, + -0.783 + ], + "mark": [ + -0.625, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 9, + "b": [ + 103.4, + 299, + 7.8, + 3.7 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.529, + -0.775 + ], + "curve": [ + -0.481, + -0.784 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 9, + "b": [ + 97.7, + 320.7, + 6.3, + 3.4 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.511, + -0.787 + ], + "curve": [ + -0.475, + -0.794 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "wasla", + "l": 9, + "b": [ + 90.9, + 298.9, + 5.3, + 4.3 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.492, + -0.801 + ], + "curve": [ + -0.469, + -0.805 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 9, + "b": [ + 78.1, + 306.1, + 4.3, + 3.3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.456, + -0.826 + ], + "curve": [ + -0.462, + -0.825 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 9, + "b": [ + 76.1, + 299.3, + 5.3, + 6.6 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.452, + -0.829 + ], + "curve": [ + -0.462, + -0.827 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 9, + "b": [ + 63.2, + 307.4, + 4.3, + 3.3 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.416, + -0.855 + ], + "curve": [ + -0.46, + -0.846 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "damma", + "l": 9, + "b": [ + 61.2, + 300.1, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.412, + -0.858 + ], + "curve": [ + -0.46, + -0.848 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "wasla", + "l": 9, + "b": [ + 54.2, + 299.7, + 5.3, + 4.3 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.394, + -0.871 + ], + "curve": [ + -0.461, + -0.858 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 9, + "b": [ + 48.9, + 299.5, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.378, + -0.882 + ], + "curve": [ + -0.463, + -0.866 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 9, + "b": [ + 44.8, + 305.4, + 5.3, + 6.6 + ], + "ink": [ + -0.562, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.368, + -0.889 + ], + "curve": [ + -0.465, + -0.87 + ], + "mark": [ + -0.562, + -0.687 + ] + } + }, + { + "n": "sukun", + "l": 9, + "b": [ + 20.7, + 302.6, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.303, + -0.936 + ], + "curve": [ + -0.485, + -0.901 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 9, + "b": [ + 10.6, + 305.1, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.277, + -0.954 + ], + "curve": [ + -0.498, + -0.912 + ], + "mark": [ + -0.625, + -0.687 + ] + } + }, + { + "n": "wasla", + "l": 10, + "b": [ + 326.6, + 334.5, + 5.3, + 4.3 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.753, + -0.876 + ], + "curve": [ + -0.595, + -0.79 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 10, + "b": [ + 321.5, + 334.1, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.748, + -0.877 + ], + "curve": [ + -0.609, + -0.802 + ], + "mark": [ + -0.625, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 319.5, + 330.7, + 7, + 3.6 + ], + "ink": [ + -0.687, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.747, + -0.878 + ], + "curve": [ + -0.611, + -0.803 + ], + "mark": [ + -0.687, + -0.75 + ] + } + }, + { + "n": "kasra", + "l": 10, + "b": [ + 316.9, + 357.5, + 6.3, + 3.4 + ], + "ink": [ + -0.375, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.745, + -0.878 + ], + "curve": [ + -0.618, + -0.809 + ], + "mark": [ + -0.375, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 300.1, + 339.8, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.73, + -0.882 + ], + "curve": [ + -0.654, + -0.841 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 291.6, + 332.7, + 7, + 3.5 + ], + "ink": [ + -0.5, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.722, + -0.884 + ], + "curve": [ + -0.671, + -0.856 + ], + "mark": [ + -0.5, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 274.6, + 336.7, + 7, + 3.6 + ], + "ink": [ + -1.125, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.706, + -0.889 + ], + "curve": [ + -0.698, + -0.884 + ], + "mark": [ + -1.125, + -1.187 + ] + } + }, + { + "n": "sukun", + "l": 10, + "b": [ + 273.1, + 342.3, + 4.4, + 3 + ], + "ink": [ + -0.312, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.704, + -0.889 + ], + "curve": [ + -0.702, + -0.888 + ], + "mark": [ + -0.312, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 10, + "b": [ + 261.9, + 356.8, + 7, + 3.6 + ], + "ink": [ + -0.375, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.694, + -0.892 + ], + "curve": [ + -0.714, + -0.903 + ], + "mark": [ + -0.375, + -0.687 + ] + } + }, + { + "n": "damma", + "l": 10, + "b": [ + 252.6, + 333.9, + 5.3, + 6.6 + ], + "ink": [ + -1.125, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.685, + -0.894 + ], + "curve": [ + -0.725, + -0.916 + ], + "mark": [ + -1.125, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 232.1, + 341.7, + 8.5, + 3.9 + ], + "ink": [ + -1.125, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.668, + -0.899 + ], + "curve": [ + -0.738, + -0.937 + ], + "mark": [ + -1.125, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 194, + 341.9, + 6.3, + 3.4 + ], + "ink": [ + -1.125, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.632, + -0.908 + ], + "curve": [ + -0.739, + -0.967 + ], + "mark": [ + -1.125, + -1 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 185.9, + 334.5, + 7, + 3.6 + ], + "ink": [ + -1, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.625, + -0.91 + ], + "curve": [ + -0.735, + -0.971 + ], + "mark": [ + -1, + -1 + ] + } + }, + { + "n": "sukun", + "l": 10, + "b": [ + 183.7, + 343.4, + 4.4, + 3 + ], + "ink": [ + -1, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.622, + -0.911 + ], + "curve": [ + -0.733, + -0.972 + ], + "mark": [ + -1, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 171.9, + 340.4, + 6.3, + 3.4 + ], + "ink": [ + -0.937, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.612, + -0.914 + ], + "curve": [ + -0.724, + -0.976 + ], + "mark": [ + -0.937, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 10, + "b": [ + 169, + 358.1, + 7, + 3.6 + ], + "ink": [ + -0.937, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.609, + -0.914 + ], + "curve": [ + -0.722, + -0.976 + ], + "mark": [ + -0.937, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 159.9, + 344.1, + 6.3, + 3.4 + ], + "ink": [ + -0.875, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.601, + -0.917 + ], + "curve": [ + -0.712, + -0.978 + ], + "mark": [ + -0.875, + -1.062 + ] + } + }, + { + "n": "wasla", + "l": 10, + "b": [ + 152.1, + 334, + 5.3, + 4.3 + ], + "ink": [ + -0.312, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.593, + -0.919 + ], + "curve": [ + -0.701, + -0.978 + ], + "mark": [ + -0.312, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 10, + "b": [ + 145.5, + 339.7, + 4.3, + 3.3 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.586, + -0.92 + ], + "curve": [ + -0.691, + -0.978 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 143.2, + 335.1, + 7, + 3.6 + ], + "ink": [ + -0.312, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.586, + -0.921 + ], + "curve": [ + -0.689, + -0.978 + ], + "mark": [ + -0.312, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 10, + "b": [ + 137.4, + 339.9, + 5.3, + 6.6 + ], + "ink": [ + -0.562, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.579, + -0.922 + ], + "curve": [ + -0.678, + -0.977 + ], + "mark": [ + -0.562, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 10, + "b": [ + 129, + 358.5, + 7, + 3.6 + ], + "ink": [ + -0.312, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.572, + -0.924 + ], + "curve": [ + -0.665, + -0.975 + ], + "mark": [ + -0.312, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 10, + "b": [ + 116.1, + 360.5, + 7, + 3.6 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.561, + -0.927 + ], + "curve": [ + -0.639, + -0.97 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "sukun", + "l": 10, + "b": [ + 105.6, + 342.9, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.55, + -0.93 + ], + "curve": [ + -0.611, + -0.964 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 87.4, + 340.6, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.534, + -0.934 + ], + "curve": [ + -0.567, + -0.952 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 10, + "b": [ + 81.5, + 342.6, + 4.4, + 3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.528, + -0.936 + ], + "curve": [ + -0.546, + -0.946 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "successive fathatan", + "l": 10, + "b": [ + 75.1, + 337.8, + 7.7, + 3.8 + ], + "ink": [ + -0.187, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.523, + -0.937 + ], + "curve": [ + -0.531, + -0.942 + ], + "mark": [ + -0.187, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 10, + "b": [ + 63.7, + 332.5, + 4.3, + 3.3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.511, + -0.94 + ], + "curve": [ + -0.489, + -0.928 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 61.5, + 329.1, + 7.8, + 3.7 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.511, + -0.941 + ], + "curve": [ + -0.487, + -0.928 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "hamza", + "l": 10, + "b": [ + 53, + 335.4, + 4, + 3.9 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.501, + -0.943 + ], + "curve": [ + -0.45, + -0.915 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 50.7, + 331.4, + 7, + 3.5 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.501, + -0.943 + ], + "curve": [ + -0.448, + -0.914 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "sukun", + "l": 10, + "b": [ + 46.1, + 342.3, + 4.4, + 3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.495, + -0.945 + ], + "curve": [ + -0.426, + -0.907 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 34, + 340.8, + 7.8, + 3.7 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.486, + -0.947 + ], + "curve": [ + -0.385, + -0.892 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 10, + "b": [ + 24.7, + 340.3, + 7.8, + 3.7 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.477, + -0.95 + ], + "curve": [ + -0.346, + -0.878 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 10, + "b": [ + 15.4, + 337.6, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.467, + -0.952 + ], + "curve": [ + -0.3, + -0.861 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 10, + "b": [ + 10, + 346.4, + 4.4, + 3 + ], + "ink": [ + -0.25, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.462, + -0.954 + ], + "curve": [ + -0.274, + -0.85 + ], + "mark": [ + -0.25, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 325.6, + 379, + 6.3, + 3.4 + ], + "ink": [ + -0.187, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.807, + -0.918 + ], + "curve": [ + -0.674, + -1.014 + ], + "mark": [ + -0.187, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 316.9, + 370.2, + 7, + 3.6 + ], + "ink": [ + -0.125, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.797, + -0.916 + ], + "curve": [ + -0.687, + -0.995 + ], + "mark": [ + -0.125, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 11, + "b": [ + 315.6, + 380.4, + 4.4, + 3 + ], + "ink": [ + -0.187, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.794, + -0.916 + ], + "curve": [ + -0.691, + -0.989 + ], + "mark": [ + -0.187, + -0.937 + ] + } + }, + { + "n": "hamza", + "l": 11, + "b": [ + 307.6, + 369.8, + 4, + 3.9 + ], + "ink": [ + -0.937, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.784, + -0.914 + ], + "curve": [ + -0.703, + -0.972 + ], + "mark": [ + -0.937, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 305.1, + 366.1, + 7, + 3.6 + ], + "ink": [ + -0.937, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.783, + -0.914 + ], + "curve": [ + -0.704, + -0.97 + ], + "mark": [ + -0.937, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 11, + "b": [ + 300.8, + 378, + 4.4, + 3 + ], + "ink": [ + -1.187, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.777, + -0.912 + ], + "curve": [ + -0.711, + -0.959 + ], + "mark": [ + -1.187, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 286.2, + 377.1, + 7.8, + 3.7 + ], + "ink": [ + -1.062, + -1.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.761, + -0.909 + ], + "curve": [ + -0.725, + -0.935 + ], + "mark": [ + -1.062, + -1.25 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 277.7, + 376.8, + 7.8, + 3.7 + ], + "ink": [ + -1.062, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.751, + -0.907 + ], + "curve": [ + -0.733, + -0.92 + ], + "mark": [ + -1.062, + -1.062 + ] + } + }, + { + "n": "damma", + "l": 11, + "b": [ + 269.1, + 375.6, + 5.3, + 6.6 + ], + "ink": [ + -1, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.739, + -0.905 + ], + "curve": [ + -0.74, + -0.904 + ], + "mark": [ + -1, + -1.062 + ] + } + }, + { + "n": "sukun", + "l": 11, + "b": [ + 260.3, + 378.6, + 4.4, + 3 + ], + "ink": [ + -1.187, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.728, + -0.903 + ], + "curve": [ + -0.745, + -0.891 + ], + "mark": [ + -1.187, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 248.2, + 369.8, + 6.3, + 3.4 + ], + "ink": [ + -0.75, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.715, + -0.9 + ], + "curve": [ + -0.749, + -0.876 + ], + "mark": [ + -0.75, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 244.4, + 376.3, + 5.6, + 3.1 + ], + "ink": [ + -0.75, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.71, + -0.899 + ], + "curve": [ + -0.75, + -0.871 + ], + "mark": [ + -0.75, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 239.2, + 374.3, + 5.6, + 3.1 + ], + "ink": [ + -0.75, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.704, + -0.898 + ], + "curve": [ + -0.751, + -0.865 + ], + "mark": [ + -0.75, + -0.687 + ] + } + }, + { + "n": "shadda", + "l": 11, + "b": [ + 233.9, + 369.4, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.697, + -0.896 + ], + "curve": [ + -0.751, + -0.858 + ], + "mark": [ + -0.75, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 232.3, + 366, + 6.3, + 3.4 + ], + "ink": [ + -0.687, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.696, + -0.896 + ], + "curve": [ + -0.751, + -0.857 + ], + "mark": [ + -0.687, + -0.687 + ] + } + }, + { + "n": "rounded zero", + "l": 11, + "b": [ + 224.3, + 369.5, + 3.2, + 4.1 + ], + "ink": [ + -0.75, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.685, + -0.894 + ], + "curve": [ + -0.75, + -0.848 + ], + "mark": [ + -0.75, + -0.625 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 214.8, + 373.8, + 7.8, + 3.7 + ], + "ink": [ + -0.687, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.676, + -0.892 + ], + "curve": [ + -0.747, + -0.841 + ], + "mark": [ + -0.687, + -0.687 + ] + } + }, + { + "n": "shadda", + "l": 11, + "b": [ + 217.3, + 378.1, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.677, + -0.892 + ], + "curve": [ + -0.748, + -0.842 + ], + "mark": [ + -0.75, + -0.625 + ] + } + }, + { + "n": "damma", + "l": 11, + "b": [ + 203.9, + 373.6, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.662, + -0.889 + ], + "curve": [ + -0.742, + -0.832 + ], + "mark": [ + -0.625, + -0.625 + ] + } + }, + { + "n": "shadda", + "l": 11, + "b": [ + 186, + 377.8, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.64, + -0.885 + ], + "curve": [ + -0.728, + -0.822 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 11, + "b": [ + 183, + 371.3, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.637, + -0.884 + ], + "curve": [ + -0.726, + -0.821 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 11, + "b": [ + 176.1, + 377.2, + 4.4, + 3 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.628, + -0.883 + ], + "curve": [ + -0.718, + -0.819 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 11, + "b": [ + 163.4, + 397.4, + 8.5, + 3.9 + ], + "ink": [ + -0.625, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.616, + -0.88 + ], + "curve": [ + -0.705, + -0.816 + ], + "mark": [ + -0.625, + -0.687 + ] + } + }, + { + "n": "damma", + "l": 11, + "b": [ + 153.1, + 373.7, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.601, + -0.877 + ], + "curve": [ + -0.688, + -0.815 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 126.4, + 380, + 8.5, + 3.9 + ], + "ink": [ + -0.562, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.572, + -0.871 + ], + "curve": [ + -0.644, + -0.82 + ], + "mark": [ + -0.562, + -1 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 87.3, + 378.3, + 6.3, + 3.4 + ], + "ink": [ + -0.5, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.524, + -0.862 + ], + "curve": [ + -0.549, + -0.844 + ], + "mark": [ + -0.5, + -1 + ] + } + }, + { + "n": "superscript alef", + "l": 11, + "b": [ + 84, + 380.2, + 2.1, + 6.1 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.517, + -0.86 + ], + "curve": [ + -0.534, + -0.849 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "maddah", + "l": 11, + "b": [ + 81, + 376.8, + 6.6, + 3.1 + ], + "ink": [ + -0.437, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.517, + -0.86 + ], + "curve": [ + -0.532, + -0.849 + ], + "mark": [ + -0.437, + -0.937 + ] + } + }, + { + "n": "hamza", + "l": 11, + "b": [ + 75.9, + 371.4, + 4, + 3.9 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.509, + -0.859 + ], + "curve": [ + -0.513, + -0.856 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 74.3, + 367.9, + 6.3, + 3.4 + ], + "ink": [ + -0.562, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.508, + -0.858 + ], + "curve": [ + -0.512, + -0.856 + ], + "mark": [ + -0.562, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 11, + "b": [ + 68.6, + 373.8, + 4.3, + 3.3 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.5, + -0.857 + ], + "curve": [ + -0.491, + -0.863 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 11, + "b": [ + 65.9, + 367.1, + 5.3, + 6.7 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.498, + -0.856 + ], + "curve": [ + -0.484, + -0.866 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 57.8, + 376.8, + 8.5, + 3.9 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.49, + -0.855 + ], + "curve": [ + -0.464, + -0.874 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "wasla", + "l": 11, + "b": [ + 43.9, + 370.4, + 5.3, + 4.3 + ], + "ink": [ + -0.375, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.472, + -0.851 + ], + "curve": [ + -0.411, + -0.895 + ], + "mark": [ + -0.375, + -0.875 + ] + } + }, + { + "n": "shadda", + "l": 11, + "b": [ + 38.3, + 370.3, + 4.3, + 3.3 + ], + "ink": [ + -0.375, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.464, + -0.85 + ], + "curve": [ + -0.388, + -0.904 + ], + "mark": [ + -0.375, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 36.4, + 366.6, + 7, + 3.6 + ], + "ink": [ + -0.375, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.464, + -0.85 + ], + "curve": [ + -0.386, + -0.905 + ], + "mark": [ + -0.375, + -0.75 + ] + } + }, + { + "n": "kasra", + "l": 11, + "b": [ + 33.5, + 395.3, + 7, + 3.5 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.46, + -0.849 + ], + "curve": [ + -0.376, + -0.909 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 11, + "b": [ + 13.3, + 379.4, + 8.5, + 3.9 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.437, + -0.844 + ], + "curve": [ + -0.299, + -0.942 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 324.5, + 413.5, + 7, + 3.6 + ], + "ink": [ + -1.5, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.664, + -1.422 + ], + "curve": [ + -1.557, + -1.461 + ], + "mark": [ + -1.5, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 311.5, + 412.4, + 7, + 3.6 + ], + "ink": [ + -1.562, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.614, + -1.391 + ], + "curve": [ + -1.531, + -1.421 + ], + "mark": [ + -1.562, + -1.312 + ] + } + }, + { + "n": "damma", + "l": 12, + "b": [ + 299.7, + 409.3, + 5.3, + 6.6 + ], + "ink": [ + -1.5, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.565, + -1.361 + ], + "curve": [ + -1.504, + -1.383 + ], + "mark": [ + -1.5, + -1.437 + ] + } + }, + { + "n": "rounded zero", + "l": 12, + "b": [ + 290.8, + 406.7, + 3.2, + 4.1 + ], + "ink": [ + -1.562, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.527, + -1.337 + ], + "curve": [ + -1.482, + -1.353 + ], + "mark": [ + -1.562, + -1.437 + ] + } + }, + { + "n": "wasla", + "l": 12, + "b": [ + 282.4, + 406.7, + 5.3, + 4.3 + ], + "ink": [ + -1.25, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.498, + -1.319 + ], + "curve": [ + -1.464, + -1.332 + ], + "mark": [ + -1.25, + -1.187 + ] + } + }, + { + "n": "sukun", + "l": 12, + "b": [ + 277.3, + 413.4, + 4.4, + 3 + ], + "ink": [ + -1.437, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.477, + -1.306 + ], + "curve": [ + -1.451, + -1.316 + ], + "mark": [ + -1.437, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 257.3, + 410.8, + 8.5, + 3.9 + ], + "ink": [ + -1.25, + -1.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.408, + -1.263 + ], + "curve": [ + -1.404, + -1.265 + ], + "mark": [ + -1.25, + -1.25 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 249.7, + 429.8, + 7.8, + 3.7 + ], + "ink": [ + -1.562, + -1.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.377, + -1.244 + ], + "curve": [ + -1.382, + -1.242 + ], + "mark": [ + -1.562, + -1.5 + ] + } + }, + { + "n": "damma", + "l": 12, + "b": [ + 229.2, + 411.3, + 5.3, + 6.6 + ], + "ink": [ + -1.312, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.293, + -1.192 + ], + "curve": [ + -1.318, + -1.183 + ], + "mark": [ + -1.312, + -1.312 + ] + } + }, + { + "n": "rounded zero", + "l": 12, + "b": [ + 220.1, + 406.9, + 3.2, + 4.1 + ], + "ink": [ + -1.312, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.254, + -1.168 + ], + "curve": [ + -1.286, + -1.156 + ], + "mark": [ + -1.312, + -1.187 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 212.8, + 427.4, + 7, + 3.6 + ], + "ink": [ + -1.312, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.233, + -1.155 + ], + "curve": [ + -1.269, + -1.142 + ], + "mark": [ + -1.312, + -1.187 + ] + } + }, + { + "n": "shadda", + "l": 12, + "b": [ + 208.6, + 412.3, + 4.3, + 3.3 + ], + "ink": [ + -1.312, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.212, + -1.142 + ], + "curve": [ + -1.25, + -1.128 + ], + "mark": [ + -1.312, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 206, + 407.8, + 7.8, + 3.7 + ], + "ink": [ + -1.312, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.208, + -1.14 + ], + "curve": [ + -1.247, + -1.126 + ], + "mark": [ + -1.312, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 200, + 428.5, + 7, + 3.6 + ], + "ink": [ + -1.562, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.184, + -1.125 + ], + "curve": [ + -1.226, + -1.109 + ], + "mark": [ + -1.562, + -1.062 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 190.7, + 413.9, + 6.3, + 3.4 + ], + "ink": [ + -1.187, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.147, + -1.102 + ], + "curve": [ + -1.192, + -1.085 + ], + "mark": [ + -1.187, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 184.1, + 426.7, + 7, + 3.6 + ], + "ink": [ + -1.125, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.122, + -1.087 + ], + "curve": [ + -1.17, + -1.069 + ], + "mark": [ + -1.125, + -1.062 + ] + } + }, + { + "n": "shadda", + "l": 12, + "b": [ + 174.9, + 414.5, + 4.3, + 3.3 + ], + "ink": [ + -1.062, + -1 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.082, + -1.062 + ], + "curve": [ + -1.131, + -1.044 + ], + "mark": [ + -1.062, + -1 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 172.7, + 409.8, + 7, + 3.6 + ], + "ink": [ + -1, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.078, + -1.06 + ], + "curve": [ + -1.127, + -1.042 + ], + "mark": [ + -1, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 12, + "b": [ + 163, + 411.4, + 5.3, + 6.6 + ], + "ink": [ + -1.062, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.038, + -1.035 + ], + "curve": [ + -1.087, + -1.016 + ], + "mark": [ + -1.062, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 144.8, + 428, + 7, + 3.6 + ], + "ink": [ + -1.125, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.971, + -0.993 + ], + "curve": [ + -1.018, + -0.976 + ], + "mark": [ + -1.125, + -1.062 + ] + } + }, + { + "n": "hamza", + "l": 12, + "b": [ + 134.7, + 427.5, + 4, + 3.9 + ], + "ink": [ + -1, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.926, + -0.965 + ], + "curve": [ + -0.969, + -0.95 + ], + "mark": [ + -1, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 134.8, + 431.2, + 4.9, + 2.6 + ], + "ink": [ + -1, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.928, + -0.967 + ], + "curve": [ + -0.971, + -0.951 + ], + "mark": [ + -1, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 124.5, + 408.2, + 7, + 3.6 + ], + "ink": [ + -0.937, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.893, + -0.945 + ], + "curve": [ + -0.932, + -0.93 + ], + "mark": [ + -0.937, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 110.8, + 410.8, + 7.8, + 3.7 + ], + "ink": [ + -0.812, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.841, + -0.913 + ], + "curve": [ + -0.873, + -0.901 + ], + "mark": [ + -0.812, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 101.6, + 410, + 7.8, + 3.7 + ], + "ink": [ + -0.687, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.806, + -0.891 + ], + "curve": [ + -0.83, + -0.882 + ], + "mark": [ + -0.687, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 12, + "b": [ + 85.8, + 402.7, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.74, + -0.85 + ], + "curve": [ + -0.75, + -0.847 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 12, + "b": [ + 82.8, + 418, + 4.4, + 3 + ], + "ink": [ + -0.937, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.727, + -0.842 + ], + "curve": [ + -0.733, + -0.84 + ], + "mark": [ + -0.937, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 73, + 427.2, + 7, + 3.6 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.694, + -0.822 + ], + "curve": [ + -0.691, + -0.823 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 12, + "b": [ + 63, + 411.5, + 7.8, + 3.7 + ], + "ink": [ + -0.437, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.657, + -0.799 + ], + "curve": [ + -0.642, + -0.805 + ], + "mark": [ + -0.437, + -0.875 + ] + } + }, + { + "n": "damma", + "l": 12, + "b": [ + 47.3, + 405.7, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.592, + -0.759 + ], + "curve": [ + -0.554, + -0.773 + ], + "mark": [ + -0.5, + -0.625 + ] + } + }, + { + "n": "sukun", + "l": 12, + "b": [ + 42.2, + 414, + 4.4, + 3 + ], + "ink": [ + -0.562, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.57, + -0.745 + ], + "curve": [ + -0.524, + -0.763 + ], + "mark": [ + -0.562, + -0.625 + ] + } + }, + { + "n": "kasra", + "l": 12, + "b": [ + 37, + 431.8, + 7.8, + 3.7 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.557, + -0.737 + ], + "curve": [ + -0.505, + -0.756 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 12, + "b": [ + 18.8, + 401.6, + 5.3, + 6.6 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.482, + -0.691 + ], + "curve": [ + -0.397, + -0.722 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 12, + "b": [ + 10.7, + 418.8, + 4.4, + 3 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.449, + -0.67 + ], + "curve": [ + -0.348, + -0.708 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 324.8, + 446.9, + 7, + 3.6 + ], + "ink": [ + -1.312, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.337, + -1.495 + ], + "curve": [ + -0.987, + -1.268 + ], + "mark": [ + -1.312, + -1.312 + ] + } + }, + { + "n": "wasla", + "l": 13, + "b": [ + 318.5, + 441.8, + 5.3, + 4.3 + ], + "ink": [ + -1.25, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.322, + -1.481 + ], + "curve": [ + -1.022, + -1.286 + ], + "mark": [ + -1.25, + -1.312 + ] + } + }, + { + "n": "sukun", + "l": 13, + "b": [ + 312.8, + 447.6, + 4.4, + 3 + ], + "ink": [ + -1.25, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.309, + -1.469 + ], + "curve": [ + -1.049, + -1.3 + ], + "mark": [ + -1.25, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 301.9, + 443.5, + 7, + 3.6 + ], + "ink": [ + -0.937, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.289, + -1.45 + ], + "curve": [ + -1.089, + -1.32 + ], + "mark": [ + -0.937, + -1.437 + ] + } + }, + { + "n": "damma", + "l": 13, + "b": [ + 294.8, + 445.8, + 5.3, + 6.6 + ], + "ink": [ + -1.312, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.272, + -1.434 + ], + "curve": [ + -1.119, + -1.335 + ], + "mark": [ + -1.312, + -1.312 + ] + } + }, + { + "n": "maddah", + "l": 13, + "b": [ + 287, + 448.1, + 6.6, + 3.1 + ], + "ink": [ + -1.25, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.257, + -1.42 + ], + "curve": [ + -1.144, + -1.347 + ], + "mark": [ + -1.25, + -1.312 + ] + } + }, + { + "n": "rounded zero", + "l": 13, + "b": [ + 281.6, + 442.5, + 3.2, + 4.1 + ], + "ink": [ + -1.312, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.242, + -1.406 + ], + "curve": [ + -1.166, + -1.357 + ], + "mark": [ + -1.312, + -1.312 + ] + } + }, + { + "n": "hamza", + "l": 13, + "b": [ + 274.8, + 442.5, + 4, + 3.9 + ], + "ink": [ + -1, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.229, + -1.394 + ], + "curve": [ + -1.184, + -1.365 + ], + "mark": [ + -1, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 272.1, + 439.1, + 7, + 3.6 + ], + "ink": [ + -1, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.226, + -1.391 + ], + "curve": [ + -1.187, + -1.366 + ], + "mark": [ + -1, + -1.437 + ] + } + }, + { + "n": "shadda", + "l": 13, + "b": [ + 262.8, + 450.5, + 4.3, + 3.3 + ], + "ink": [ + -1.312, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.204, + -1.37 + ], + "curve": [ + -1.213, + -1.376 + ], + "mark": [ + -1.312, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 259, + 446.1, + 8.5, + 3.9 + ], + "ink": [ + -1.062, + -1.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.2, + -1.367 + ], + "curve": [ + -1.216, + -1.377 + ], + "mark": [ + -1.062, + -1.5 + ] + } + }, + { + "n": "wasla", + "l": 13, + "b": [ + 243.4, + 442.6, + 5.3, + 4.3 + ], + "ink": [ + -0.875, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.164, + -1.333 + ], + "curve": [ + -1.245, + -1.386 + ], + "mark": [ + -0.875, + -1.187 + ] + } + }, + { + "n": "shadda", + "l": 13, + "b": [ + 237.2, + 447.8, + 4.3, + 3.3 + ], + "ink": [ + -0.875, + -1.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.15, + -1.32 + ], + "curve": [ + -1.253, + -1.387 + ], + "mark": [ + -0.875, + -1.375 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 234.4, + 443.4, + 7.8, + 3.7 + ], + "ink": [ + -0.875, + -1.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.148, + -1.318 + ], + "curve": [ + -1.254, + -1.387 + ], + "mark": [ + -0.875, + -1.25 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 227.3, + 448.9, + 7.1, + 3.6 + ], + "ink": [ + -0.875, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.133, + -1.303 + ], + "curve": [ + -1.26, + -1.386 + ], + "mark": [ + -0.875, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 217.7, + 444.9, + 7, + 3.5 + ], + "ink": [ + -0.875, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.112, + -1.284 + ], + "curve": [ + -1.264, + -1.383 + ], + "mark": [ + -0.875, + -1.187 + ] + } + }, + { + "n": "damma", + "l": 13, + "b": [ + 211.4, + 447.8, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.097, + -1.27 + ], + "curve": [ + -1.264, + -1.379 + ], + "mark": [ + -0.875, + -1.187 + ] + } + }, + { + "n": "damma", + "l": 13, + "b": [ + 198.9, + 440.1, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.071, + -1.246 + ], + "curve": [ + -1.259, + -1.368 + ], + "mark": [ + -0.875, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 186.3, + 446.9, + 7, + 3.6 + ], + "ink": [ + -1.625, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.047, + -1.222 + ], + "curve": [ + -1.248, + -1.353 + ], + "mark": [ + -1.625, + -1.312 + ] + } + }, + { + "n": "sukun", + "l": 13, + "b": [ + 180.5, + 447.7, + 4.4, + 3 + ], + "ink": [ + -1.75, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.032, + -1.209 + ], + "curve": [ + -1.238, + -1.343 + ], + "mark": [ + -1.75, + -1.312 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 166, + 449.6, + 8.5, + 3.9 + ], + "ink": [ + -1.75, + -1.375 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.006, + -1.184 + ], + "curve": [ + -1.216, + -1.321 + ], + "mark": [ + -1.75, + -1.375 + ] + } + }, + { + "n": "wasla", + "l": 13, + "b": [ + 152, + 442.2, + 5.3, + 4.3 + ], + "ink": [ + -1.25, + -1.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.973, + -1.153 + ], + "curve": [ + -1.178, + -1.286 + ], + "mark": [ + -1.25, + -1.5 + ] + } + }, + { + "n": "sukun", + "l": 13, + "b": [ + 146.8, + 442.6, + 4.4, + 3 + ], + "ink": [ + -1.312, + -1.437 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.961, + -1.142 + ], + "curve": [ + -1.161, + -1.272 + ], + "mark": [ + -1.312, + -1.437 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 139.7, + 449.7, + 7, + 3.6 + ], + "ink": [ + -1.312, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.949, + -1.131 + ], + "curve": [ + -1.143, + -1.257 + ], + "mark": [ + -1.312, + -1.312 + ] + } + }, + { + "n": "sukun", + "l": 13, + "b": [ + 133.5, + 450.6, + 4.4, + 3 + ], + "ink": [ + -1.437, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.933, + -1.116 + ], + "curve": [ + -1.117, + -1.235 + ], + "mark": [ + -1.437, + -1.312 + ] + } + }, + { + "n": "kasra", + "l": 13, + "b": [ + 128.1, + 461.8, + 6.3, + 3.4 + ], + "ink": [ + -1.5, + -1.25 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.924, + -1.107 + ], + "curve": [ + -1.1, + -1.222 + ], + "mark": [ + -1.5, + -1.25 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 116.8, + 450.5, + 7, + 3.6 + ], + "ink": [ + -1.25, + -1.5 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.901, + -1.086 + ], + "curve": [ + -1.056, + -1.186 + ], + "mark": [ + -1.25, + -1.5 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 108.6, + 444.5, + 7, + 3.6 + ], + "ink": [ + -0.875, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.884, + -1.07 + ], + "curve": [ + -1.019, + -1.157 + ], + "mark": [ + -0.875, + -1.062 + ] + } + }, + { + "n": "sukun", + "l": 13, + "b": [ + 104.5, + 443.7, + 4.4, + 3 + ], + "ink": [ + -0.875, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.873, + -1.059 + ], + "curve": [ + -0.993, + -1.137 + ], + "mark": [ + -0.875, + -1.062 + ] + } + }, + { + "n": "kasra", + "l": 13, + "b": [ + 100.2, + 467.2, + 7.8, + 3.7 + ], + "ink": [ + -1.25, + -1.312 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.867, + -1.054 + ], + "curve": [ + -0.98, + -1.127 + ], + "mark": [ + -1.25, + -1.312 + ] + } + }, + { + "n": "kasra", + "l": 13, + "b": [ + 90.1, + 465, + 6.3, + 3.4 + ], + "ink": [ + -1.062, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.845, + -1.032 + ], + "curve": [ + -0.922, + -1.083 + ], + "mark": [ + -1.062, + -1.187 + ] + } + }, + { + "n": "small yeh", + "l": 13, + "b": [ + 82.7, + 457.1, + 6.4, + 4.7 + ], + "ink": [ + -1, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.829, + -1.018 + ], + "curve": [ + -0.88, + -1.051 + ], + "mark": [ + -1, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 73.1, + 450.7, + 7.8, + 3.7 + ], + "ink": [ + -0.812, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.811, + -1 + ], + "curve": [ + -0.826, + -1.011 + ], + "mark": [ + -0.812, + -0.937 + ] + } + }, + { + "n": "hamza", + "l": 13, + "b": [ + 67.4, + 442.7, + 4, + 3.9 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.795, + -0.986 + ], + "curve": [ + -0.778, + -0.974 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 65.1, + 439.2, + 6.3, + 3.4 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.792, + -0.983 + ], + "curve": [ + -0.77, + -0.969 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 13, + "b": [ + 60.3, + 448.5, + 4.3, + 3.3 + ], + "ink": [ + -0.812, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.78, + -0.972 + ], + "curve": [ + -0.731, + -0.94 + ], + "mark": [ + -0.812, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 58.1, + 445.1, + 7, + 3.5 + ], + "ink": [ + -0.562, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.778, + -0.97 + ], + "curve": [ + -0.725, + -0.935 + ], + "mark": [ + -0.562, + -0.75 + ] + } + }, + { + "n": "damma", + "l": 13, + "b": [ + 51, + 446.2, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.762, + -0.955 + ], + "curve": [ + -0.668, + -0.894 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "small waw", + "l": 13, + "b": [ + 45.2, + 458.8, + 5.5, + 5.7 + ], + "ink": [ + -0.812, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.75, + -0.943 + ], + "curve": [ + -0.626, + -0.863 + ], + "mark": [ + -0.812, + -0.937 + ] + } + }, + { + "n": "maddah", + "l": 13, + "b": [ + 45, + 455.1, + 6.6, + 3.1 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.751, + -0.944 + ], + "curve": [ + -0.628, + -0.865 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "hamza", + "l": 13, + "b": [ + 38.7, + 463.8, + 4, + 3.9 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.735, + -0.929 + ], + "curve": [ + -0.57, + -0.822 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 13, + "b": [ + 38.7, + 467.5, + 4.9, + 2.6 + ], + "ink": [ + -0.375, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.736, + -0.93 + ], + "curve": [ + -0.573, + -0.824 + ], + "mark": [ + -0.375, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 13, + "b": [ + 28.1, + 441.9, + 8.5, + 3.9 + ], + "ink": [ + -0.437, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.717, + -0.913 + ], + "curve": [ + -0.502, + -0.773 + ], + "mark": [ + -0.437, + -0.687 + ] + } + }, + { + "n": "sukun", + "l": 13, + "b": [ + 21.8, + 451.8, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.7, + -0.896 + ], + "curve": [ + -0.432, + -0.722 + ], + "mark": [ + -0.375, + -0.687 + ] + } + }, + { + "n": "kasra", + "l": 13, + "b": [ + 7.1, + 463.8, + 5.6, + 3.1 + ], + "ink": [ + -0.375, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.67, + -0.868 + ], + "curve": [ + -0.306, + -0.631 + ], + "mark": [ + -0.375, + -0.687 + ] + } + }, + { + "n": "damma", + "l": 14, + "b": [ + 323.1, + 474.8, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.834, + -0.919 + ], + "curve": [ + -0.897, + -0.983 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 14, + "b": [ + 319, + 484.8, + 4.4, + 3 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.829, + -0.917 + ], + "curve": [ + -0.887, + -0.975 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 308, + 480.1, + 7.8, + 3.7 + ], + "ink": [ + -0.812, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.818, + -0.914 + ], + "curve": [ + -0.866, + -0.961 + ], + "mark": [ + -0.812, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 14, + "b": [ + 299.6, + 482.1, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.808, + -0.91 + ], + "curve": [ + -0.844, + -0.947 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 275, + 485.4, + 8.5, + 3.9 + ], + "ink": [ + -0.937, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.782, + -0.902 + ], + "curve": [ + -0.797, + -0.917 + ], + "mark": [ + -0.937, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 234, + 484.3, + 7, + 3.6 + ], + "ink": [ + -0.812, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.735, + -0.886 + ], + "curve": [ + -0.721, + -0.872 + ], + "mark": [ + -0.812, + -0.937 + ] + } + }, + { + "n": "wasla", + "l": 14, + "b": [ + 229, + 478.4, + 5.3, + 4.3 + ], + "ink": [ + -0.687, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.729, + -0.884 + ], + "curve": [ + -0.712, + -0.867 + ], + "mark": [ + -0.687, + -0.937 + ] + } + }, + { + "n": "shadda", + "l": 14, + "b": [ + 223, + 482.1, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.722, + -0.882 + ], + "curve": [ + -0.702, + -0.862 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 220.8, + 477.6, + 7.8, + 3.7 + ], + "ink": [ + -0.687, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.721, + -0.881 + ], + "curve": [ + -0.701, + -0.861 + ], + "mark": [ + -0.687, + -0.937 + ] + } + }, + { + "n": "damma", + "l": 14, + "b": [ + 211.7, + 477.6, + 5.3, + 6.7 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.71, + -0.878 + ], + "curve": [ + -0.686, + -0.853 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "rounded zero", + "l": 14, + "b": [ + 201.8, + 477.9, + 3.2, + 4.1 + ], + "ink": [ + -0.75, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.697, + -0.874 + ], + "curve": [ + -0.67, + -0.846 + ], + "mark": [ + -0.75, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 14, + "b": [ + 192.7, + 498.7, + 7.8, + 3.7 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.69, + -0.871 + ], + "curve": [ + -0.661, + -0.842 + ], + "mark": [ + -0.5, + -0.875 + ] + } + }, + { + "n": "sukun", + "l": 14, + "b": [ + 185.3, + 484.1, + 4.4, + 3 + ], + "ink": [ + -0.625, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.68, + -0.868 + ], + "curve": [ + -0.649, + -0.837 + ], + "mark": [ + -0.625, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 173.2, + 483.2, + 7, + 3.6 + ], + "ink": [ + -0.625, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.668, + -0.864 + ], + "curve": [ + -0.636, + -0.832 + ], + "mark": [ + -0.625, + -0.75 + ] + } + }, + { + "n": "successive fathatan", + "l": 14, + "b": [ + 163.9, + 479, + 8.8, + 4.3 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.658, + -0.861 + ], + "curve": [ + -0.627, + -0.829 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 14, + "b": [ + 157.5, + 476.1, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.649, + -0.857 + ], + "curve": [ + -0.618, + -0.826 + ], + "mark": [ + -0.625, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 155.3, + 472.8, + 7.8, + 3.7 + ], + "ink": [ + -0.562, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.648, + -0.857 + ], + "curve": [ + -0.617, + -0.826 + ], + "mark": [ + -0.562, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 14, + "b": [ + 138.4, + 477.7, + 5.3, + 6.6 + ], + "ink": [ + -0.375, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.628, + -0.85 + ], + "curve": [ + -0.6, + -0.822 + ], + "mark": [ + -0.375, + -0.625 + ] + } + }, + { + "n": "kasra", + "l": 14, + "b": [ + 134.6, + 500.1, + 7.8, + 3.7 + ], + "ink": [ + -0.625, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.625, + -0.849 + ], + "curve": [ + -0.598, + -0.822 + ], + "mark": [ + -0.625, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 112.8, + 482.3, + 7.8, + 3.7 + ], + "ink": [ + -0.562, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.601, + -0.841 + ], + "curve": [ + -0.581, + -0.822 + ], + "mark": [ + -0.562, + -0.875 + ] + } + }, + { + "n": "shadda", + "l": 14, + "b": [ + 106.1, + 483.2, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.591, + -0.838 + ], + "curve": [ + -0.575, + -0.822 + ], + "mark": [ + -0.625, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 103.4, + 478.8, + 8.5, + 3.9 + ], + "ink": [ + -0.562, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.591, + -0.838 + ], + "curve": [ + -0.575, + -0.822 + ], + "mark": [ + -0.562, + -0.687 + ] + } + }, + { + "n": "wasla", + "l": 14, + "b": [ + 94.7, + 478.1, + 5.3, + 4.3 + ], + "ink": [ + -0.875, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.579, + -0.834 + ], + "curve": [ + -0.569, + -0.824 + ], + "mark": [ + -0.875, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 14, + "b": [ + 89.5, + 477.6, + 4.3, + 3.3 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.573, + -0.832 + ], + "curve": [ + -0.566, + -0.826 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 86.6, + 474.2, + 7.8, + 3.7 + ], + "ink": [ + -0.937, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.571, + -0.832 + ], + "curve": [ + -0.566, + -0.826 + ], + "mark": [ + -0.937, + -0.937 + ] + } + }, + { + "n": "kasra", + "l": 14, + "b": [ + 84, + 501.2, + 6.3, + 3.4 + ], + "ink": [ + -0.187, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.568, + -0.83 + ], + "curve": [ + -0.564, + -0.827 + ], + "mark": [ + -0.187, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 61, + 486.6, + 8.5, + 3.9 + ], + "ink": [ + -0.375, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.543, + -0.822 + ], + "curve": [ + -0.556, + -0.835 + ], + "mark": [ + -0.375, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 43.6, + 480.4, + 7, + 3.6 + ], + "ink": [ + -0.562, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.523, + -0.816 + ], + "curve": [ + -0.553, + -0.845 + ], + "mark": [ + -0.562, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 14, + "b": [ + 27.3, + 478.1, + 7, + 3.6 + ], + "ink": [ + -0.437, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.505, + -0.81 + ], + "curve": [ + -0.551, + -0.856 + ], + "mark": [ + -0.437, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 14, + "b": [ + 19, + 482, + 5.3, + 6.6 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.495, + -0.806 + ], + "curve": [ + -0.552, + -0.863 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "rounded zero", + "l": 14, + "b": [ + 7.6, + 478.5, + 3.2, + 4.1 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.481, + -0.802 + ], + "curve": [ + -0.553, + -0.874 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 15, + "b": [ + 326.2, + 534.6, + 5.6, + 3.1 + ], + "ink": [ + -0.437, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.63, + -0.87 + ], + "curve": [ + -0.155, + -0.855 + ], + "mark": [ + -0.437, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 15, + "b": [ + 287.8, + 516.1, + 5.3, + 6.6 + ], + "ink": [ + -0.312, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.622, + -0.86 + ], + "curve": [ + -0.419, + -0.853 + ], + "mark": [ + -0.312, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 15, + "b": [ + 280.8, + 519.9, + 4.4, + 3 + ], + "ink": [ + -0.437, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.621, + -0.858 + ], + "curve": [ + -0.461, + -0.853 + ], + "mark": [ + -0.437, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 262.4, + 517.4, + 8.5, + 3.9 + ], + "ink": [ + -0.5, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.617, + -0.854 + ], + "curve": [ + -0.543, + -0.851 + ], + "mark": [ + -0.5, + -0.812 + ] + } + }, + { + "n": "maddah", + "l": 15, + "b": [ + 257.2, + 515, + 6.6, + 3.1 + ], + "ink": [ + -0.312, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.616, + -0.852 + ], + "curve": [ + -0.571, + -0.851 + ], + "mark": [ + -0.312, + -0.812 + ] + } + }, + { + "n": "shadda", + "l": 15, + "b": [ + 248.7, + 521.7, + 4.3, + 3.3 + ], + "ink": [ + -0.625, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.614, + -0.85 + ], + "curve": [ + -0.61, + -0.849 + ], + "mark": [ + -0.625, + -0.937 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 245.3, + 517.4, + 8.5, + 3.9 + ], + "ink": [ + -0.5, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.614, + -0.849 + ], + "curve": [ + -0.615, + -0.849 + ], + "mark": [ + -0.5, + -0.937 + ] + } + }, + { + "n": "successive fathatan", + "l": 15, + "b": [ + 230.3, + 515.7, + 9.5, + 4.4 + ], + "ink": [ + -0.375, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.611, + -0.845 + ], + "curve": [ + -0.664, + -0.847 + ], + "mark": [ + -0.375, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 220.8, + 519.5, + 7, + 3.6 + ], + "ink": [ + -0.375, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.609, + -0.843 + ], + "curve": [ + -0.694, + -0.845 + ], + "mark": [ + -0.375, + -0.75 + ] + } + }, + { + "n": "wasla", + "l": 15, + "b": [ + 214.7, + 512.5, + 5.3, + 4.3 + ], + "ink": [ + -0.875, + -0.937 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.608, + -0.841 + ], + "curve": [ + -0.709, + -0.844 + ], + "mark": [ + -0.875, + -0.937 + ] + } + }, + { + "n": "sukun", + "l": 15, + "b": [ + 208.1, + 518, + 4.4, + 3 + ], + "ink": [ + -0.75, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.606, + -0.839 + ], + "curve": [ + -0.723, + -0.843 + ], + "mark": [ + -0.75, + -1.187 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 194, + 515.1, + 7, + 3.6 + ], + "ink": [ + -1, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.604, + -0.835 + ], + "curve": [ + -0.741, + -0.84 + ], + "mark": [ + -1, + -0.812 + ] + } + }, + { + "n": "damma", + "l": 15, + "b": [ + 187.2, + 516.3, + 5.3, + 6.6 + ], + "ink": [ + -0.875, + -1.062 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.602, + -0.833 + ], + "curve": [ + -0.748, + -0.838 + ], + "mark": [ + -0.875, + -1.062 + ] + } + }, + { + "n": "maddah", + "l": 15, + "b": [ + 180.5, + 518.2, + 6.6, + 3.1 + ], + "ink": [ + -0.75, + -1.187 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.601, + -0.832 + ], + "curve": [ + -0.751, + -0.837 + ], + "mark": [ + -0.75, + -1.187 + ] + } + }, + { + "n": "rounded zero", + "l": 15, + "b": [ + 175.3, + 512.4, + 3.2, + 4.1 + ], + "ink": [ + -0.875, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.6, + -0.83 + ], + "curve": [ + -0.752, + -0.835 + ], + "mark": [ + -0.875, + -0.875 + ] + } + }, + { + "n": "hamza", + "l": 15, + "b": [ + 168.8, + 514, + 4, + 3.9 + ], + "ink": [ + -0.875, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.599, + -0.828 + ], + "curve": [ + -0.751, + -0.833 + ], + "mark": [ + -0.875, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 165.9, + 510.1, + 7, + 3.6 + ], + "ink": [ + -1, + -0.625 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.598, + -0.828 + ], + "curve": [ + -0.751, + -0.833 + ], + "mark": [ + -1, + -0.625 + ] + } + }, + { + "n": "shadda", + "l": 15, + "b": [ + 155.2, + 520.3, + 4.3, + 3.3 + ], + "ink": [ + -0.875, + -0.75 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.596, + -0.825 + ], + "curve": [ + -0.743, + -0.83 + ], + "mark": [ + -0.875, + -0.75 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 152.7, + 515.7, + 8.5, + 3.9 + ], + "ink": [ + -0.875, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.596, + -0.825 + ], + "curve": [ + -0.742, + -0.83 + ], + "mark": [ + -0.875, + -0.687 + ] + } + }, + { + "n": "wasla", + "l": 15, + "b": [ + 137.1, + 513.4, + 5.3, + 4.3 + ], + "ink": [ + -0.625, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.593, + -0.82 + ], + "curve": [ + -0.717, + -0.824 + ], + "mark": [ + -0.625, + -0.687 + ] + } + }, + { + "n": "shadda", + "l": 15, + "b": [ + 130.4, + 518.8, + 4.3, + 3.3 + ], + "ink": [ + -0.75, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.591, + -0.818 + ], + "curve": [ + -0.702, + -0.822 + ], + "mark": [ + -0.75, + -0.875 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 128.2, + 514, + 7.8, + 3.7 + ], + "ink": [ + -0.687, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.591, + -0.818 + ], + "curve": [ + -0.701, + -0.822 + ], + "mark": [ + -0.687, + -0.687 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 121.3, + 520.3, + 7, + 3.6 + ], + "ink": [ + -0.75, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.59, + -0.816 + ], + "curve": [ + -0.683, + -0.819 + ], + "mark": [ + -0.75, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 106, + 517.4, + 8.5, + 3.9 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.587, + -0.813 + ], + "curve": [ + -0.639, + -0.814 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 15, + "b": [ + 95.8, + 535.9, + 6.3, + 3.4 + ], + "ink": [ + -0.562, + -0.687 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.585, + -0.81 + ], + "curve": [ + -0.597, + -0.81 + ], + "mark": [ + -0.562, + -0.687 + ] + } + }, + { + "n": "damma", + "l": 15, + "b": [ + 77.2, + 517.3, + 5.3, + 6.6 + ], + "ink": [ + -0.312, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.581, + -0.805 + ], + "curve": [ + -0.512, + -0.802 + ], + "mark": [ + -0.312, + -0.812 + ] + } + }, + { + "n": "wasla", + "l": 15, + "b": [ + 67.1, + 512.7, + 5.3, + 4.3 + ], + "ink": [ + -0.312, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.579, + -0.802 + ], + "curve": [ + -0.459, + -0.798 + ], + "mark": [ + -0.312, + -0.812 + ] + } + }, + { + "n": "sukun", + "l": 15, + "b": [ + 61.5, + 513.3, + 4.4, + 3 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.578, + -0.8 + ], + "curve": [ + -0.425, + -0.795 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 15, + "b": [ + 56.6, + 535.2, + 7.8, + 3.7 + ], + "ink": [ + -0.375, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.577, + -0.799 + ], + "curve": [ + -0.406, + -0.794 + ], + "mark": [ + -0.375, + -0.812 + ] + } + }, + { + "n": "fatha", + "l": 15, + "b": [ + 45.2, + 516.8, + 8.5, + 3.9 + ], + "ink": [ + -0.312, + -0.812 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.575, + -0.797 + ], + "curve": [ + -0.337, + -0.789 + ], + "mark": [ + -0.312, + -0.812 + ] + } + }, + { + "n": "kasra", + "l": 15, + "b": [ + 31.9, + 538.4, + 7, + 3.5 + ], + "ink": [ + -0.5, + -0.875 + ], + "t": 1, + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.572, + -0.793 + ], + "curve": [ + -0.24, + -0.782 + ], + "mark": [ + -0.5, + -0.875 + ] + } + } + ], + "words": [ + { + "l": 1, + "b": [ + 309.3, + 15.1, + 22.5, + 22.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.874, + -0.929 + ], + "curve": [ + -0.775, + -0.939 + ], + "mark": [ + -0.874, + -0.929 + ] + } + }, + { + "l": 1, + "b": [ + 255.6, + 12.3, + 53.8, + 33.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.812, + -0.903 + ], + "curve": [ + -0.787, + -0.905 + ], + "mark": [ + -0.812, + -0.903 + ] + } + }, + { + "l": 1, + "b": [ + 241.5, + 20.8, + 10, + 17.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.752, + -0.878 + ], + "curve": [ + -0.778, + -0.876 + ], + "mark": [ + -0.752, + -0.878 + ] + } + }, + { + "l": 1, + "b": [ + 202.1, + 14.2, + 42, + 24.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.714, + -0.862 + ], + "curve": [ + -0.761, + -0.858 + ], + "mark": [ + -0.714, + -0.862 + ] + } + }, + { + "l": 1, + "b": [ + 179.8, + 12.1, + 19.3, + 21.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.658, + -0.84 + ], + "curve": [ + -0.723, + -0.833 + ], + "mark": [ + -0.658, + -0.84 + ] + } + }, + { + "l": 1, + "b": [ + 128.7, + 14.8, + 47.4, + 21.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.597, + -0.814 + ], + "curve": [ + -0.66, + -0.808 + ], + "mark": [ + -0.597, + -0.814 + ] + } + }, + { + "l": 1, + "b": [ + 133, + 11.3, + 5.3, + 6.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.569, + -0.803 + ], + "curve": [ + -0.625, + -0.797 + ], + "mark": [ + -0.569, + -0.803 + ] + } + }, + { + "l": 1, + "b": [ + 116.9, + 20.4, + 10.8, + 18.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.547, + -0.794 + ], + "curve": [ + -0.593, + -0.789 + ], + "mark": [ + -0.547, + -0.794 + ] + } + }, + { + "l": 1, + "b": [ + 106.3, + 17.8, + 11.8, + 16.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.531, + -0.787 + ], + "curve": [ + -0.568, + -0.783 + ], + "mark": [ + -0.531, + -0.787 + ] + } + }, + { + "l": 1, + "b": [ + 68.1, + 17.8, + 35.1, + 19.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.487, + -0.769 + ], + "curve": [ + -0.493, + -0.768 + ], + "mark": [ + -0.487, + -0.769 + ] + } + }, + { + "l": 1, + "b": [ + 52.7, + 15.3, + 13.5, + 25.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.444, + -0.751 + ], + "curve": [ + -0.409, + -0.755 + ], + "mark": [ + -0.444, + -0.751 + ] + } + }, + { + "l": 1, + "b": [ + 7.1, + 17.4, + 42.3, + 20.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.687, + -0.812 + ], + "tilt": [ + -0.392, + -0.73 + ], + "curve": [ + -0.295, + -0.74 + ], + "mark": [ + -0.392, + -0.73 + ] + } + }, + { + "l": 2, + "b": [ + 321.4, + 55.7, + 10.4, + 18.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.041, + -1.439 + ], + "curve": [ + -1.3, + -1.008 + ], + "mark": [ + -1.041, + -1.439 + ] + } + }, + { + "l": 2, + "b": [ + 277.4, + 48, + 45.8, + 25.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.029, + -1.428 + ], + "curve": [ + -1.166, + -1.202 + ], + "mark": [ + -1.029, + -1.428 + ] + } + }, + { + "l": 2, + "b": [ + 254.1, + 47.6, + 19.7, + 21.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.013, + -1.414 + ], + "curve": [ + -1.018, + -1.405 + ], + "mark": [ + -1.013, + -1.414 + ] + } + }, + { + "l": 2, + "b": [ + 231.9, + 53.5, + 20.8, + 19 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -1.004, + -1.406 + ], + "curve": [ + -0.952, + -1.492 + ], + "mark": [ + -1.004, + -1.406 + ] + } + }, + { + "l": 2, + "b": [ + 217.7, + 55.2, + 10, + 18.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.995, + -1.398 + ], + "curve": [ + -0.905, + -1.547 + ], + "mark": [ + -0.995, + -1.398 + ] + } + }, + { + "l": 2, + "b": [ + 184.7, + 52.4, + 37.4, + 26 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.987, + -1.39 + ], + "curve": [ + -0.872, + -1.58 + ], + "mark": [ + -0.987, + -1.39 + ] + } + }, + { + "l": 2, + "b": [ + 125.6, + 47.6, + 56.2, + 26.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.965, + -1.371 + ], + "curve": [ + -0.845, + -1.57 + ], + "mark": [ + -0.965, + -1.371 + ] + } + }, + { + "l": 2, + "b": [ + 97.4, + 51.7, + 25.5, + 21.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.946, + -1.354 + ], + "curve": [ + -0.889, + -1.447 + ], + "mark": [ + -0.946, + -1.354 + ] + } + }, + { + "l": 2, + "b": [ + 56, + 50.2, + 36.6, + 22.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.93, + -1.339 + ], + "curve": [ + -0.974, + -1.266 + ], + "mark": [ + -0.93, + -1.339 + ] + } + }, + { + "l": 2, + "b": [ + 7.1, + 52.9, + 46, + 16.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.911, + -1.322 + ], + "curve": [ + -1.139, + -0.944 + ], + "mark": [ + -0.911, + -1.322 + ] + } + }, + { + "l": 2, + "b": [ + 8, + 44.3, + 5.3, + 6.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.562 + ], + "tilt": [ + -0.902, + -1.314 + ], + "curve": [ + -1.232, + -0.768 + ], + "mark": [ + -0.902, + -1.314 + ] + } + }, + { + "l": 3, + "b": [ + 315.4, + 85.1, + 16.5, + 26.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.798, + -0.934 + ], + "curve": [ + -0.774, + -0.918 + ], + "mark": [ + -0.798, + -0.934 + ] + } + }, + { + "l": 3, + "b": [ + 291.2, + 82.3, + 20.6, + 22.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.766, + -0.935 + ], + "curve": [ + -0.756, + -0.929 + ], + "mark": [ + -0.766, + -0.935 + ] + } + }, + { + "l": 3, + "b": [ + 245, + 87.5, + 43.9, + 29.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.716, + -0.938 + ], + "curve": [ + -0.723, + -0.943 + ], + "mark": [ + -0.716, + -0.938 + ] + } + }, + { + "l": 3, + "b": [ + 204.6, + 89.2, + 38, + 23.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.652, + -0.941 + ], + "curve": [ + -0.674, + -0.955 + ], + "mark": [ + -0.652, + -0.941 + ] + } + }, + { + "l": 3, + "b": [ + 128.9, + 86.4, + 54.2, + 23.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.554, + -0.946 + ], + "curve": [ + -0.578, + -0.962 + ], + "mark": [ + -0.554, + -0.946 + ] + } + }, + { + "l": 3, + "b": [ + 118, + 90.8, + 10.1, + 17.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.506, + -0.949 + ], + "curve": [ + -0.522, + -0.96 + ], + "mark": [ + -0.506, + -0.949 + ] + } + }, + { + "l": 3, + "b": [ + 105.1, + 80.5, + 14.6, + 26 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.49, + -0.95 + ], + "curve": [ + -0.503, + -0.958 + ], + "mark": [ + -0.49, + -0.95 + ] + } + }, + { + "l": 3, + "b": [ + 80.4, + 83.3, + 20.6, + 21.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.458, + -0.951 + ], + "curve": [ + -0.463, + -0.954 + ], + "mark": [ + -0.458, + -0.951 + ] + } + }, + { + "l": 3, + "b": [ + 42.1, + 88.2, + 35.3, + 21.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.413, + -0.954 + ], + "curve": [ + -0.401, + -0.946 + ], + "mark": [ + -0.413, + -0.954 + ] + } + }, + { + "l": 3, + "b": [ + 7.1, + 81.9, + 36.8, + 28 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.363, + -0.956 + ], + "curve": [ + -0.327, + -0.933 + ], + "mark": [ + -0.363, + -0.956 + ] + } + }, + { + "l": 4, + "b": [ + 265.6, + 120, + 66.2, + 30.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.544, + -1.156 + ], + "curve": [ + -0.672, + -1.296 + ], + "mark": [ + -0.544, + -1.156 + ] + } + }, + { + "l": 4, + "b": [ + 227.1, + 125.2, + 16.6, + 23.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.751, + -1.257 + ], + "curve": [ + -0.664, + -1.163 + ], + "mark": [ + -0.751, + -1.257 + ] + } + }, + { + "l": 4, + "b": [ + 156.9, + 119.9, + 66.9, + 26.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.897, + -1.329 + ], + "curve": [ + -0.745, + -1.164 + ], + "mark": [ + -0.897, + -1.329 + ] + } + }, + { + "l": 4, + "b": [ + 133, + 122.4, + 24, + 18.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.045, + -1.402 + ], + "curve": [ + -0.901, + -1.244 + ], + "mark": [ + -1.045, + -1.402 + ] + } + }, + { + "l": 4, + "b": [ + 70.2, + 121.5, + 59.8, + 22.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.191, + -1.474 + ], + "curve": [ + -1.127, + -1.403 + ], + "mark": [ + -1.191, + -1.474 + ] + } + }, + { + "l": 4, + "b": [ + 33.2, + 119.4, + 37.9, + 32.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.348, + -1.55 + ], + "curve": [ + -1.448, + -1.66 + ], + "mark": [ + -1.348, + -1.55 + ] + } + }, + { + "l": 4, + "b": [ + 34.2, + 113, + 8.8, + 6.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.392, + -1.572 + ], + "curve": [ + -1.553, + -1.748 + ], + "mark": [ + -1.392, + -1.572 + ] + } + }, + { + "l": 4, + "b": [ + 19.3, + 126.4, + 10.2, + 18.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.438, + -1.595 + ], + "curve": [ + -1.671, + -1.849 + ], + "mark": [ + -1.438, + -1.595 + ] + } + }, + { + "l": 4, + "b": [ + 7.1, + 123.6, + 16.2, + 19.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -1.468, + -1.609 + ], + "curve": [ + -1.751, + -1.918 + ], + "mark": [ + -1.468, + -1.609 + ] + } + }, + { + "l": 5, + "b": [ + 293.3, + 157.4, + 38.6, + 24.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -2.128, + -2.826 + ], + "curve": [ + -2.076, + -2.712 + ], + "mark": [ + -2.128, + -2.826 + ] + } + }, + { + "l": 5, + "b": [ + 266, + 157.3, + 25.6, + 23.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.945, + -2.694 + ], + "curve": [ + -1.934, + -2.67 + ], + "mark": [ + -1.945, + -2.694 + ] + } + }, + { + "l": 5, + "b": [ + 239.5, + 160.4, + 30, + 21.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.814, + -2.6 + ], + "curve": [ + -1.825, + -2.625 + ], + "mark": [ + -1.814, + -2.6 + ] + } + }, + { + "l": 5, + "b": [ + 197.8, + 152.2, + 42.2, + 28.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.622, + -2.461 + ], + "curve": [ + -1.656, + -2.537 + ], + "mark": [ + -1.622, + -2.461 + ] + } + }, + { + "l": 5, + "b": [ + 200.9, + 152.1, + 8.8, + 6.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.548, + -2.409 + ], + "curve": [ + -1.588, + -2.496 + ], + "mark": [ + -1.548, + -2.409 + ] + } + }, + { + "l": 5, + "b": [ + 186.7, + 161.3, + 10, + 19.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.475, + -2.356 + ], + "curve": [ + -1.518, + -2.451 + ], + "mark": [ + -1.475, + -2.356 + ] + } + }, + { + "l": 5, + "b": [ + 174.5, + 158.2, + 17, + 20.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.428, + -2.322 + ], + "curve": [ + -1.472, + -2.42 + ], + "mark": [ + -1.428, + -2.322 + ] + } + }, + { + "l": 5, + "b": [ + 128.4, + 156.6, + 42.6, + 25.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.248, + -2.192 + ], + "curve": [ + -1.291, + -2.287 + ], + "mark": [ + -1.248, + -2.192 + ] + } + }, + { + "l": 5, + "b": [ + 100.6, + 159.3, + 26, + 17.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -1.053, + -2.052 + ], + "curve": [ + -1.082, + -2.116 + ], + "mark": [ + -1.053, + -2.052 + ] + } + }, + { + "l": 5, + "b": [ + 87.4, + 162.9, + 10.6, + 18.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.94, + -1.971 + ], + "curve": [ + -0.955, + -2.004 + ], + "mark": [ + -0.94, + -1.971 + ] + } + }, + { + "l": 5, + "b": [ + 76.7, + 156.9, + 13.8, + 25.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.891, + -1.935 + ], + "curve": [ + -0.899, + -1.953 + ], + "mark": [ + -0.891, + -1.935 + ] + } + }, + { + "l": 5, + "b": [ + 47.9, + 157.2, + 25.7, + 28.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.768, + -1.847 + ], + "curve": [ + -0.754, + -1.816 + ], + "mark": [ + -0.768, + -1.847 + ] + } + }, + { + "l": 5, + "b": [ + 7.1, + 153.8, + 38.3, + 28.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.437, + -2.25 + ], + "tilt": [ + -0.582, + -1.713 + ], + "curve": [ + -0.525, + -1.588 + ], + "mark": [ + -0.582, + -1.713 + ] + } + }, + { + "l": 6, + "b": [ + 290.5, + 190.3, + 41.4, + 28.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.807, + -1.621 + ], + "curve": [ + -0.711, + -1.608 + ], + "mark": [ + -0.807, + -1.621 + ] + } + }, + { + "l": 6, + "b": [ + 257.7, + 195.2, + 31.8, + 21.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.829, + -1.533 + ], + "curve": [ + -0.834, + -1.533 + ], + "mark": [ + -0.829, + -1.533 + ] + } + }, + { + "l": 6, + "b": [ + 243.2, + 199, + 10.3, + 18.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.845, + -1.473 + ], + "curve": [ + -0.897, + -1.48 + ], + "mark": [ + -0.845, + -1.473 + ] + } + }, + { + "l": 6, + "b": [ + 231.1, + 192.3, + 14.2, + 24.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.851, + -1.449 + ], + "curve": [ + -0.918, + -1.458 + ], + "mark": [ + -0.851, + -1.449 + ] + } + }, + { + "l": 6, + "b": [ + 181.8, + 189.8, + 51.4, + 26.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.87, + -1.377 + ], + "curve": [ + -0.965, + -1.39 + ], + "mark": [ + -0.87, + -1.377 + ] + } + }, + { + "l": 6, + "b": [ + 167.1, + 199.1, + 10.3, + 17.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.891, + -1.295 + ], + "curve": [ + -0.989, + -1.308 + ], + "mark": [ + -0.891, + -1.295 + ] + } + }, + { + "l": 6, + "b": [ + 137.5, + 188.4, + 30, + 26.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.903, + -1.248 + ], + "curve": [ + -0.989, + -1.26 + ], + "mark": [ + -0.903, + -1.248 + ] + } + }, + { + "l": 6, + "b": [ + 113, + 191.8, + 21.2, + 21.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.92, + -1.181 + ], + "curve": [ + -0.971, + -1.187 + ], + "mark": [ + -0.92, + -1.181 + ] + } + }, + { + "l": 6, + "b": [ + 94.3, + 197.6, + 15.7, + 26.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.933, + -1.13 + ], + "curve": [ + -0.944, + -1.132 + ], + "mark": [ + -0.933, + -1.13 + ] + } + }, + { + "l": 6, + "b": [ + 27.7, + 191.1, + 63.8, + 26.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.959, + -1.03 + ], + "curve": [ + -0.855, + -1.017 + ], + "mark": [ + -0.959, + -1.03 + ] + } + }, + { + "l": 7, + "b": [ + 295, + 222.5, + 36.8, + 28.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.244, + -1.26 + ], + "curve": [ + -1.479, + -1.29 + ], + "mark": [ + -1.244, + -1.26 + ] + } + }, + { + "l": 7, + "b": [ + 247.3, + 220.8, + 44.6, + 32.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -1.116, + -1.183 + ], + "curve": [ + -1.081, + -1.178 + ], + "mark": [ + -1.116, + -1.183 + ] + } + }, + { + "l": 7, + "b": [ + 208.2, + 226.6, + 36.2, + 26.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.989, + -1.106 + ], + "curve": [ + -0.788, + -1.081 + ], + "mark": [ + -0.989, + -1.106 + ] + } + }, + { + "l": 7, + "b": [ + 164, + 223.1, + 40.8, + 30.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.866, + -1.031 + ], + "curve": [ + -0.601, + -0.998 + ], + "mark": [ + -0.866, + -1.031 + ] + } + }, + { + "l": 7, + "b": [ + 141.6, + 226.1, + 20.5, + 22.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.77, + -0.973 + ], + "curve": [ + -0.519, + -0.942 + ], + "mark": [ + -0.77, + -0.973 + ] + } + }, + { + "l": 7, + "b": [ + 128.8, + 234.2, + 11.6, + 18.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.72, + -0.943 + ], + "curve": [ + -0.499, + -0.915 + ], + "mark": [ + -0.72, + -0.943 + ] + } + }, + { + "l": 7, + "b": [ + 86.3, + 227.4, + 43.5, + 25.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.642, + -0.896 + ], + "curve": [ + -0.5, + -0.878 + ], + "mark": [ + -0.642, + -0.896 + ] + } + }, + { + "l": 7, + "b": [ + 74.9, + 234.5, + 11, + 17.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.561, + -0.846 + ], + "curve": [ + -0.54, + -0.844 + ], + "mark": [ + -0.561, + -0.846 + ] + } + }, + { + "l": 7, + "b": [ + 65.5, + 225.9, + 12, + 22.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.534, + -0.831 + ], + "curve": [ + -0.562, + -0.834 + ], + "mark": [ + -0.534, + -0.831 + ] + } + }, + { + "l": 7, + "b": [ + 38, + 222.3, + 25.5, + 31.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.474, + -0.794 + ], + "curve": [ + -0.628, + -0.813 + ], + "mark": [ + -0.474, + -0.794 + ] + } + }, + { + "l": 7, + "b": [ + 7.1, + 229.6, + 28.6, + 18.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.75, + -1.062 + ], + "tilt": [ + -0.388, + -0.742 + ], + "curve": [ + -0.762, + -0.788 + ], + "mark": [ + -0.388, + -0.742 + ] + } + }, + { + "l": 8, + "b": [ + 321.5, + 271.7, + 10.4, + 17.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.557, + -0.915 + ], + "curve": [ + -0.708, + -0.703 + ], + "mark": [ + -0.557, + -0.915 + ] + } + }, + { + "l": 8, + "b": [ + 295.2, + 259.4, + 29, + 28.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.579, + -0.903 + ], + "curve": [ + -0.673, + -0.771 + ], + "mark": [ + -0.579, + -0.903 + ] + } + }, + { + "l": 8, + "b": [ + 241.5, + 264.7, + 54.2, + 25.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.634, + -0.873 + ], + "curve": [ + -0.617, + -0.896 + ], + "mark": [ + -0.634, + -0.873 + ] + } + }, + { + "l": 8, + "b": [ + 209, + 271.1, + 10.2, + 17.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.706, + -0.832 + ], + "curve": [ + -0.604, + -0.976 + ], + "mark": [ + -0.706, + -0.832 + ] + } + }, + { + "l": 8, + "b": [ + 199.4, + 262.4, + 12.6, + 22.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.717, + -0.826 + ], + "curve": [ + -0.608, + -0.979 + ], + "mark": [ + -0.717, + -0.826 + ] + } + }, + { + "l": 8, + "b": [ + 159.2, + 261.5, + 39.6, + 28.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.752, + -0.806 + ], + "curve": [ + -0.632, + -0.975 + ], + "mark": [ + -0.752, + -0.806 + ] + } + }, + { + "l": 8, + "b": [ + 101, + 260.4, + 58.9, + 30.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.816, + -0.771 + ], + "curve": [ + -0.719, + -0.908 + ], + "mark": [ + -0.816, + -0.771 + ] + } + }, + { + "l": 8, + "b": [ + 72.8, + 261.3, + 24.5, + 27.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.877, + -0.737 + ], + "curve": [ + -0.85, + -0.775 + ], + "mark": [ + -0.877, + -0.737 + ] + } + }, + { + "l": 8, + "b": [ + 30.9, + 267.6, + 41.9, + 21.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.92, + -0.713 + ], + "curve": [ + -0.976, + -0.634 + ], + "mark": [ + -0.92, + -0.713 + ] + } + }, + { + "l": 8, + "b": [ + 18.7, + 270.2, + 10.1, + 18.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.958, + -0.692 + ], + "curve": [ + -1.104, + -0.487 + ], + "mark": [ + -0.958, + -0.692 + ] + } + }, + { + "l": 8, + "b": [ + 7.1, + 265.5, + 14.8, + 24.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.97, + -0.685 + ], + "curve": [ + -1.15, + -0.433 + ], + "mark": [ + -0.97, + -0.685 + ] + } + }, + { + "l": 9, + "b": [ + 319.2, + 298.3, + 12.6, + 21.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -1.113, + -0.358 + ], + "curve": [ + -1.324, + -0.317 + ], + "mark": [ + -1.113, + -0.358 + ] + } + }, + { + "l": 9, + "b": [ + 268.7, + 302.6, + 50.8, + 23.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -1.029, + -0.418 + ], + "curve": [ + -1.127, + -0.399 + ], + "mark": [ + -1.029, + -0.418 + ] + } + }, + { + "l": 9, + "b": [ + 239.2, + 311.3, + 7.4, + 7.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.892, + -0.516 + ], + "curve": [ + -0.86, + -0.522 + ], + "mark": [ + -0.892, + -0.516 + ] + } + }, + { + "l": 9, + "b": [ + 222.7, + 301.7, + 17.2, + 24.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.861, + -0.538 + ], + "curve": [ + -0.809, + -0.548 + ], + "mark": [ + -0.861, + -0.538 + ] + } + }, + { + "l": 9, + "b": [ + 193.3, + 301.5, + 25.6, + 22.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.793, + -0.586 + ], + "curve": [ + -0.71, + -0.602 + ], + "mark": [ + -0.793, + -0.586 + ] + } + }, + { + "l": 9, + "b": [ + 151.2, + 299.3, + 44.1, + 28.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.705, + -0.649 + ], + "curve": [ + -0.606, + -0.668 + ], + "mark": [ + -0.705, + -0.649 + ] + } + }, + { + "l": 9, + "b": [ + 122, + 303.9, + 25.7, + 19.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.602, + -0.722 + ], + "curve": [ + -0.519, + -0.738 + ], + "mark": [ + -0.602, + -0.722 + ] + } + }, + { + "l": 9, + "b": [ + 97.7, + 298.5, + 20.3, + 25.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.53, + -0.774 + ], + "curve": [ + -0.481, + -0.783 + ], + "mark": [ + -0.53, + -0.774 + ] + } + }, + { + "l": 9, + "b": [ + 58.7, + 298.9, + 37.5, + 26 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.449, + -0.832 + ], + "curve": [ + -0.461, + -0.829 + ], + "mark": [ + -0.449, + -0.832 + ] + } + }, + { + "l": 9, + "b": [ + 7.1, + 299.5, + 52.4, + 24.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.331, + -0.916 + ], + "curve": [ + -0.475, + -0.888 + ], + "mark": [ + -0.331, + -0.916 + ] + } + }, + { + "l": 10, + "b": [ + 300.1, + 330.7, + 31.7, + 30.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.741, + -0.879 + ], + "curve": [ + -0.628, + -0.817 + ], + "mark": [ + -0.741, + -0.879 + ] + } + }, + { + "l": 10, + "b": [ + 284.3, + 332.7, + 14.4, + 23 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.718, + -0.885 + ], + "curve": [ + -0.677, + -0.863 + ], + "mark": [ + -0.718, + -0.885 + ] + } + }, + { + "l": 10, + "b": [ + 225.6, + 333.9, + 58.9, + 26.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.685, + -0.894 + ], + "curve": [ + -0.725, + -0.916 + ], + "mark": [ + -0.685, + -0.894 + ] + } + }, + { + "l": 10, + "b": [ + 190.6, + 341.9, + 9.9, + 18.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.63, + -0.909 + ], + "curve": [ + -0.738, + -0.968 + ], + "mark": [ + -0.63, + -0.909 + ] + } + }, + { + "l": 10, + "b": [ + 178.9, + 334.5, + 14, + 26.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.622, + -0.911 + ], + "curve": [ + -0.733, + -0.972 + ], + "mark": [ + -0.622, + -0.911 + ] + } + }, + { + "l": 10, + "b": [ + 158.5, + 340.2, + 22.5, + 21.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.607, + -0.915 + ], + "curve": [ + -0.719, + -0.977 + ], + "mark": [ + -0.607, + -0.915 + ] + } + }, + { + "l": 10, + "b": [ + 137.4, + 334, + 20, + 21.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.586, + -0.921 + ], + "curve": [ + -0.69, + -0.978 + ], + "mark": [ + -0.586, + -0.921 + ] + } + }, + { + "l": 10, + "b": [ + 99, + 340.4, + 37.1, + 23.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.559, + -0.928 + ], + "curve": [ + -0.634, + -0.969 + ], + "mark": [ + -0.559, + -0.928 + ] + } + }, + { + "l": 10, + "b": [ + 71.8, + 337.8, + 28.8, + 22.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.53, + -0.936 + ], + "curve": [ + -0.553, + -0.948 + ], + "mark": [ + -0.53, + -0.936 + ] + } + }, + { + "l": 10, + "b": [ + 7.1, + 329.1, + 62.2, + 31.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.486, + -0.947 + ], + "curve": [ + -0.386, + -0.893 + ], + "mark": [ + -0.486, + -0.947 + ] + } + }, + { + "l": 10, + "b": [ + 9.4, + 328.9, + 8.8, + 6.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.464, + -0.953 + ], + "curve": [ + -0.281, + -0.853 + ], + "mark": [ + -0.464, + -0.953 + ] + } + }, + { + "l": 11, + "b": [ + 321.7, + 379, + 10.1, + 17.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.805, + -0.918 + ], + "curve": [ + -0.677, + -1.009 + ], + "mark": [ + -0.805, + -0.918 + ] + } + }, + { + "l": 11, + "b": [ + 310.7, + 370.2, + 13.2, + 26.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.794, + -0.916 + ], + "curve": [ + -0.692, + -0.988 + ], + "mark": [ + -0.794, + -0.916 + ] + } + }, + { + "l": 11, + "b": [ + 254.1, + 366.1, + 58.5, + 29.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.753, + -0.908 + ], + "curve": [ + -0.731, + -0.923 + ], + "mark": [ + -0.753, + -0.908 + ] + } + }, + { + "l": 11, + "b": [ + 224.3, + 366, + 30.1, + 30.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.701, + -0.897 + ], + "curve": [ + -0.751, + -0.862 + ], + "mark": [ + -0.701, + -0.897 + ] + } + }, + { + "l": 11, + "b": [ + 213, + 373.8, + 9.9, + 21.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.675, + -0.892 + ], + "curve": [ + -0.747, + -0.841 + ], + "mark": [ + -0.675, + -0.892 + ] + } + }, + { + "l": 11, + "b": [ + 193.3, + 373.6, + 21.3, + 30.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.659, + -0.889 + ], + "curve": [ + -0.74, + -0.831 + ], + "mark": [ + -0.659, + -0.889 + ] + } + }, + { + "l": 11, + "b": [ + 119.1, + 371.3, + 72.2, + 30 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.601, + -0.877 + ], + "curve": [ + -0.687, + -0.815 + ], + "mark": [ + -0.601, + -0.877 + ] + } + }, + { + "l": 11, + "b": [ + 52.7, + 367.1, + 41.2, + 28 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.503, + -0.857 + ], + "curve": [ + -0.499, + -0.861 + ], + "mark": [ + -0.503, + -0.857 + ] + } + }, + { + "l": 11, + "b": [ + 7.1, + 366.6, + 42.1, + 32.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.45, + -0.847 + ], + "curve": [ + -0.342, + -0.924 + ], + "mark": [ + -0.45, + -0.847 + ] + } + }, + { + "l": 12, + "b": [ + 290.8, + 406.7, + 41.1, + 25.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.6, + -1.382 + ], + "curve": [ + -1.524, + -1.41 + ], + "mark": [ + -1.6, + -1.382 + ] + } + }, + { + "l": 12, + "b": [ + 220.1, + 406.7, + 67.7, + 26.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.378, + -1.245 + ], + "curve": [ + -1.383, + -1.243 + ], + "mark": [ + -1.378, + -1.245 + ] + } + }, + { + "l": 12, + "b": [ + 200, + 407.8, + 19.8, + 24.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.208, + -1.14 + ], + "curve": [ + -1.247, + -1.126 + ], + "mark": [ + -1.208, + -1.14 + ] + } + }, + { + "l": 12, + "b": [ + 187.5, + 413.9, + 9.9, + 17.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.141, + -1.098 + ], + "curve": [ + -1.187, + -1.081 + ], + "mark": [ + -1.141, + -1.098 + ] + } + }, + { + "l": 12, + "b": [ + 141.3, + 409.4, + 49.8, + 23.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -1.04, + -1.036 + ], + "curve": [ + -1.089, + -1.018 + ], + "mark": [ + -1.04, + -1.036 + ] + } + }, + { + "l": 12, + "b": [ + 121.2, + 408.2, + 18.5, + 25.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.902, + -0.951 + ], + "curve": [ + -0.942, + -0.936 + ], + "mark": [ + -0.902, + -0.951 + ] + } + }, + { + "l": 12, + "b": [ + 80.6, + 402.7, + 38, + 28.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.783, + -0.877 + ], + "curve": [ + -0.803, + -0.87 + ], + "mark": [ + -0.783, + -0.877 + ] + } + }, + { + "l": 12, + "b": [ + 58.9, + 409.8, + 21.1, + 20.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.667, + -0.805 + ], + "curve": [ + -0.655, + -0.809 + ], + "mark": [ + -0.667, + -0.805 + ] + } + }, + { + "l": 12, + "b": [ + 7.1, + 401.6, + 49.3, + 33.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.522, + -0.715 + ], + "curve": [ + -0.455, + -0.74 + ], + "mark": [ + -0.522, + -0.715 + ] + } + }, + { + "l": 12, + "b": [ + 8.4, + 400.8, + 8.8, + 6.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -1.062, + -1.062 + ], + "tilt": [ + -0.448, + -0.67 + ], + "curve": [ + -0.347, + -0.707 + ], + "mark": [ + -0.448, + -0.67 + ] + } + }, + { + "l": 13, + "b": [ + 321.3, + 446.9, + 10.5, + 18.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.333, + -1.492 + ], + "curve": [ + -0.996, + -1.272 + ], + "mark": [ + -1.333, + -1.492 + ] + } + }, + { + "l": 13, + "b": [ + 281.6, + 441.8, + 42.3, + 27.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.283, + -1.445 + ], + "curve": [ + -1.1, + -1.325 + ], + "mark": [ + -1.283, + -1.445 + ] + } + }, + { + "l": 13, + "b": [ + 251.9, + 439.1, + 27.9, + 26.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.206, + -1.372 + ], + "curve": [ + -1.211, + -1.375 + ], + "mark": [ + -1.206, + -1.372 + ] + } + }, + { + "l": 13, + "b": [ + 227.3, + 442.6, + 21.4, + 20.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.148, + -1.317 + ], + "curve": [ + -1.254, + -1.387 + ], + "mark": [ + -1.148, + -1.317 + ] + } + }, + { + "l": 13, + "b": [ + 196.2, + 440.1, + 30.7, + 28.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.092, + -1.265 + ], + "curve": [ + -1.264, + -1.377 + ], + "mark": [ + -1.092, + -1.265 + ] + } + }, + { + "l": 13, + "b": [ + 159.6, + 446.9, + 33.7, + 20.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -1.019, + -1.196 + ], + "curve": [ + -1.228, + -1.332 + ], + "mark": [ + -1.019, + -1.196 + ] + } + }, + { + "l": 13, + "b": [ + 128, + 442.2, + 29.4, + 26.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.948, + -1.13 + ], + "curve": [ + -1.141, + -1.255 + ], + "mark": [ + -0.948, + -1.13 + ] + } + }, + { + "l": 13, + "b": [ + 113.7, + 450.5, + 10.2, + 17.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.898, + -1.083 + ], + "curve": [ + -1.049, + -1.181 + ], + "mark": [ + -0.898, + -1.083 + ] + } + }, + { + "l": 13, + "b": [ + 82.7, + 443.7, + 33, + 27.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.857, + -1.044 + ], + "curve": [ + -0.954, + -1.108 + ], + "mark": [ + -0.857, + -1.044 + ] + } + }, + { + "l": 13, + "b": [ + 69.4, + 450.7, + 11.5, + 17.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.807, + -0.997 + ], + "curve": [ + -0.815, + -1.002 + ], + "mark": [ + -0.807, + -0.997 + ] + } + }, + { + "l": 13, + "b": [ + 45, + 439.2, + 26.9, + 25.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.772, + -0.964 + ], + "curve": [ + -0.703, + -0.919 + ], + "mark": [ + -0.772, + -0.964 + ] + } + }, + { + "l": 13, + "b": [ + 7.1, + 441.9, + 36.5, + 28.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.937, + -1.25 + ], + "tilt": [ + -0.703, + -0.899 + ], + "curve": [ + -0.444, + -0.73 + ], + "mark": [ + -0.703, + -0.899 + ] + } + }, + { + "l": 14, + "b": [ + 266.1, + 474.8, + 65.7, + 29.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.804, + -0.909 + ], + "curve": [ + -0.837, + -0.942 + ], + "mark": [ + -0.804, + -0.909 + ] + } + }, + { + "l": 14, + "b": [ + 231.4, + 484.3, + 9.9, + 18.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.734, + -0.886 + ], + "curve": [ + -0.72, + -0.871 + ], + "mark": [ + -0.734, + -0.886 + ] + } + }, + { + "l": 14, + "b": [ + 201.8, + 477.6, + 32.5, + 25.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.714, + -0.879 + ], + "curve": [ + -0.691, + -0.856 + ], + "mark": [ + -0.714, + -0.879 + ] + } + }, + { + "l": 14, + "b": [ + 163.9, + 479, + 36.6, + 23.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.674, + -0.866 + ], + "curve": [ + -0.643, + -0.835 + ], + "mark": [ + -0.674, + -0.866 + ] + } + }, + { + "l": 14, + "b": [ + 148.9, + 472.8, + 14.2, + 26.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.645, + -0.856 + ], + "curve": [ + -0.614, + -0.825 + ], + "mark": [ + -0.645, + -0.856 + ] + } + }, + { + "l": 14, + "b": [ + 103.3, + 477.7, + 43.8, + 26.1 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.61, + -0.845 + ], + "curve": [ + -0.587, + -0.821 + ], + "mark": [ + -0.61, + -0.845 + ] + } + }, + { + "l": 14, + "b": [ + 53.8, + 474.2, + 46.2, + 30.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.556, + -0.827 + ], + "curve": [ + -0.56, + -0.83 + ], + "mark": [ + -0.556, + -0.827 + ] + } + }, + { + "l": 14, + "b": [ + 7.1, + 478.1, + 43.7, + 26.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.503, + -0.809 + ], + "curve": [ + -0.551, + -0.858 + ], + "mark": [ + -0.503, + -0.809 + ] + } + }, + { + "l": 15, + "b": [ + 275.7, + 516.1, + 56.1, + 21.6 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.625, + -0.863 + ], + "curve": [ + -0.336, + -0.854 + ], + "mark": [ + -0.625, + -0.863 + ] + } + }, + { + "l": 15, + "b": [ + 230.3, + 515, + 46.8, + 21 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.615, + -0.85 + ], + "curve": [ + -0.599, + -0.85 + ], + "mark": [ + -0.615, + -0.85 + ] + } + }, + { + "l": 15, + "b": [ + 232, + 506.3, + 8.8, + 6.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.612, + -0.846 + ], + "curve": [ + -0.66, + -0.847 + ], + "mark": [ + -0.612, + -0.846 + ] + } + }, + { + "l": 15, + "b": [ + 218.1, + 519.5, + 9.9, + 18.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.609, + -0.842 + ], + "curve": [ + -0.697, + -0.845 + ], + "mark": [ + -0.609, + -0.842 + ] + } + }, + { + "l": 15, + "b": [ + 175.3, + 512.4, + 44.7, + 27.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.604, + -0.836 + ], + "curve": [ + -0.741, + -0.84 + ], + "mark": [ + -0.604, + -0.836 + ] + } + }, + { + "l": 15, + "b": [ + 145.6, + 510.1, + 27.8, + 26.8 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.596, + -0.825 + ], + "curve": [ + -0.745, + -0.83 + ], + "mark": [ + -0.596, + -0.825 + ] + } + }, + { + "l": 15, + "b": [ + 121.3, + 513.4, + 21.1, + 21.5 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.591, + -0.818 + ], + "curve": [ + -0.701, + -0.822 + ], + "mark": [ + -0.591, + -0.818 + ] + } + }, + { + "l": 15, + "b": [ + 75.9, + 517.3, + 42.7, + 21.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.584, + -0.809 + ], + "curve": [ + -0.591, + -0.809 + ], + "mark": [ + -0.584, + -0.809 + ] + } + }, + { + "l": 15, + "b": [ + 27.8, + 512.7, + 44.5, + 29.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.575, + -0.797 + ], + "curve": [ + -0.341, + -0.789 + ], + "mark": [ + -0.575, + -0.797 + ] + } + } + ], + "ornaments": [ + { + "l": 3, + "fit": [ + 194.15, + 100.35 + ], + "ours": [ + 194.2, + 100.3 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.609, + -0.943 + ], + "curve": [ + -0.635, + -0.96 + ], + "mark": [ + -0.609, + -0.943 + ] + } + }, + { + "l": 4, + "fit": [ + 252.81, + 135.68 + ], + "ours": [ + 252.8, + 135.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.312 + ], + "tilt": [ + -0.694, + -1.23 + ], + "curve": [ + -0.652, + -1.184 + ], + "mark": [ + -0.694, + -1.23 + ] + } + }, + { + "l": 6, + "fit": [ + 15.37, + 207.15 + ], + "ours": [ + 15.3, + 207.2 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.875, + -1.437 + ], + "tilt": [ + -0.986, + -0.927 + ], + "curve": [ + -0.715, + -0.891 + ], + "mark": [ + -0.986, + -0.927 + ] + } + }, + { + "l": 8, + "fit": [ + 229.88, + 278.88 + ], + "ours": [ + 229.9, + 278.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.685, + -0.844 + ], + "curve": [ + -0.601, + -0.963 + ], + "mark": [ + -0.685, + -0.844 + ] + } + }, + { + "l": 9, + "fit": [ + 256.67, + 313.95 + ], + "ours": [ + 256.6, + 313.9 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.687 + ], + "tilt": [ + -0.928, + -0.49 + ], + "curve": [ + -0.925, + -0.49 + ], + "mark": [ + -0.928, + -0.49 + ] + } + }, + { + "l": 10, + "fit": [ + 212.28, + 350.62 + ], + "ours": [ + 212.2, + 350.7 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.5, + -0.937 + ], + "tilt": [ + -0.646, + -0.905 + ], + "curve": [ + -0.743, + -0.958 + ], + "mark": [ + -0.646, + -0.905 + ] + } + }, + { + "l": 11, + "fit": [ + 106.03, + 386.09 + ], + "ours": [ + 106.1, + 386 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.937 + ], + "tilt": [ + -0.542, + -0.865 + ], + "curve": [ + -0.589, + -0.832 + ], + "mark": [ + -0.542, + -0.865 + ] + } + }, + { + "l": 14, + "fit": [ + 253.74, + 494.36 + ], + "ours": [ + 253.8, + 494.4 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.625, + -0.875 + ], + "tilt": [ + -0.754, + -0.892 + ], + "curve": [ + -0.749, + -0.888 + ], + "mark": [ + -0.754, + -0.892 + ] + } + }, + { + "l": 15, + "fit": [ + 14.97, + 529.02 + ], + "ours": [ + 15, + 529 + ], + "o": { + "page": [ + -0.75, + -0.937 + ], + "line": [ + -0.562, + -0.812 + ], + "tilt": [ + -0.568, + -0.787 + ], + "curve": [ + -0.081, + -0.772 + ], + "mark": [ + -0.568, + -0.787 + ] + } + } + ], + "drawnPage": { + "shipped": 0.987, + "page": 0.16, + "line": 0.078, + "tilt": 0.034, + "curve": 0.03, + "mark": 0 + }, + "corpus": { + "pages": 604, + "marks": 326515, + "pooled": { + "shipped": { + "far": 0.965, + "p50": 1.241 + }, + "page": { + "far": 0.193, + "p50": 0.395 + }, + "line": { + "far": 0.107, + "p50": 0.28 + }, + "tilt": { + "far": 0.042, + "p50": 0.208 + }, + "curve": { + "far": 0.029, + "p50": 0.175 + } + }, + "medianPage": { + "shipped": 0.975, + "page": 0.181, + "line": 0.101, + "tilt": 0.031, + "curve": 0.02 + }, + "shuffle": 0.328, + "lines": 8820 + }, + "heldOut": { + "shipped": { + "pages": 604, + "scored": 161265, + "far": 96.44, + "ci": [ + 96.35, + 96.53 + ], + "p50": 1.233, + "p95": 2.489, + "trained": 96.39 + }, + "page": { + "pages": 604, + "scored": 161265, + "far": 18.2, + "ci": [ + 18.02, + 18.39 + ], + "p50": 0.395, + "p95": 1.338, + "trained": 18.05 + }, + "line": { + "pages": 604, + "scored": 161265, + "far": 11.84, + "ci": [ + 11.68, + 12 + ], + "p50": 0.313, + "p95": 1.01, + "trained": 9.91 + }, + "tilt": { + "pages": 604, + "scored": 161265, + "far": 4.95, + "ci": [ + 4.84, + 5.05 + ], + "p50": 0.224, + "p95": 0.746, + "trained": 3.49 + }, + "curve": { + "pages": 604, + "scored": 161265, + "far": 4.12, + "ci": [ + 4.02, + 4.22 + ], + "p50": 0.198, + "p95": 0.691, + "trained": 2.34 + }, + "shuffle": 32.72, + "shuffleCurve": 34.4, + "mark": { + "placed": 324638, + "fellBack": 1877, + "share": 99.43, + "weak": 1210, + "clamped": 730, + "fallback": { + "far": 73.15, + "p50": 1.547, + "p95": 4.71 + }, + "far": 0.42 + } + }, + "neighbour": { + "anyP50": 8.32, + "sameP50": 24.8, + "radius": 3, + "jumpAny": 1.08, + "jumpSame": 0.014, + "jumpSameN": 47 + }, + "unmeasured": [ + 1, + 2, + 603, + 604 + ] +} diff --git a/docs/design/mark-placement.html b/docs/design/mark-placement.html new file mode 100644 index 00000000..8672cbef --- /dev/null +++ b/docs/design/mark-placement.html @@ -0,0 +1,937 @@ +Where the rectangles go + + + +
    +

    A decision, drawn — Hifth

    +

    Should the app line up what it draws one page at a time, or one printed line at a time?

    +

    + Hifth draws a rectangle over every mark and every word in the mus'haf, so it knows what your + finger is touching. Today those rectangles sit about a mark's height away from the marks they + are meant to be on. There are 6 things we could do about it, and this page draws + every one of them on the same real page of the mus'haf, at the size you would actually see them. +

    +
    +
    97%of rectangles are badly out today
    +
    5.0%after the recommended fix
    +
    326,515marks measured, on 604 pages
    +
    4pages nothing could measure
    +
    +
    + +
    + +
    +

    1A few words, before anything else

    +
    +
    Mark
    +
    One of the small signs printed above or below a letter — the vowel signs, the doubling sign, + the little circle over a silent letter. There are about 540 + of them on a page.
    +
    Printed line
    +
    One line of text as the printer set it. A page of this mus'haf holds fifteen, and the + printer stretches each one on its own so both ends reach the margins.
    +
    Verse-end circle
    +
    The small ornament that closes each verse. There are 9 of them on the page + drawn here, and they matter more than they look — see section 3.
    +
    Badly out
    +
    A rectangle sitting further than 0.75 of a unit from the ink it is meant to be on, on a + page 345 units across, where a mark is about five units wide. Roughly: far enough that you + would point at it and say it is on the wrong thing.
    +
    +
    + +
    +

    2What does it look like today?

    +

    + This is a real page of the mus'haf with the app's own rectangles drawn on it. The key below + reads every drawing on this page — the same three shapes reappear under every option in + section 6. +

    +
      +
    • a mark — a single tajweed shape, like a small vowel sign
    • +
    • a word — the outline around one printed word
    • +
    • where a mark's ink really is — found by searching around it; shown only in the close-ups, as the answer each option is graded against
    • +
    +
    +
    + + +
    +
    Three printed lines of page 179, at about the size a phone draws them. Every filled box is where the app currently believes a mark is.
    +
    +
    +
    + + +
    +
    Close up. The dashed outlines are where each mark actually is; the filled boxes are where the app puts them. That gap is the whole subject of this page.
    +
    +

    + So why not simply draw the dashed outlines? It is the first thing anybody asks + here, and it is a good enough question that it became option H below. The honest answer has two + halves. The outlines are not a placement, they are the measurement: each one is where a search + around that one mark found the most ink, which makes them the answer sheet every other option is + marked against — ship them and the score is perfect by construction, and a perfect score that + could not have come out any other way tells you nothing. And they are a record rather than a + rule: two numbers written down for each of 326,515 marks, which say what + this printing did and predict nothing whatever about a page nobody has measured. +

    +

    + Neither of those makes it wrong to ship — they make it something a different kind of check has + to vouch for. Option H does ship them, with a guard, and section 12 says what would have to be + true before anybody could trust it. +

    +

    + Across the whole mus'haf, 97 rectangles in every hundred + are badly out by the definition above. It is not a handful of bad pages: it is nearly every + rectangle on nearly every page, all sliding the same way. +

    +
    + +
    +

    3Why are they out at all?

    +

    + Because of how the app learned where things are on the page — and the answer is a little + embarrassing, which is exactly why it is worth reading. +

    +

    + Hifth ships pictures of the mus'haf from one source and the positions of the marks from another. + The two draw the same print at different sizes, so something has to line them up. The only + objects both sources name are the verse-end circles, so those are what the lining-up was + measured on — and it works beautifully. On the page drawn here the circles land within + 0.087 of a unit of each other, which is a fraction of the width of one of them. +

    +
    +
    + + +
    +
    The verse-end circles. The large outlines are where our picture of the page has them; the small dots are where the other source says they are, after the lining-up. They agree.
    +
    +

    + And then the text is out by a mark's height anyway. The two prints agree about + where their ornaments sit and disagree about where they set their words. So a lining-up measured + on the ornaments grades itself as excellent and is wrong where it is used. +

    +
    + +
    +

    4Why is this being asked now, and what if nobody answers?

    +

    + A by-eye session was opened to judge whether a proposed correction was better than what ships, + and it was stopped after a handful of trials because the person sitting it reported the + rectangles were wrong in several ways at once. That is not a verdict about the correction; it is + a report that both things being compared were frequently wrong. The measurement then confirmed + it. So the question has to be settled before any more of anyone's time is spent looking at + rectangles. +

    +

    + If nobody decides, option A is what happens, and it is not neutral: colouring a mark, tapping a + mark, or telling a reader which mark a rule lands on all rest on the rectangle being on the + right thing. Those are the next things this app was going to build, and they are all waiting on + this. +

    +
    + +
    +

    5Does anyone outside this project have this problem?

    +

    + Yes, and one half of it is a published result with a name. The other half we looked for and did + not find. +

    +
    +
    Lining up on one thing and using it for another is a known trap
    +
    + In medical imaging the error where you measured is called fiducial registration error and the + error where you actually care is target registration error, and + Fitzpatrick showed + the two are uncorrelated — how well the lining-up closed tells you nothing about how wrong + it is at the target. Our own numbers reproduce it: the closeness at the circles predicts + almost nothing about the error at the text, and on the component that matters most it is + slightly anti-correlated, which + theory also + predicts. There is even + a teaching tool built to break the + intuition, because practitioners keep falling for it. So did we, for three sittings. +
    +
    The remedy is the one below: hold out something the lining-up never saw
    +
    + SimpleITK's + worked notebook is the standard version. Section 8 is ours, inverted: the circles stop + being what we measure on and become what we check against. +
    +
    Why the printed line, and not something else
    +
    + Two settings of the same text drift apart because their letter widths and line spacing differ, + so the error builds up along a line and resets at the next one. That is documented in + typesetting — + inconsistent + glyph widths, + substituted + metrics moving line breaks, + line metrics, and most + sharply in + work showing letter positions leak redacted words. + This was written down before our own numbers were looked at, and then it held. +
    +
    Global first, then local, is the conventional shape
    +
    + Thin-plate splines + pair a global part with a local warp; + a 2024 document-registration + paper learns a coarse transform then a fine one; and there is + a patent on + line-based registration for moving annotations between images, which is almost exactly + what we are doing. +
    +
    What we looked for and did not find — said plainly
    +
    + Nobody appears to have published on our actual case: lining up two independently typeset + editions of the same text at the level of individual letters, where both sides are + drawn shapes with no photographs and no text recognition involved. The document work we found + is all about photographs of paper, where the distortion is physical. Nor did we find anyone + writing up the inversion in section 9 as a recipe. Two areas were not opened at all: + Arabic-typesetting corpora, and the Qur'anic-computing literature. Either could hold + something. +
    +
    +
    + +
    +

    6The options, drawn

    +

    + All 6 on the same page, the same crops, the same size. For all of them but the + last, the percentages under each are measured on marks that option's correction was not worked + out from, which is the only way a finer correction cannot flatter itself. The last one, H, cannot + be measured that way at all, and its card says what it reports instead. +

    +
      +
    • a mark — a single tajweed shape, like a small vowel sign
    • +
    • a word — the outline around one printed word
    • +
    • where a mark's ink really is — found by searching around it; shown only in the close-ups, as the answer each option is graded against
    • +
    +
    +
    +

    A — Leave the rectangles where they are

    +

    what the app does today

    +
    +
    +
    + + +
    +
    Where option A would put every rectangle on these three lines — key above.
    +
    +
    +
    + + +
    +
    The same option, close up — about 10 times the size above, on the mark this correction moves furthest. The filled box is still where option A puts the mark; the new dashed outline is where the mark really is, so the gap between the two is the error this option leaves behind.
    +
    +
    +

    What it does

    Nothing changes. The rectangle for every mark and every word stays where the fit on the verse-end circles put it.

    +

    What a reader would notice

    A reader sees what they see today: a highlight or a tap-target that sits noticeably off the letter it is meant to be on, on nearly every line, in the same direction every time. It does not wobble or vary page to page — it is a steady, learnable offset, which is exactly why it reads as "the app is a bit off" rather than as a glitch.

    +

    Rectangles badly out

    96.44% [96.35, 96.53]
    measured on marks the correction was not worked out from

    +

    On this page

    98.7%
    of 536 marks further than 0.75 of a unit from their own ink

    +
    +
    +
    +
    +

    B — Line each page up as a whole

    +

    two more numbers a page

    +
    +
    +
    + + +
    +
    Where option B would put every rectangle on these three lines — key above.
    +
    +
    +
    + + +
    +
    The same option, close up — about 10 times the size above, on the mark this correction moves furthest. The filled box is still where option B puts the mark; the new dashed outline is where the mark really is, so the gap between the two is the error this option leaves behind.
    +
    +
    +

    What it does

    Every rectangle on a page moves together, by one amount worked out from that page's own marks.

    +

    What a reader would notice

    The offset shrinks a lot on most lines but does not disappear, because one correction is being asked to fit every line on the page and most pages are not printed with every line in exactly the same place. A reader would likely still notice marks sitting slightly off on some lines, especially near the top or bottom of the page, even though the page as a whole looks visibly better aligned than today.

    +

    Rectangles badly out

    18.20% [18.02, 18.39]
    measured on marks the correction was not worked out from

    +

    On this page

    16.0%
    of 536 marks further than 0.75 of a unit from their own ink

    +
    +
    +
    +
    +

    F — Line each printed line up on its own, and let it tilt

    +

    the recommendation — four more numbers a line

    +
    +
    +
    + + +
    +
    Where option F would put every rectangle on these three lines — key above.
    +
    +
    +
    + + +
    +
    The same option, close up — about 10 times the size above, on the mark this correction moves furthest. The filled box is still where option F puts the mark; the new dashed outline is where the mark really is, so the gap between the two is the error this option leaves behind.
    +
    +
    +

    What it does

    Each printed line gets its own amount, and that amount is allowed to grow along the line rather than being the same at both ends. Marks and words move together.

    +

    What a reader would notice

    A reader tapping or reading along nearly any single line would see the highlight sit on the right letter, start to finish. The visible exceptions are concentrated: whole lines that still read as a little off, and the two ends of a line reading worse than its middle — so an occasional line, or the first/last word of a line, is where a mismatch would still show up.

    +

    Rectangles badly out

    4.95% [4.84, 5.05]
    measured on marks the correction was not worked out from

    +

    On this page

    3.4%
    of 536 marks further than 0.75 of a unit from their own ink

    +
    +
    +
    +
    +

    G — Line the marks up, and leave the words where they are

    +

    the same, for half the page's geometry

    +
    +
    +
    + + +
    +
    Where option G would put every rectangle on these three lines — key above.
    +
    +
    +
    + + +
    +
    The same option, close up — about 10 times the size above, on the mark this correction moves furthest. The filled box is still where option G puts the mark; the new dashed outline is where the mark really is, so the gap between the two is the error this option leaves behind.
    +
    +
    +

    What it does

    The same per-line correction, applied only to the marks. Word rectangles stay on the fit they ship on today.

    +

    What a reader would notice

    Wherever the app highlights a single mark, it looks the same as option F — sitting on the right letter. But anything that outlines or reacts to a whole word — tapping a word, a word-level highlight — still shows today's offset, so a reader who taps a word right after seeing a mark land correctly would notice the two behaving differently.

    +

    Rectangles badly out

    4.95% [4.84, 5.05]
    measured on marks the correction was not worked out from

    +

    On this page

    3.4%
    of 536 marks further than 0.75 of a unit from their own ink

    +
    +
    +
    +
    +

    I — Line each printed line up on its own, and let it bend

    +

    the strongest measured option — one more number a line than F

    +
    +
    +
    + + +
    +
    Where option I would put every rectangle on these three lines — key above.
    +
    +
    +
    + + +
    +
    The same option, close up — about 10 times the size above, on the mark this correction moves furthest. The filled box is still where option I puts the mark; the new dashed outline is where the mark really is, so the gap between the two is the error this option leaves behind.
    +
    +
    +

    What it does

    The same as F, except the amount is allowed to bend along the line rather than only grow at a steady rate. Marks and words move together.

    +

    What a reader would notice

    Close to F in what a reader would see day to day — marks sitting on their letters along almost every line — with fewer of the whole-line misses F still leaves behind. The two ends of a line are still the likeliest place to notice anything off; bending the correction does not reach that particular pattern any better than tilting it does.

    +

    Rectangles badly out

    4.12% [4.02, 4.22]
    measured on marks the correction was not worked out from

    +

    On this page

    3.0%
    of 536 marks further than 0.75 of a unit from their own ink

    +
    +
    +
    +
    +

    H — Put each mark where its own ink is, and line up the rest

    +

    the most accurate — and the one this measurement cannot grade

    +
    +
    +
    + + +
    +
    Where option H would put every rectangle on these three lines — key above.
    +
    +
    +
    + + +
    +
    The same option, close up — about 10 times the size above, on the mark this correction moves furthest. The filled box is still where option H puts the mark; the new dashed outline is where the mark really is, so the gap between the two is the error this option leaves behind.
    +
    +
    +

    What it does

    Every mark whose ink was found convincingly goes exactly where it was found. The rest — and every word rectangle — get the per-line correction instead.

    +

    What a reader would notice

    For most marks, a reader would see the highlight sit exactly on the letter's own ink — not merely on the line, on that letter. A minority of marks (the ones the search could not place convincingly) and every word behave like option F instead, so the experience is not perfectly uniform: most of the page looks exact, and the rest looks like the recommendation above.

    +

    Rectangles badly out

    0.42%
    counting only the 0.6% it would not place from ink — the rest sit on the measurement and cannot be graded by it

    +

    Placed from the mark's own ink

    99.4%
    1,877 marks fall back to the printed line: 730 where the search ran out of room and 1,210 where it found nothing convincing

    +
    +
    +
    +

    + F still carries the word “recommended” and I is the better option on every + number here. That is not an oversight. I was worked out after this page was written, + by asking what the rectangles F leaves badly out have in common — and the answer was each + other: more than half of them sit on the one printed line in fifteen that has gone wrong as a + whole, and a rectangle at either end of a line is about twice as likely to be badly out as one + in the middle. A correction that can only grow at a steady rate cannot follow that, because a + rate that splits the difference is wrong in the same direction at both ends. Letting it bend is + the whole of option I, and it costs one more number a line. +

    +

    + Moving the recommendation is the decision this page exists to ask for, so the page does not + quietly move it. What the page can do is put the two side by side and say which way the numbers + point, which is what section 7 does. +

    +
    +
    + +
    +

    7The same options, as numbers

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Every mark on all 604 pages, 326,515 of them, split down the + middle: half of each page's marks used to work the correction out, the other half — which the + correction never saw — used to grade it. The first column is that grade. The last column is the + same correction marked on its own homework, and the gap between the two is what a correction + fitting noise rather than a real difference between the two prints would show up as. The two + middle distances are in the same units as the pictures above, where the typical mark on this + page is 5.6 units across and 3.6 tall — so a miss past 0.75 of a unit, + which is what “badly out” counts, is a rectangle sitting off its own mark. +
    OptionBadly out95% rangeTypical missWorst 5%Marked on its own homework
    A — as it ships96.44%96.35 – 96.531.232.4996.39%
    B — each page18.20%18.02 – 18.390.401.3418.05%
    E — each printed line, no tilt11.84%11.68 – 12.000.311.019.91%
    F and G — each line, tilted4.95%4.84 – 5.050.220.753.49%
    I — each line, bent4.12%4.02 – 4.220.200.692.34%
    H — each mark, where its own ink is
    on the 0.6% it hands back to the printed line
    0.42%1.554.71
    +
    +

    + Why H's row is mostly empty, and why that is the honest way to print it. + Every other row is graded by holding half of each page's marks back and marking the + correction on marks it never saw. H has nothing to hold back: each mark carries its own two + numbers, so there is no second half of anything. What the row does state is the part of H + that is a model — the 1,877 marks it refuses to + place from ink and hands to the printed line instead, which is + 0.6% of the mus'haf and where all of its remaining + error lives. The other 99.4% sit exactly on the ink they were + measured against, so their score is zero by construction and means nothing at all. That is + the trade this option asks you to accept: far fewer rectangles out of place, and no way for + this instrument to prove it. Section 12 says what could. +

    +

    + The control that makes this a finding rather than a bigger model. Give every + printed line another line's correction, chosen at random, and grade it on the same + unseen half: 32.7% of rectangles are badly out — worse than + applying no per-line correction at all, and + 7 times worse than the real one. So the + per-line figures are not what any correction with that many knobs would have given. The last + column says the same thing from the other side: the finest correction does flatter itself, by + 1.5 points, and that is the whole of the + flattery — against a fall from 18.2% to + 5.0% that it cannot account for. +

    +

    + The same control, run again on the bent line. A bend has one more number a + line to be wrong with, so it is where that objection bites hardest. Wearing another line's bend + leaves 34.4% badly out — worse again than wearing another + line's tilt, and far worse than correcting nothing per-line at all. A shape freer still, one + allowed to change direction twice along a line rather than bend once, was worked out the same + way and refused: it does better on the marks it was worked out from and worse on the marks it + was not, which is what an allowance with nothing to find looks like. The ladder stops where it + stops being paid for. +

    +

    + And what bending does not reach, since it is the reason I is not the end of this. + It fixes the middle of a printed line and barely touches the ends. A rectangle at the end of a + line remains about twice as likely to be badly out as one in the middle, under every option on + this page. That is a fact with no explanation yet, and section 12 is where it waits. +

    +
    +
    + +
    +

    8What does each one cost?

    +

    + Accuracy is one column of this and it is the column where two of the options are the same + number. Everything that separates them is here instead: what has to be worked out, what it + disturbs on the way in, and what has to be kept true forever afterwards. +

    +
    +
    +
    +

    A

    +

    Leave the rectangles where they are

    +

    what the app does today

    +
    +

    B

    +

    Line each page up as a whole

    +

    better, and measurably not enough

    +
    +

    F

    +

    Line each printed line up on its own, and let it tilt

    +

    the recommendation

    +
    +

    G

    +

    Line the marks up, and leave the words where they are

    +

    the fork worth noticing

    +
    +

    I

    +

    Line each printed line up on its own, and let it bend

    +

    the strongest thing that has been measured

    +
    +

    H

    +

    Put each mark where its own ink is, and line up the rest

    +

    the most accurate, and the least provable

    +
    +
    How wrong it leaves things
    +
    + 96.4% +
    + of rectangles land badly out +
    + 18.2% +
    + of rectangles land badly out +
    + 5.0% +
    + of rectangles land badly out +
    + 5.0% +
    + of rectangles land badly out +
    + 4.1% +
    + of rectangles land badly out +
    + 0.4% +
    + of rectangles land badly out — counting only the ones it will not place from ink +
    +
    What it costs to work out
    NothingNo new numbers to work out, nothing to write down, nothing to rebuild.
    Two numbers a pageAbout 1,208 numbers for the whole book, in a table the app already has a place for.
    Four numbers a printed lineAbout 35,280 numbers for the whole book, in a table that does not exist yet — both the part that builds the rectangles and the part that reads them have to learn it.
    The same tableExactly the same numbers as the option above. The only difference is what they are applied to.
    Six numbers a printed lineAbout 52,920 numbers for the whole book — the same table as the option above with one more number in each row, so it costs the same work to build and read. Nothing new has to be learnt to use it.
    Two numbers for every mark653,030 numbers for the whole book — not a rule that explains anything, but one measurement of one printing, written down in full. It cannot live in the small table the app reads today and needs a file of its own beside it.
    +
    What it disturbs
    NothingEvery rectangle in the app stays exactly where it is.
    Every word rectangleMoving a page moves the words on it too, so every word rectangle the app ships is rebuilt and re-checked, and the pictures the tests compare against may move.
    Every word rectangleSame as the option above: the words move with the marks, so all of them are rebuilt and re-checked and the comparison pictures may move.
    Only the marksNo word rectangle moves, so nothing the app already ships for words is rebuilt, re-checked, or re-photographed.
    Every word rectangleThe same as the two options above: words move with the marks, so all of them are rebuilt and re-checked and the comparison pictures may move.
    Every word rectangle, stillThe marks go where their own ink is, but words and verse-end circles have no such measurement, so they take the per-line correction — which means they move, are rebuilt, and are re-checked exactly as in the two options above.
    +
    What it risks
    The complaint standsAlmost every rectangle sits about a mark's height from the mark it is meant to name. Colouring one exact letter cannot be built on rectangles this far out — it would put the colour on the letter next door.
    About one rectangle in six is still badly outAnd that is the ceiling, not a first attempt: the best correction of this shape that could be fitted to a page from its own ink does not do better. Adding a stretch to it buys under two percent and makes a third of pages worse.
    Half of what is left over now has a nameAfter this correction a small minority of rectangles are badly out for a reason of their own rather than everything being slightly out. That reason was looked for: whole printed lines go wrong together, and the ends of a line are twice as bad as the middle. Option I is what came of it, and it does better on every number here.
    Two geometries from one measurement, foreverThe marks would sit on a lining-up we measured, and the words on one we have measured as wrong — and something has to hold the two to each other from then on. The saving is real and it is one-off; the obligation is permanent.
    The ends of a line are still twice the middleBending fixes the middle of a printed line and barely touches its ends, so a rectangle at the end of a line is still about twice as likely to be badly out as one in the middle. Whatever causes that is not about where along the line a mark sits, and no further bending will reach it.
    It cannot be graded, and it says nothing about an unmeasured pageEvery other option can be marked on marks it never saw. This one has nothing held back, so the only honest check is a person looking at rectangles and saying whether they sit right. And because it records rather than explains, it predicts nothing at all about a printing nobody has measured.
    +
    Getting back
    Nothing to undo
    Reversible, not freeDropping the table means rebuilding everything it moved.
    Reversible, not freeDropping the table means rebuilding everything it moved.
    Easiest to undoNothing shipped has moved, so dropping it rebuilds nothing.
    Reversible, not freeDropping the table means rebuilding everything it moved — the same cost as the option above.
    Reversible, not freeDropping the table means rebuilding everything it moved — the same cost as the two options above.
    +
    +
    +

    + Read F and G side by side, because that is the fork this page cannot settle by + measuring. Their first rows are identical, and not by coincidence — they are the same + correction, so no measurement on this page can tell them apart. Every figure here is taken on + marks, and G is the option that leaves the words alone, so the whole of its cost falls + somewhere nothing above is looking. It buys one rebuild it does not have to do, and pays for it + by keeping two different linings-up true to each other for as long as the app exists. +

    +
    +
    + +
    +

    9What does moving everything do to the verse-end circles?

    +

    + This is the check that costs us something, so it is the one worth trusting. The circles are the + one thing on the page that is not ink under a rectangle — the correction is worked out + without them, so where they end up is a free, independent opinion. +

    +

    + The opinion is unwelcome and it is the same at every option: correcting the text pulls the + circles off themselves, on every page measured. That is not a bug in the correction. It is the + two prints disagreeing about their text and agreeing about their ornaments, stated from the + other side — you cannot have both, and this is the cost of choosing the text. +

    +
    +
    +
    + + +
    +
    A — the circles land on themselves.
    +
    +
    +
    + + +
    +
    F — the same circles, after the recommended correction. They no longer do.
    +
    +
    +

    + Whether that matters depends entirely on what the circles are used for, and today they are used + for one thing: working out the lining-up in the first place. Nothing the reader touches is + placed by them. So the honest statement is that this option trades an error nobody sees for an + error everybody sees — in the right direction — and that trade should be made knowingly rather + than discovered later. +

    +
    + +
    +

    10What about the pages nothing could measure?

    +

    + 4 pages of the mus'haf — 1, 2, 603, 604 — carry too little to + measure a correction from. They are the opening and closing pages, which are set differently + from every other page in the book. +

    +

    + This is the whole of the generalisation question, and it is worth being blunt about + it. Every option here is worked out on the very page it is applied to. There is no + version of this that generalises to a page it has not seen, because there is no model of a page + — there is a table. That is not a flaw to be fixed; a table over 604 pages and + 8,820 printed lines is exactly the right shape when each page really + does differ. But it means those 4 pages get whatever fallback we choose, + and the fallback is the entire answer for them. The proposal is that they keep option A + unchanged, and that the app never claims a precision there it has not earned. +

    +
    + +
    +

    11What else could have been on this page?

    +
    +
    Letting every rectangle find its own nearest ink, with nothing holding it back
    +
    + This page was written twice on this point, and the first version was wrong, so the reasoning + is left here rather than quietly replaced. The objection was that two marks sit about as far + apart as the error is large, so a search for the nearest ink would sometimes find the + neighbour's and centre a rectangle neatly on the wrong mark — which is one of the + complaints that started all of this. It was worth measuring rather than asserting, and it did + not survive. The nearest mark of any kind is a median 8.32 units away and the + nearest one that looks the same is 24.8, against a search that + reaches 3 and an error near one. Across all + 326,515 marks the search ends up nearer some other mark + 1.08% of the time and nearer an identical-looking one + 0.01% — 47 marks in the whole mus'haf. + What the measuring did refuse is different. Where the ink match is poor the + search is chasing noise rather than finding a mark, and a search that stops at the edge of the + distance it was allowed has not found anything at all — it has run out of room. Option H is + this idea with those two refusals built in, and the marks it refuses inherit the printed line. +
    +
    Drawing the marks but never letting anything point at one
    +
    + Kept in the written record as option D and not drawn here, because there is nothing to draw: + it puts the same rectangles on the page and forbids the feature that would use them. It is + the honest halfway house if none of the drawn options is good enough, and it is a decision + about the app rather than about placement. +
    +
    Stretching each page rather than sliding it
    +
    + Option C in the written record. Measured and dropped. The stretch is real — it is certain, statistically — and it is worth + about a fifth of the sliding, removes under two per cent of what is left, and makes a third of + pages slightly worse. It is a footnote, not an option. +
    +
    Correcting word by word
    +
    + Not usable. A word carries about four marks, and an average over four marks is mostly noise; + the correction would fit the noise and look excellent while being worse. The printed line is + the finest grain with enough marks in it to mean anything. +
    +
    Letting a printed line bend more freely than option I bends it
    +
    + Option I lets the correction bend once along a line. A shape freer still — allowed to change + direction twice — was worked out the same way and refused. It does better on the marks it was + worked out from and worse on the marks it was not, which is what an allowance with nothing + left to find looks like: it is fitting the wobble in the marks it was given rather than + anything about the page. Each extra freedom also needs more marks on a line before the line + is allowed to use it, so the freer shape quietly abandons the short lines, which are already + the ones going worst. That is where this ladder stops. +
    +
    Fixing the mark names instead
    +
    + Already ruled out by counting. Every one of the 326,515 marks was + checked against the name the printer gave it, and the number of genuinely wrong names is zero. + Every complaint about a rectangle being on the wrong thing is this page's problem, not a + naming problem, and the two are kept separate on purpose. +
    +
    +
    + +
    +

    12What would change the answer?

    +
      +
    • + A person preferring the old rectangle. Everything here is measured against + ink. One forced-choice sitting on pages the correction was never worked out from is the only + witness that is not, and it is owed before anything ships. If a reader cannot see the + difference, option B is the cheaper answer and this page over-argues. +
    • +
    • + Someone finding a use for the verse-end circles. If the circles ever place + something a reader touches, section 9 stops being a free trade and becomes a real cost. +
    • +
    • + The half of the remainder that still has no cause. This bullet used to say + nobody had looked for why the rectangles still badly out are more concentrated than the general + spread predicts, and that finding out could make another option. Somebody looked, and it did: + whole printed lines go wrong together, and lines go wrong in a way a steady rate cannot follow, + which is option I. But that only accounts for the middle of a line. A rectangle at either + end of a printed line is still about twice as likely to be badly out as one in the + middle, after every correction on this page, and nothing here explains why. Whoever finds it + could make another option again. +
    • +
    • + Somebody checking option H by eye and finding it holds. H cannot be graded + the way the others are, and that is not a reason to refuse it — it is a statement about which + instrument is needed. The check it wants is not the same as the one above: rather than being + shown two rectangles and asked which sits better, a reader is shown a sample of rectangles H + has placed and asked, of each one, whether it is on the mark it is named after. If a sample + drawn at random comes back clean, the procedure that placed all + 326,515 of them has been vouched for, which is the only kind of + guarantee a record rather than a rule can be given. That sitting has not happened. Until it + does, H is the most accurate option on this page and the one with the least behind it. +
    • +
    +
    + +
    +

    13What is this not settling?

    +

    + Not whether the app should colour individual marks at all — that is + its own question, and it is open. Not which colours anything + is drawn in. Not the mark names, which were counted and are right. And not the four pages in + section 10, which need their own answer whichever option wins. +

    +
    + +
    + +
    +

    + Where the pictures come from. Nothing here is a mock-up. The page is the real + mus'haf image the app ships. Every rectangle is computed by the same code that would build the + shipped geometry, from the printer's own measured ink, and the colours are read out of the app's + stylesheet when this page is built. All the counts are recomputed from the measurements each + time. +

    +

    + Built by scripts/build-placement-options.mjs — edit that, not this. The technical + write-up behind it, including everything that is still unresolved, is + docs/design/mark-registration.md. Page 179 was chosen because its numbers sit + at the middle of all 604 measured pages on every option, not at either end. The print is + assets/pages/hafs-kfqc/179.svg: outlined shapes only, no text. Measured + 2026-08-15. +

    +
    diff --git a/docs/design/mark-registration.md b/docs/design/mark-registration.md index f807c802..c2f67c91 100644 --- a/docs/design/mark-registration.md +++ b/docs/design/mark-registration.md @@ -246,12 +246,46 @@ it does so on every page. That is the finding. ### Is this a fact about the marks, or about the whole page? -About the whole page. The same displacement can be recovered **without looking at a single -mark**: rasterising the entire second printing of a page — letters and all — and sliding it -against the entire shipped page needs the same shift, around 0.75 across and 1.00 down, with +About the whole page's **text**. The same displacement can be recovered **without looking at a +single mark**: rasterising the entire second printing of a page — letters and all — and sliding +it against the entire shipped page needs the same shift, around 0.75 across and 1.00 down, with the two pages' total ink agreeing to within 0.05 percentage points. It is the same print, displaced. The marks are not wrong; the alignment between the two printings is. +But "whole page" was too generous a word, and the next question says why. + +### Is there anything on the page that is *not* displaced? + +Yes, and it is the one thing we were already using. Every ayah ends with a small ornament, and +both printings mark them — they are the only objects the two have in common, which is why the +alignment between the printings was built out of them in the first place. + +So they can be asked a question nothing else on the page can answer. The alignment was fitted +to the ornaments and knows nothing about the ink; the displacement above was measured from the +ink and has never seen an ornament. Move the text by that displacement, and where do the +ornaments end up? + +| | ornaments land this far from their partners | +|---|---| +| under the alignment we ship today | **0.053 units** | +| if the text's displacement were applied to them too | **1.206 units** | +| pages where that makes them worse | **120 of 120** | + +Measured on 1,305 ornament pairs across 120 pages. + +They were already right, and the correction would break them — by very close to the whole size +of the correction, on every page it was asked. That is the sharpest form of the finding in this +document: **the two printings agree about where their ornaments are and disagree about where +they set their text.** A difference the whole page shared would have read the same on both +lines of that table. + +Two things follow, and both are load-bearing. First, this is the only evidence here that does +not come from the same well as the claim — everything else fits a rectangle to agree with ink +and then grades it by agreement with ink, whereas the ornaments could not have been fitted to +and were still asked. Second, whatever correction ships belongs to the *text* — the words and +the marks — and must leave the ayah-end ornaments exactly where they are. Moving everything +together would fix the marks by breaking the one part of the page that is currently right. + ### Is the error systematic or random, and would correcting it help? Separating the two is the difference between a fixable bug and an irreducible noise floor. @@ -359,7 +393,8 @@ So 107 of the 108 are the last sixteenth of a unit of residual placement, and th indistinguishable from noise. **There is no evidence here that the two printings disagree about what any mark is.** This section was written expecting to find something and found nothing, and saying so plainly is the honest result rather than a wasted section — it means the entire -measured defect in the mark layer is the displacement, and Option B below is the whole remedy. +measured defect in the mark layer is the displacement, and the recommended option in §⑦ is the +whole remedy. That also settles what the remaining work is. Separating fatha from kasra is a *positional* feature — where the mark sits relative to the letter carrying it — and not a better shape @@ -376,13 +411,152 @@ residue, which is a known artefact of correcting by a median rather than a mean. hypothesis is refuted, and refuting it is what makes the single page-wide displacement the right correction rather than a per-class one. +### Does any mark go wrong per class in a way this measurement cannot see? + +The section above refutes a per-class bias using the ink search. On 2026-08-17 a reader found +one the ink search cannot see, and the two results do not disagree — they are about different +marks. + +Two sittings, thirty marks each, drawn not at random but from the **bottom of the marks the +search accepts** — the band where our rectangle and the ink it settled on overlap between 0.55 +and 0.65, which begins exactly at the floor below which the search gives up, and the band +immediately above it, 0.65 to 0.75. Being two rather than one is what makes them worth +reporting: each finding below can be read twice, on marks that differ only in how sure the +search was. Nothing here transfers to the 324,638 marks it placed confidently, and that caveat +travels with every number. + +**Hamza is about a page unit across, and nothing else is.** Every hamza in both sittings was +moved sideways in the same direction — eleven of eleven — with the size left as drawn. + +| | marks | sideways, median | vertical | width | +|---|---|---|---|---| +| overlap 0.55–0.65 | 7 | **1.077** units | 0.015 | 0.000 | +| overlap 0.65–0.75 | 4 | **0.573** units | 0.039 | 0.000 | + +In the weaker band six of the seven fall between 1.04 and 1.35; in the better band all four fall +between 0.50 and 0.81. Across every other mark in the weaker sitting the median sideways move is +**0.002 units** — nothing at all. So this is not a page-wide shift surfacing in a small sample. +It is one class, in one direction, at a size a reader sees without being told to look — and it +**halves as the match improves**. + +A hamza is the one mark here regularly printed sitting *on* another letter, and the obvious +mechanism is that a search matching ink against a drawing can settle on the seat rather than on +the hamza where the two touch — which would put the rectangle a letter's width across and leave +its size right, which is exactly what the reader corrected. That is a hypothesis, recorded as +one; nothing here tests it. + +**Fatha and kasra are drawn too short, and by less when the match is better.** Both were grown +taller with their width left alone: fatha by a median of 0.600 units in the weaker band and +0.065 in the better one, kasra by 0.500 and 0.215, on boxes about 3.9 tall. Nine of the eleven +marks in the weaker sitting that grew by more than 0.2 are one or the other. + +**Everything shrinks the same way**, which is the finding that ties the rest together. Between +the two bands the median distance the reader moved a rectangle falls from 0.363 units to 0.276, +and the worst from 5.877 to 2.053 — that worst being a kasra in the weaker band that ended with +essentially no overlap at all with where we drew it. **So the overlap score is doing its job.** +It does not predict whether a reader complains, because a reader who reaches for a mark by +pointing at its ink files a complaint either way (㉑ below); it predicts, quite well, *how far +out we were*. + +None of this contradicts the refutation above, and it is worth being exact about why. That one +measured the residue left after correction across every mark in the book, using the ink. This +one measured sixty marks the ink search itself flagged as doubtful, using a person. A class +error living only in the tail the search is unsure about is invisible to an average over the +whole book, and it is the first thing a reader looking at that tail would notice. Both readings +are true. + +What to do about it is ㉒ in the open questions below, and the short version is: find out +whether this is a property of the weak tail or a property of hamza, before touching any rule. + ## ⑤ What do people outside this project do about this? -Enough of the answer is surprising that it is worth stating up front: **there is no published -precedent for validating per-mark boxes against the ink.** Two adjacent fields have most of -the answer, and neither does quite this. The claims below were checked against primary -sources; where something could not be established, it is named as such at the end of the -section rather than smoothed over. +Two different questions get asked here, and the outside world answers them very differently. +*How should a box be scored against ink?* — **there is no published precedent for validating +per-mark boxes against the ink**, though two adjacent fields have most of the answer. *Why were +the boxes in the wrong place to begin with?* — that one turns out to be a **published theorem +with a name**, in a field that has been warning about it for twenty years. The claims below were +checked against primary sources; where something could not be established, it is named as such +at the end of the section rather than smoothed over. + +### Why the boxes were wrong — somebody proved this, and it has a name + +**Our headline finding is a theorem.** In medical image registration, the error measured at the +landmarks an alignment was fitted to has a name, and so does the error at the thing you actually +care about — and Fitzpatrick's +[*Fiducial registration error and target registration error are uncorrelated*](https://spie.org/Publications/Proceedings/Paper/10.1117/12.813601) +(SPIE Medical Imaging 2009; +[abstract](https://ui.adsabs.harvard.edu/abs/2009SPIE.7261E..02F/abstract), +[Semantic Scholar](https://www.semanticscholar.org/paper/8ff408ad2d79dc5d8121bed0a702eb6d70a4e258)) +shows the two are **uncorrelated**. How well the fit closed where it was fitted tells you nothing +about how wrong it is where it was used. Our ayah-end ornaments are the landmarks, our marks are +the target, and the correlation of **0.117** in §④ is that result reproduced in a mus'haf. + +Three things follow, and they are why this is worth reading rather than merely citing. + +It is a **trap people keep falling into**, not an exotic case: the literature says in as many +words that practitioners still fight the intuition that a tight fit at the landmarks means a +good alignment, and somebody built +[a teaching tool specifically to break that intuition](https://pmc.ncbi.nlm.nih.gov/articles/PMC7612039/). +Three sessions of this project went into arguing about that residual. That is the trap, and we +were in it. + +Worse, the residual is not merely uninformative — it is +[weakly *anti*-correlated in theory](https://www.sciencedirect.com/science/article/abs/pii/S1361841511000028), +because a fit can close tighter at its landmarks by absorbing their noise and be worse +everywhere else. So "this page fits its ornaments to a tenth of a unit" was never evidence in our +favour at all. + +And the remedy that field reaches for is the one §④ now uses: **hold out a target the fit never +saw**. [SimpleITK's registration-error notebook](http://insightsoftwareconsortium.github.io/SimpleITK-Notebooks/Python_html/68_Registration_Errors.html) +is the worked version. Turning the ornaments from the thing we fit on into the thing we check +is that recipe, inverted. + +**The mechanism is a typography one, and it predicted the line before we measured it.** Two +renderings of one text diverge because their glyph advance widths and line metrics differ, which +slides every glyph along a line and changes where lines break and how they justify — documented +for PDF in [inconsistent glyph width information](http://martin.hoppenheit.info/blog/2018/pdfa-validation-and-inconsistent-glyph-width-information/), +in [how substituted metrics move line breaks and spacing](https://www.syncfusion.com/blogs/post/pdf-font-issues-javascript-pdf-viewer), +in [line metrics as a font-development concern](https://silnrsi.github.io/FDBP/en-US/Line_Metrics.html), +and most sharply in +[*Story Beyond the Eye: Glyph Positions Break PDF Text Redaction*](https://arxiv.org/pdf/2206.02285), +where per-glyph positions carry enough information to leak redacted words. Error that +accumulates **along a line** and resets at the next one is the expected signature of two prints +of one text. That is an argument for the printed line as the unit of the correction, and it was +arrived at without looking at our own numbers. + +**It has since been tested, and it held.** That order matters more than the result: a prediction +made from somebody else's literature, written down, and only then measured is a different kind +of evidence from a pattern noticed in our own data and explained afterwards — the second is +always available and is worth very little. Correcting line by line takes badly-placed rectangles +from about one in five to about one in nineteen, and letting each line tilt is what does most of +that, which is the specific shape the typographic argument predicts: not a constant offset a +line, but an error that accumulates *along* it. §⑦ carries the figures and the controls. + +**And the standard fix is local rather than global, which is well-trodden ground.** Thin-plate +splines pair a global part with a local warp +([Chui & Rangarajan](https://www.cise.ufl.edu/~anand/pdf/rangarajan_cviu_si_final.pdf)); +[non-rigid registration](https://www.sciencedirect.com/topics/computer-science/nonrigid-registration) +generally treats a good global fit as the *prerequisite* for a local stage rather than as the +answer. For documents specifically, ICDAR 2024's +[coarse-to-fine document image registration](https://dl.acm.org/doi/10.1007/978-3-031-70546-5_20) +learns a global transform and then a local one, and there is a patent family on +[line-based registration for transferring annotations between images](https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/10503868) +— *annotation transfer*, which is exactly what we do with mark rectangles. + +**Where we looked and found nothing — said plainly, because a search that came back empty is a +different thing from a search nobody ran.** We found nobody publishing on our actual case: +registering two independently typeset editions of the *same* text at glyph level, where both +sides are vector outlines with no pixels and no recognition involved. The document work we found +is about photographs of paper, where the distortion is physical. Nor did we find a published +version of the inversion above — *fit on the text, validate on the landmarks* — which follows +straightforwardly from Fitzpatrick but which nobody appears to have written up as a recipe. +Searched: non-rigid document registration and piecewise text-line alignment; Fitzpatrick's +result; document registration and cross-image annotation transfer; justification, line breaking +and glyph-metric differences between two renderings. **Not searched:** Arabic-script-specific +typesetting corpora, and the Qur'anic-computing literature — either could hold something, and +neither was opened. + +### How a box should be scored — the part with no precedent **Object detection has a conventional threshold, and it was set for the opposite reason.** The familiar 0.5 overlap comes from the PASCAL VOC challenge, whose own paper says why: *"The @@ -521,33 +695,118 @@ touches the font. ## ⑦ What are the options? -Every option below was measured on the same 4,000 marks, so the numbers are comparable. +Every option below is measured the same way, by the same instrument, on the same marks — and +on marks it was **not allowed to see while it was being worked out**. That last part is what +makes these numbers comparable at all, and it is worth one plain sentence before the table. + +A correction is worked out by looking at where the marks actually sit and moving the rectangles +to match. Grade it on those same marks and it will always look better, including when it has +learnt nothing and simply memorised them — and the more elaborate the correction, the more +flattering that is. So each page's marks are split down the middle at random: the correction is +worked out from one half and graded on the other half, which it never saw. A correction that +only memorises does well on the half it was given and no better than the simpler ones on the +half it was not. Everything below is the half it was not. + +The numbers: 120 pages, about 63,600 marks, half of them held back — so every figure in the +table is 31,700 marks the correction was blind to. + +| what it records | marks badly out | how much ink a rectangle covers | clears its control by | beats a deliberately wrong placement | +|---|---|---|---|---| +| A · nothing | 96.63% | 0.015 | **−0.234** — fails | 13.4% | +| B · one move per page | 18.38% | 0.494 | +0.282 | 76.2% | +| E · one move per printed line | 11.77% | 0.584 | +0.383 | 83.4% | +| **F · a printed line allowed to tilt** | **5.24%** | **0.681** | **+0.491** | **90.6%** | + +*Badly out* means further than 0.75 units from the mark's own ink, on marks about 5.6 wide and +3.6 tall. *How much ink a rectangle covers* runs 0 to 1 and cannot exceed **0.909** for these +rectangles even when they are placed perfectly, so read 0.681 against 0.909 rather than against +1. *Clears its control* is the gap between a rectangle and a deliberately wrong one; below 0.25 +nothing else in the row means anything. + +**The same split run over the whole book gives the same answer, and it is worth saying that it +did.** The table above is 120 pages measured by the scorer in full, which is where the last two +columns come from — ink overlap and control separation both need a re-score against the page's +own ink at the corrected position, and no table of displacements can reconstruct them. The first +column can be reconstructed, so it was, over all 604 pages and 326,515 marks on the identical +half-and-half split: **96.44% → 18.20% → 11.84% → 4.95%**, against 96.63 → 18.38 → 11.77 → 5.24 +here. Four independent rungs, five times the pages, and nothing moves by a third of a point. +Those are the figures the drawn page carries, because it can carry all 604 pages honestly and +this table cannot; the two are not in conflict, they are the same measurement at two widths. + +**Option I is a fifth rung on that reconstruction and deliberately not a fifth row in the table +above.** It was worked out after the 120-page scoring run and only the first column can be +reconstructed without re-scoring every page against its own ink, so what is known about it is +**4.12%** badly out on the whole book, on the same held-back half — and the other three columns +are honestly blank rather than estimated. Adding a row of dashes to a table a reader scans +downward would read as a worse score than F rather than as an unrun measurement, so it is stated +here in a sentence instead. The Option I section below carries the rest. + +The options are lettered in the order they were first written, not in order of preference — the +table above is the order of preference, and the recommendation moved. + +**These options are also drawn, and the drawing is the thing to send anybody who has to choose.** +Six of the nine — A, B, F, G, I and H — are rendered on a real page of the mus'haf at the size a +phone actually draws it, each one beside the printer's own ink, because a displacement of one +unit is a number here and a picture there, and only one of those settles anything. The page is +[`mark-placement.html`](mark-placement.html), rebuilt by +[`scripts/build-placement-options.mjs`](../../scripts/build-placement-options.mjs), and published +for a reader with no repository at + (an earlier copy on another host: ). C and D are not drawn +because there is nothing in them to look at: one is a stretch too small to see and the other puts +these same rectangles on the page and forbids using them. E appears in the drawn page's table of +numbers but has no picture of its own, because F is E with one thing added and drawing both would +ask a reader to spot the difference between two nearly identical pages. The same letters mean the +same options in both places, though the drawn page puts I next to F rather than last, because the +two differ by one thing and seeing that difference is the whole reason I is drawn at all; the +register row is `mark-placement` in +[`docs/decisions.json`](../decisions.json). ### Option A — change nothing, and do not draw marks The rectangles stay as they are and no feature is built on them. Honest, and it costs nothing today. -Measured consequence: the mark layer is permanently unusable for pointing at anything. 97.80% +Measured consequence: the mark layer is permanently unusable for pointing at anything. 96.63% of rectangles are further than 0.75 units from where their own ink is; the median rectangle -misses its mark's ink almost entirely (overlap 0.011); and a rectangle picked at random from +misses its mark's ink almost entirely (overlap 0.015); and a rectangle picked at random from elsewhere on the page describes the ink *better* than the right one does. -### Option B — record one displacement per page, measured from the ink · **recommended** +### Option B — record one displacement per page, measured from the ink · **superseded by F** Each page's fit gains a small correction, derived as the median displacement of that page's marks and recorded alongside the four numbers already stored per page. It is written down once, exactly like the fit itself, and read thereafter — so the existing rule against re-fitting at call sites is respected rather than broken. -Measured consequence — the second column of §④'s table. Blank rectangles fall from 1.15% to -**0.10%**; badly-placed rectangles from 97.80% to **18.27%**; the median overlap with the ink -rises from 0.011 to 0.507; and the measure clears its own control by 0.287 where it previously -failed it by 0.242. Two numbers per page, both derived offline from committed bytes, and the -derivation is reproducible byte-for-byte. - -Cost: the correction is derived from the ink, so it must be re-derived if either printing -changes — which is what a gate is for, and §⑩ ② is that gate. +Measured consequence: badly-placed rectangles fall from 96.63% to **18.38%**, blank ones to +0.08%, the median overlap rises from 0.015 to 0.494, and the measure clears its own control by +0.282 where it previously failed it by 0.234. Two numbers per page, both derived offline from +committed bytes, and the derivation is reproducible byte-for-byte. + +**Why this is no longer the recommendation, and it is not a close call.** Two things were +learned after it was written, and each on its own would be enough. + +The first is that **it fails the threshold this very section proposes for it**. Read down to +"What thresholds would go with this" and the number is *no more than 2% of rectangles further +than 0.75 units*. This option delivers 18.38%, and on the very same pages it delivers 18.12% on +the marks it was worked out from — so this was true the day it was written and nobody put the +two figures side by side. One rectangle in five badly out is not a threshold being narrowly +missed; it is a model the threshold refuses outright. Those two figures are worth keeping +together for a second reason: a quarter of a percentage point between grading a model on its own +marks and grading it on marks it never saw is what *no overfitting at all* looks like, which is +the baseline the finer options have to be read against. + +The second is that **the family this option belongs to is exhausted**, which §④ measures. Every +option that moves a whole page as one rigid thing — whether by shifting it, stretching it, or +re-deriving it from scratch — leaves about 0.44 units of scatter across the page and 0.34 down, +because the two printings do not disagree about the page *as a page*. They disagree about where +each printed line of text sits. No amount of care spent on a per-page number reaches inside +that, which is why two sessions of careful measurement moved the figure and never the problem. + +Cost, if it were adopted anyway: the correction is derived from the ink, so it must be +re-derived if either printing changes — which is what a gate is for, and §⑩ ② is that gate. +That cost is unchanged for E and F below; it is a property of correcting from the ink at all, +not of the grain. ### Option C — re-derive the whole fit from the ink, scale and all @@ -569,7 +828,217 @@ never draw a rectangle. Measured consequence: everything in §④ becomes irrelevant, and so does the feature. The whole reason to hold 326,515 rectangles is to point at one. This is Option A with extra steps. -### What thresholds would go with Option B, and are they enforced? +### Option E — record one displacement per printed line + +Each printed line of the page gets its own small move, recorded on top of its page's. A page +holds fifteen lines, so this is thirty numbers a page rather than two, all of them derived the +same way and written down once exactly as the page's own move is. + +**Why a line, and not something else.** This is not our own numbers suggesting it — it was +predicted before they were looked at, and by an argument that has nothing to do with this +project. Two printings of one text disagree because their letters are set to slightly different +widths. Widths accumulate *along* a line and start over at the next one, so error that grows +across a line and resets at the beginning of the following one is exactly the signature two +independent settings of the same text produce. §⑤ carries that literature. The line is also the +only unit here that is both meaningful and populous enough to measure: a printed line carries +about 36 marks — thirty at the sparsest, forty-three at the densest — and a move worked out from +36 observations is a measurement, where a move worked out from the 3.7 marks on a word would be +a fit to noise wearing the costume of a correction. + +Measured consequence: badly-placed rectangles fall from 18.38% to **11.77%**, overlap rises +0.494 → 0.584, the control gap widens 0.282 → 0.383, and the share of rectangles beating a +deliberately wrong placement goes 76.2% → 83.4%. The tail moves too, which is the part a median +hides: the 95th-percentile miss drops from 1.417 units to 1.008. + +Cost: the correction no longer fits in the four numbers a page already carries, so it needs a +small table of its own beside them, and everything that places a rectangle has to read it. That +is the real price of E and F both, and it is what §⑩ ⑨ is holding open. + +### Option F — record a printed line that is allowed to tilt · **recommended** + +The same as E, except that a line's move is permitted to change gradually from one end of the +line to the other rather than being one number for the whole line. Four numbers a line instead +of two. + +This is the same typographic prediction as E, taken one step further and to its actual claim. +If the disagreement really is letters set to different widths, then it does not merely differ +between lines — it *grows along* each one, so the far end of a line is further out than the +near end. E can only record where a line sits on average. F records the growth, which is the +thing the mechanism actually predicts, and it is why F was written down before it was measured +rather than being found by trying things. + +Measured consequence, and it is the largest single step on the whole ladder: badly-placed +rectangles fall from 11.77% to **5.24%**, overlap 0.584 → **0.681** against a ceiling of 0.909, +the control gap 0.383 → **0.491**, and 90.6% of rectangles now beat a deliberately wrong +placement. The typical miss is 0.225 units — well under a tenth of a mark's width — and the +95th-percentile miss is 0.755, meaning nineteen rectangles in twenty now land inside the +distance §⑦'s own error budget allows. + +**The control that makes this believable.** A model with sixty numbers a page can flatter itself +in a way a two-number one cannot, so it was given a test designed to catch exactly that: every +line was made to wear some *other* line's correction, at random. If the per-line numbers were +absorbed noise, wearing the wrong one would be about as good as wearing the right one. Instead +the shuffle scores **31.2% badly out** — far worse than doing nothing per-line at all, and six +times worse than F. A line's correction is a fact about that line. + +Cost, and one honest reservation. The table is twice the size of E's, and a line with too few +marks to fit a tilt has to fall back to its line's plain move and then to its page's, so what +ships is a small ladder rather than one number — which is more machinery to get wrong. And the +5.24% left over is **not** simply the tail of a tight scatter: a bell curve of the same width +would leave about 3.1%, so there is a minority of rectangles still badly out for some reason of +their own rather than every rectangle being slightly out. That difference is a real finding and +it is deliberately not averaged into the headline: it says a further question exists, and it is +not the question this section is answering. §⑩ ⑨ carries it. + +That question has since been asked, and half of it has an answer: the rectangles F leaves badly +out are concentrated on whole printed lines that have gone wrong together, and a line can go +wrong in a way a steady change from end to end cannot follow. Option I is that finding turned +into an option. The other half is still open — a rectangle at either *end* of a printed line +remains about twice as likely to be badly out as one in the middle, under F and under I alike, +and nothing here explains why. + +### Option G — the same, applied to the marks and not to the words + +The correction from F, carried onto the mark rectangles only. Word rectangles stay exactly where +they ship today. + +This is a separate question wearing the same letter-shaped clothes, and it is written down as an +option because it was quietly being assumed rather than chosen. The four numbers this whole +document is about do not place marks; they place *everything on the page*, word rectangles +included — a word box is derived from the same fit, in the same file, by the same line of +arithmetic. So a correction that moves the marks and not the words is not the cheaper half of F. +It is a decision that the two layers may drift apart, made once and then true forever. + +Measured consequence: **identical to F for every number in the table above**, because every +number in the table is measured on marks. That is exactly what makes this option dangerous to +compare — it costs nothing by the only measure on this page, and everything it costs is +somewhere the page does not look. + +What it costs is elsewhere and is real: a word rectangle is what a reader's finger actually lands +on when they pick out words, so leaving it on a fit we have now measured as about one unit wrong +is knowingly shipping the error we just spent this document establishing, in the one layer a +finger touches. Against that, a word box is roughly 22 units wide against a mark's 5.6, so the +same absolute error is a far smaller fraction of it — which is an argument about priority, not +about correctness. + +The reason to draw it rather than argue it: on the page, G and F are the same picture except for +the word rectangles, and whether that difference matters is a thing to look at rather than a +thing to reason about. + +Cost: two geometries fitted from one set of measurements and free to disagree, which needs a gate +holding them to each other or it rots silently — and rebuilding the word shards is the work F +requires anyway, so choosing G to avoid a rebuild does not avoid it. + +### Option H — put each mark where its own ink is, and line up the rest + +Instead of working out a move for a page or a line and applying it to every rectangle inside it, +look at each mark individually, find where its own printed strokes actually are, and write that +down. A mark whose strokes cannot be found convincingly does not get this treatment; it inherits +its printed line's move from F. Word rectangles and verse-end circles have no such measurement of +their own, so they inherit F as well. + +**This is the idea §⑧ opens by refusing, brought back with two refusals built into it**, and the +refusals are the option. A search is trusted only where it has evidence. It is not trusted where +the best match it found was poor — there it is chasing noise rather than finding a mark — and it +is not trusted where the answer sits on the very edge of how far the search was allowed to look, +because that is a search which has run out of room rather than one which has found something. +Over the whole book those two refusals hand back **1,877 rectangles, 0.57% of the mus'haf** — +1,210 poor matches and 730 that ran out of room — and accept **324,638, or 99.43%**. + +**What killed the original objection, and it is worth reading rather than taking on trust.** The +first version of §⑧ refused this idea partly on the grounds that adjacent marks sit about as far +apart as the error is large, so a search for the nearest ink would sometimes settle neatly onto +the *neighbour's* — manufacturing exactly the complaint that started all of this. That was a +guess, it was measured, and it was wrong by an order of magnitude. The nearest mark of any name +is a median **8.32 units** away and the nearest one that looks the same is **24.80**, against a +search reaching 3 units and an error near 1. Across all 326,515 marks the search lands nearer +some other mark 1.08% of the time and nearer an identical-looking one **0.014%** — 47 marks in +the whole mus'haf. + +Measured consequence, and the shape of it matters more than the number. Counting only the marks +this option refuses to place from ink — the ones it hands to F — **0.42%** of the whole book is +badly out, against F's 4.95%. On the page drawn throughout the options record it is **zero**. + +**And that number cannot be read the way the others can.** Every other option in this section is +graded by holding half of each page's marks back and marking the correction on marks it never +saw. This one has nothing to hold back: each mark carries its own two numbers, so there is no +second half of anything, and the 99.43% it accepts score zero by construction — a zero that +carries no information whatever, because the thing being graded and the thing grading it are the +same measurement. That is why it is not in the table above and why its 0.42% is stated as *the +part of it that is a model*. This is the trade the option asks for: far fewer rectangles out of +place, and no way for this instrument to prove it. + +Cost, and it is the largest on the page. Two numbers for every mark in the mus'haf — a table +bigger than the mark data it corrects, expressible in no version of the small per-page table the +app reads today, and needing a file of its own beside it. It records rather than explains, so it +says nothing at all about a printing nobody has measured, which is the one thing a per-line model +does offer. And the word rectangles still move, are still rebuilt and still re-checked, exactly +as under F — choosing H does not avoid that work. + +What it needs before it could ship is not another measurement. It is a person looking at a sample +of the rectangles it places and saying whether they sit right, because that is the only witness +that is not made of the same ink the option was measured against. §⑩ ① is the sitting; §⑩ ⑩ is +the question of what the ones it hands back actually are. + +That sitting now exists, in two halves, and it is worth being exact about what each half can and +cannot buy. Sixty rectangles are drawn from the 99.43% this option places and sixty from the +0.57% it hands back, and they are read apart and never pooled — the two populations differ in +size by a factor of 173, so a single rate over both would be a fact about how many of each the +sample happened to contain. The reader answers in words rather than distances, and passing a +rectangle is itself the answer that nothing is wrong with it, so the rate is faults over +rectangles-looked-at and the page keeps that count for exactly that reason. **A clean sixty is +not a zero.** Sixty rectangles with no fault in them bound the failure rate at about one in +twenty, which over 324,638 marks is up to sixteen thousand of them — so the sitting can say *not +more than this* and can never say *none*, and the interval rather than the percentage is the +finding. What it can do, and nothing else here can, is notice a fault that is confidently +identical everywhere: three or four faults in sixty is a few per cent of almost the whole book, +which is a statement about the mus'haf, where the same share in the tail is a rounding error. + +### Option I — record a printed line that is allowed to bend + +The same as F, except that a line's move may bend along the line rather than only grow at a +steady rate from one end to the other. Six numbers a line instead of four. + +**Where it came from matters, because it is the one option on this page that was not reasoned +out in advance.** F was written down before it was measured, from a claim about how the two +printings set their letters. I was not: it came from asking what F's leftover rectangles have in +common, and the answer was each other. Of the marks F still leaves badly out, **54%** sit on the +one printed line in fifteen that has gone wrong as a whole — 590 lines out of 8,817 — rather than +being spread evenly over the book. Lines fail together, and a line that fails does so in a shape +a straight change from end to end cannot follow. So this option is a fit to an observation, and +that is a weaker kind of reason than F has. What makes it believable is not the story but the +half of the marks it was never shown. + +Measured consequence, on the whole book and on the held-back half: badly out **4.95% → 4.12%**, +and the typical miss **0.224 → 0.198** units, over 326,515 marks and 8,702 printed lines. The +other columns of §⑦'s table are not filled in for this option, for the reason given above it. + +**The control, run again because a bend has more room to flatter itself than a tilt does.** Every +line was made to wear some other line's bend, at random. That scores **34.40%** badly out — +worse than wearing another line's tilt, and far worse than the 18.20% for no per-line correction +at all. A line's bend is a fact about that line, not absorbed noise. + +**And the rung above it was refused, which is how a ladder is meant to end.** A shape freer still, +allowed to change direction twice along a line rather than bend once, was fitted the same way and +rejected: better on the marks it was fitted from and worse on the marks it was not, which is what +an allowance with nothing left to find looks like. Each extra freedom also raises the number of +marks a line needs before it may use it, so the freer shape abandons the short lines, which are +already the ones going worst. That comparison was made off this ladder and is **not reproducible +from what is checked in**; the four rungs above are. + +Cost: half as much table again as F, six numbers a printed line rather than four, and a deeper +fallback ladder — a line too thin to carry a bend takes its tilt, then its plain move, then its +page's. Everything F disturbs, I disturbs identically: the word rectangles move too, are rebuilt, +and are re-checked. Nothing new has to be learnt to read it, because it is the same table shape +with more numbers in each row. + +The honest reservation: **it fixes the middle of a line and barely touches the ends.** The middle +goes from 4.34% badly out to 2.94%, while the two ends go from 6.58% and 6.95% to 5.97% and +6.02%. So the concentration this option was built from is only half accounted for, and the half +left standing is the half this whole enquiry started from. That is left as an open question in +§⑩ ⑨ rather than chased with a seventh rung nobody can justify in advance. + +### What thresholds would go with the recommended option, and are they enforced? Proposed, and deliberately **not enforced anywhere** — not in the build, not in continuous integration, not in any gate: @@ -583,6 +1052,47 @@ integration, not in any gate: - No more than **0.5%** of rectangles essentially blank, against a measured 0.10% after correction and two independent measurements agreeing on roughly 2% before it. +**The second of those three is not met by any option this instrument can grade, and that has to +be said before anything else about it.** The recommended option leaves 5.24% beyond 0.75 units +against a threshold of 2%. The blank threshold is met with room to spare (0.03% against 0.5%) and +so is the control threshold (0.491 against 0.25); it is the middle one, and only the middle one, +that refuses every option including the recommended one. + +**Option H appears to clear it and that appearance is exactly what the threshold cannot check.** +Its attributable figure is 0.42%, comfortably inside 2% — but the 99.43% of rectangles it places +directly are not being tested by anything, because the number the threshold measures against is +the number H ships. A threshold is a claim that something independent would agree; here nothing +independent has been asked. So the honest statement is that H is untested against this threshold +rather than that it passes it, and what would change that is a person looking, not a re-run. + +**Somebody has now looked at sixty of them, and it moves the statement without settling it.** +Sixty crops drawn reproducibly from the marks H places directly, sat in one go on 2026-08-14: +every one explicitly vouched for, no fault of any kind reported, no printing oddity raised. Read +strictly, sixty clean answers put an upper bound of about **5%** on how often a reader would find +something wrong there — not zero, because sixty is sixty. Read honestly, it says less than that, +and the caveat has to travel with the number wherever it goes: those sixty are drawn from the +population *defined by* a match of 0.55 or better and a displacement under three units on a mark +roughly 5.6 by 3.6, so a gross error was structurally impossible on those cards before the reader +arrived. Sixty clean answers from a population that cannot contain the fault being looked for is +weak evidence about the fault and strong evidence about something else: that the instrument works +and a reader can sit it. The threshold stays untested. What tests it is the other population, the +one the correction refuses and falls back for, and all 1,877 of those are now dealt out to be +seen rather than sampled. + +There is an obvious and dishonest way out, which is to notice that the 0.75 came from a budget +whose first term was *the scatter left after the fit* — 0.46 units under a per-page model — and +that the recommended option leaves 0.34, so the budget recomputes to about 0.50 and the +threshold could be re-derived and then re-declared met. That would be moving the target to +where the arrow landed, and doing it inside a document whose whole purpose is to be checkable +by somebody who was not here. So: the threshold as written stands, it is not met, and +re-deriving it is a decision that belongs to whoever adopts a correction — made in the open, +with the old number and the new one both visible, and not folded into this paragraph. + +What the recommended option can say for itself against that threshold is narrower and true: +nineteen rectangles in twenty land within 0.755 units, so the budget describes the *typical* +rectangle accurately and the argument is entirely about how long a tail is tolerable when there +are 326,515 of them. That is a judgement, and §⑩ ① is where a person makes it. + The measurement exits with a failure code when a threshold is breached, so that it *could* become an enforced check later. It is not one today, and making it one has a named prerequisite: somebody has to look, under conditions where looking can actually settle it. @@ -601,6 +1111,73 @@ answer releases these thresholds; only that one can change them. ## ⑧ What else could be considered, and why is it not here? +**Unguarded per-mark local search — the version of option H that was excluded, and why the +guarded one is not.** This entry was written first as a three-part refusal of shipping +`bestPlacement`'s own output. One of the three parts was an assertion that had never been +measured, and measuring it killed it. The refusal is left here rather than deleted, because the +part that died is the reason option H exists at all. + +*It is unfalsifiable — this part stands.* Those per-mark displacements are the reference every +figure in §⑦ is scored against: the residual under a correction is `(dx, dy)` minus what the +correction proposes. Ship them and the residual is identically zero, `far` is 0%, and no +split-half, null control or held-out page can say otherwise, because the grader and the candidate +are the same object. Split-half is not merely inconvenient here, it is structurally impossible — +each mark carries one observation and the model spends two parameters on it, so there is no +half to hold back. That does **not** make the option wrong; it makes it one this instrument +cannot rule on, which is a statement about the instrument. The check it needs is §⑩ ⑨. + +*It manufactures the wrong-mark symptom — this part was false.* The claim was that adjacent marks +sit roughly a unit apart while the error is roughly a unit, so a search for the nearest ink would +often find the neighbour's and centre a rectangle on the wrong mark. The first half was a guess and +it was wrong by an order of magnitude. Measured over all 326,515 marks, per page, centre to centre: +the nearest mark of *any* name is a median **8.32** units away and the nearest one bearing the +**same** name is **24.80**, against a search that reaches 3 units and an error near 1. Counting +landings that end up closer to another mark's centre than to their own: **1.08%** for any +neighbour and **0.014%** — 47 marks in the whole mus'haf — for one that would look identical. The +symptom the 2026-08-12 sitting reported is displacement being misread, exactly as §④ concluded; it +is not this. `neighbour` in `mark-placement.data.json` carries the figures, computed in the same +pass as everything else on the options page. + +*It is not a model — this part stands, and is a cost rather than a disqualification.* A per-page +translation is two numbers, `line-tilt` about sixty a page. This is 326,515 pairs, a table larger +than the mark data it corrects, expressible in no version of `word-boxes.pin.json`, and silent +about every page the probe never sampled. It says nothing about *why* the prints differ, so it +cannot generalise, which is the whole point of §④'s finding that the difference is systematic. It +is re-derivable — the probe is deterministic and the correction rungs are fitted from the same run +— but re-deriving it is not the same as predicting anything. + +What the measurement *did* refuse is narrower than the original entry and sharper. The search is +trustworthy exactly where it has evidence, and two populations have none: + +- **Weak matches.** Grouped by `iouBest`, the search's departure from `line-tilt`'s answer is a + median 1.79 / 2.77 / 1.72 units below 0.15 / 0.15–0.35 / 0.35–0.55, throwing 44.6% / 71.7% / + 44.3% of marks past 2 units. At `iouBest ≥ 0.55` — 325,305 marks, 99.63% — the median departure + is **0.21** and only **0.2%** go past 2. Below that threshold the search is chasing noise. +- **Clamped searches.** 730 marks (0.22%) come back with a component sitting exactly on the ±3-unit + edge of `bestPlacement`'s window. Their `dx, dy` are not a finding about ink, they are a fact + about the window, and they are a caveat on those marks' figures in §⑦ since their *grading + target* is clamped. At this scale it is a rounding error rather than an argument for a wider + `RADIUS`, but the wider run is cheap and settles it. + + **The first version of this bullet said 9,569 marks, and the arithmetic behind it was wrong.** + It asked whether the straight-line distance `hypot(dx, dy)` exceeded the radius. The search + slides the outline over a *square* window — ±3 in each axis, independently — so that test draws + a circle inside the square and condemns every corner of it. It discarded **8,358 marks whose + median overlap was 0.905 against an achievable 0.909**: placements sitting essentially perfectly + on their ink, thrown out under a label saying the search had run out of room when it had not + come near its edge. Distance travelled is not evidence of a bad placement. The test is per axis + now, and the error is left written down here because every H figure published before this + correction was built on it. + +Option H is this idea with those two refusals built in: place from ink where `iouBest ≥ 0.55` and +neither component of the search's answer sits on its boundary, inherit `line-tilt` otherwise. That +accepts **324,638 marks (99.43%)** from direct evidence and hands back **1,877 (0.57%)** — 1,210 +weak matches and 730 clamped. The fallback set scores 73.2% badly out under `line-tilt`, far worse +than the corpus, which is what a set selected for having no usable ink evidence should look like. +Since the accepted marks' zero carries no information, the only figure honestly attributable to H +is that fallback share of the whole book: **0.42%**, against `line-tilt`'s 4.95%. On page 179, the +page drawn throughout the options record, it is **zero**. + **A borrowed overlap threshold.** The obvious method: rasterise, compute overlap, fail below 0.5. Rejected for the two reasons in §⑤ — the conventional threshold was set to *hide* label error rather than find it, and overlap collapses under sub-pixel displacement at this object @@ -739,13 +1316,47 @@ named so somebody can go and look at those boxes, and the headline's denominator A run of them is a finding about extent, and it belongs in §④ with the residuals rather than here. +One thing about it changed after §⑧, and it changed for a reason that had nothing to do with +the question and everything to do with which pages could be asked about. A trial cannot be +built for a page with no proposed move, so while the measurement covered forty pages, every +question this session could put came from one of the forty the correction had been fitted to. +A hundred trials would have bought a very precise answer about the pages the correction was +made from, and the correction is going to ship on all six hundred and four. Now the trials are +drawn from forty pages the fit has never seen, spread evenly through the print rather than +taken from its extremes — the placing session wants the extremes because it is measuring how +far, and this one is only ever asking which of two is closer, so what it has to lose by +bunching at the ends is being about the mus'haf at all. + +**What "the corrected rectangle" means has moved, and it moves two things here.** When this +section was written there was one candidate, and it moved every mark on a page by the same +amount. The recommended one now moves each printed line by its own amount, and the two +consequences pull in opposite directions. The first is unwelcome: the difference a reader is +being asked to see is now typically about a fifth of a unit rather than two fifths, which is +half the size, on a mark under four units tall — so the ceiling this session measures with its +same-distance decoys stops being a formality and becomes the number the headline lives or dies +by. The second is a gift, and it arrived by accident: the old worry that a reader would learn +the rule within twenty trials rested on the error pointing the same way on every page, and it +does not point the same way on every *line*. A learned rule is worth much less against a +correction that changes direction down the page, which means the decoys are doing less work +holding the session honest and more work measuring what an eye can actually resolve. + +Neither of those is a reason to sit it sooner. It is the opposite: a session built against the +per-page move would be measuring a candidate nobody intends to ship, at a difference twice the +size of the real one, and would come back more favourable than the truth. Whatever ships gets +built into the trials first. + The runbook is `placement-correction-by-eye` in [`docs/validation/ledger.json`](../validation/ledger.json); it takes about twenty minutes and -needs no mushaf, no phone and no network. +needs no mushaf, no phone and no network. If the same person is going to sit the placing +session as well, this one goes first: dragging rectangles onto marks for twenty minutes is the +most efficient way there is to learn where our correction tends to sit, and a reader who has +learned it is answering these hundred trials from the rule rather than from the ink. The +reverse order costs nothing, because knowing which of two rectangles you preferred does not +tell you where to drag anything. ### ② Is there anything stopping the correction going stale · **open** -If Option B is adopted, the correction is data derived from two printings, and nothing +If any correction in §⑦ is adopted, it is data derived from two printings, and nothing re-derives it or checks it. That is the same shape of problem as the fit residual in §④ — a recorded number nobody re-validates — and it deserves the same answer a gate gives everything else here: re-derive offline from committed bytes and fail on drift. @@ -793,8 +1404,8 @@ displaced in a random direction, and the reader drags it onto the mark. Nothing says where we think it goes — the proposed move is stripped out before the trials reach the page — so the landing is a placement rather than an agreement. Subtracting our move from it leaves the **residual**: a distance, in page units, that would put our rectangles where a -reader puts them. That is a number Option B can be edited by, where ① can only say whether to -adopt it at all. +reader puts them. That is a number a correction can be edited by, where ① can only say whether +to adopt it at all. A hand-measured number is worth nothing without the hand's own noise measured beside it. A residual of a fifth of a unit is a finding if the reader is repeatable to a fiftieth and it is @@ -814,14 +1425,1473 @@ teaches the correction's direction — it points the same way on almost every pa twenty trials. For the same reason the two sessions should not be worked back to back by the same person. +A third property was learned the hard way, after the first sitting. A crop of print this size +holds several marks and often two of the same name, so a trial that only says "put the box on +the mark" is a trial about whichever mark the reader picked. That is not a small error: a +placement on the neighbouring mark is a whole letter out, and the residual would record a +whole letter of registration error that has nothing to do with registration. So each trial now +names its mark — the name, the letters it belongs to, and, when those letters carry more than +one of that name, which of them counting from the right — and it points at those letters in +the picture. + +The first attempt at pointing was a mistake worth writing down, because it would have quietly +destroyed the measurement rather than breaking it. The letters were drawn crisply, in colour, +over the print. But the crisp letters come from the other printing's drawing, carried onto our +frame by exactly the fit that places the rectangle — so the visible gap between those letters +and the ink underneath them *is* the correction being measured, about a page unit, which at +the size these panels are worked is a finger's width on screen. A reader would have spent an +hour looking at the answer and their landings would have been a tracing of it. The fix is to +point softly: the letters sit under a wide blurred wash whose edge is a gradient several times +wider than the correction, and which looks the same whether the fit is right or a unit out. It +says *these letters* and refuses to say anything finer. The mark itself is never washed and +never outlined. + If ① and ⑦ disagree, that is the most valuable outcome either can produce and neither can produce alone: two instruments looking at the same rectangles from opposite directions, one of them wrong, and a reason to re-measure before three hundred thousand rectangles move. +**What a sitting said**, recorded in full in +[`docs/validation/rulings/2026-08-12T1650-placement-residual-by-hand.seed23.json`](../validation/rulings/2026-08-12T1650-placement-residual-by-hand.seed23.json). +Sixty marks, placed by one reader in about seven seconds each. +Fifty-nine of the sixty landed nearer the corrected rectangle than the one the app draws +today — 98%, and the interval around it reaches down only to 91%, so it clears half by a +wide margin and **the correction points the right way**. The typical miss from today's +rectangle was 1.15 page units and from the corrected one 0.39, which is the same statement in +a second form. Nine marks came round twice from independent starting points and the two +landings agreed to 0.03 units, so that is the floor everything else is read against. + +What is left over once our move is subtracted is 0.07 units left and 0.11 units up — about +four times that floor, so it is bigger than the hand's own wobble. That was first read as +*therefore real*, and it is not, for a reason that took a second look to see. The sixty +placements sit on forty pages, and two marks on one page are not two facts about the fit: they +share that page's frame, so whatever is wrong with it is wrong for both of them, in the same +direction, by nearly the same amount. Counted as forty pages rather than as sixty independent +trials, the vertical part runs from 0.23 units up to 0.01 units *down* — it no longer excludes +nothing. It was marginal before that correction and it is not established after it. Most of +the range still sits on one side, so this says our correction **may** be short; it does not +say that it is. + +Two further limits the sitting could not see past, neither of which appeared anywhere in its +own numbers until now: + +- **It was asked on its own pages.** A trial needs a proposed move to start from, and moves + had been measured for only forty of the mus'haf's 604 pages — so every mark placed came from + a page the correction had already been fitted to. Nothing here says whether it holds on a + page nobody has measured. That is §⑧, below. +- **The size was never measurable.** Across those forty pages the proposed move barely varies, + and with that little to compare against, "exactly right" and "a fifth short" produce the same + landings. The sitting's estimate of the correction's *scale* spans from the wrong sign to + two and a half times too much. More trials would not fix it: the limit is the forty pages, + not the sixty answers. **That limit has since been lifted** — the whole mus'haf has now been + measured, and it holds pages whose corrections differ far more widely than those forty did. + §⑧ says what that changes and how a sitting would have to be drawn to use it. + +Three explanations were tested and none of them survived, which is worth not buying a second +time. It is **not the mark**: a separate number per mark name leaves more spread than one +number for all of them, over eleven names, so the rectangle is not anchored wrong inside +particular shapes. It is **not a stretch**: what is left over does not depend on where the mark +sits on the page, so the fit is out by a shift and not by a scale. And it is **not the starting +point** and **not fatigue**: the pull back toward wherever each rectangle started came out at +effectively nil, which is what the evenly-spread starting positions were for, and nothing +drifted as the sitting wore on. + +So the answer to this section's question is: **the direction is settled and the distance is +not.** Adopt the correction as measured; do not apply what is left over on top of it. A tenth +of a unit spread over three hundred thousand rectangles is a move that would have to be bought +again the first time anybody asked how it was known. What would settle it is a second sitting +on pages the correction was *not* fitted to, several marks on each page so the page and the +hand can be told apart, and a second reader — whose disagreement by more than 0.03 units would +mean this is a fact about a person and not about the print. **Every page that sitting needs now +exists**, which was not true when this section was first written; what it still needs is +somebody's half hour. + The runbook is `placement-residual-by-hand` in [`docs/validation/ledger.json`](../validation/ledger.json); about twenty-five minutes, and it needs no mushaf, no phone and no network either. +**What has changed since, and it changes what this sitting is for.** The sitting's own summary +was *the direction is settled and the distance is not*, and it tested three explanations for +the leftover distance — the mark's name, a stretch across the page, and the reader's starting +point — and rejected all three. It could not test a fourth, because a sitting with sixty marks +on forty pages has no way to look inside a page. The machine has since looked: the leftover is +organised by **printed line**, and correcting line by line removes most of it. So the sitting +was right that it is not the mark, not the reader and not a stretch of the whole page — and the +thing it was left with is a stretch of each *line*, which is both smaller than a page and larger +than a mark, and sits in the one place sixty trials could not reach. + +That does not retire this question; it sharpens what it is worth asking. Re-asking *is the +correction far enough* about a per-page move is now spending a reader's half hour on a model +nobody intends to ship. Asked about the correction that does ship, on pages it was never fitted +to, it is the only instrument here that is not made of ink at all — and the leftover it would +measure is now expected to be small, which makes the hand's own repeatability the thing that +decides whether the sitting can say anything. That floor was 0.03 units. Whatever ships should +be built into the trials before anybody sits them; the runbook says so in as many words. + +**And what the leftover is made of, which until now this section could only size.** A second +kind of sitting asks the other half of the question: not *how far out* but *what kind of +wrong*, one mark at a time, in words rather than in a distance. It is being sat over the marks +the machine could not place from their own ink — 1,877 of them, every one to be seen — and 160 +have been looked at so far. **158 came back carrying a complaint.** Named: 158 said the +rectangle is in the wrong place, 37 said it is also the wrong shape, 15 said the printed mark +itself is odd, and 2 said nothing was wrong. On the largest single sitting the share is 99.0% +with a 95% interval of 94.6% to 99.8%, over 101 marks once the ones that questioned the print +are held out. + +**Read that as a fact about the population before reading it as a fact about the print.** These +are by construction the marks nothing could place, so a near-total fault rate is what the set +was selected to produce and is not news. What *is* news is the composition. Only 15 of 160 were +called odd in the print, so the leftover here is overwhelmingly ours to fix rather than the +printer's — the opposite of the comfortable reading. And the direction agrees with the ink: +where the reader left those rectangles, measured from the box before any correction, runs about +3.3 units across and 2.6 units down in medians, which is the same way and roughly the same size +as the machine's own measurement, arrived at by an instrument made of nobody's arithmetic. + +**Two of the six things a reader can say were never said once**, across 564 answers: that the +rectangle is round the wrong ink, and that something else is wrong here. A word nobody uses is +usually a word that should go — but the first of those only became cheap to say in the same +week the sittings resumed, when tapping the ink became the way to say it, so this is a reading +to take again after the next few sittings rather than a licence to delete anything now. + +### ⑧ Does the correction hold on a page nobody measured · **open** + +When this question was written the per-page correction existed for **forty pages of 604**. It +had been measured by sampling pages and requiring enough marks on each to be worth fitting, and +forty was the number that sitting needed; the other 564 pages had no measured move at all, +while the app shipped rectangles on all of them. + +That was a gap in coverage, and §⑦ made it a gap in evidence too: because a trial cannot be +built for a page with no proposed move, every placement a reader has ever judged came from one +of those same forty. The correction had been checked, and only ever where it was fitted. +Nothing measured then distinguished *the print is displaced this way* from *these forty pages +are displaced this way*. + +The first half of the answer was arithmetic rather than judgement and needed nobody's time: +measure all 604. **That has now been done — 30,000 marks, 2026-08-12 — and the coverage half +of this question is closed.** Six hundred pages of 604 carry a measured correction. The four +that do not are pages 1, 2, 603 and 604, the short ornamental frames, which never carry enough +marks to be worth fitting; they need a stated fallback rather than a silent one, and the +average of everything else is the honest choice. + +**The corrections do vary, and the first forty pages hid it.** Two things came out of the full +pass, and they point in opposite directions. The forty were representative in the *middle* — +their average correction is indistinguishable from the other 560 — which is reassuring and was +not guaranteed. But they understated the *variation* by more than half, and worse, most of what +variation they did show was never real: measuring those same forty pages twice, from different +samples of their marks, disagrees by about as much as the pages differ from each other. What +looked like forty slightly different pages was largely the same page measured noisily forty +times, which is exactly why the sitting built on them could confirm the direction of the +correction and never its size. + +Across the whole mus'haf the real variation is around a tenth of a unit — the same size as the +leftover distance §⑦ could not settle, which is precisely why it could not be settled. And the +variation is **not the same on both axes**: nineteen pages in twenty need a downward move +inside the range the first forty spanned, so down behaves almost like a single number for the +whole print. Sideways it does not. Only about half of all pages fall inside the sideways range +the first forty covered, roughly nine pages need essentially **no** sideways move at all while +still needing the usual downward one, and one page — 113 — wants to move the *opposite* way +down from every other page in the book. + +**So the second sitting is worth someone's half hour, and it must not draw its marks at +random.** The first forty offered about a third of a unit of difference between the largest and +smallest move they proposed, nearly all of it noise, which is why the size came back +undecidable. Across all 604 that span is over one and a half units. A sitting that deliberately +takes marks from both ends of it gets several times the leverage per placement — enough to tell +a correction that is exactly right from one that is a fifth short, which no number of +placements on the original forty could ever have done. The handful of pages needing no sideways +move, and page 113, are worth the most of all: they are where *one number for the whole print* +and *a number per page* disagree most loudly, so they are where a reader's eye settles it +fastest. + +The second half is still open, and it is the half only a person can answer: a sitting on +**held-out** pages — marks from pages the correction was measured on but which no reader has +judged. Everything a reader has judged so far came from the pages the correction was fitted to. +That is the only thing that separates a fit from a finding. + +**That sitting now exists and nobody has sat it.** It is two short blocks. The first is +forty-seven rectangles from forty pages that were not among the original forty, drawn on purpose +from the pages wanting the largest and the smallest moves rather than at random — which is what +buys the leverage described above, and it does: across those forty pages the proposed moves +differ by nearly two units sideways and over three down, against a third of a unit before. The +second is twenty-three rectangles over five further pages, four or five on each, and it exists +because no sitting so far has ever put enough rectangles on one page to tell how much of the +leftover distance belongs to the page and how much to the hand holding the mouse. The five pages +are none of the forty, so the two blocks can disagree with each other. + +Neither block has the correction applied to it. Applying it first would have measured the +leftover distance on top of itself, where a genuine nought and a lucky cancellation look the +same — and it would have built a number this project has not settled into the instrument +meant to settle it. + +Two things about that sitting are worth saying plainly, because they are what make it evidence +rather than an exercise. **What it expects is written down before anybody places a rectangle**, +in the validation register, four statements about numbers that do not exist yet, each one saying +what its own failure would mean — a result read afterwards can always be made to sound like a +confirmation, and one predicted beforehand can fail. And **one hand cannot tell a print that is +off by this much from one reader who places rectangles this way.** Those two produce the +identical number, and nothing in a single sitting's output distinguishes them. + +**So there is a third page now, and a second person may sit it.** It is the first block again — +the same forty-seven rectangles, the same order, the same starting points — worked independently +by somebody else. Only whose hand it is changes, and that is the one thing about the sitting that +never reaches a trial: if the second person saw different marks, or the same marks from different +starting points, the two hands would not be comparable and the whole reason for asking a second +one would be gone. It is folded into the name of the file that comes back and into how a +half-finished sitting resumes, because two people working the same build would otherwise share +both, and the second would resume into the first one's answers. + +The two sittings are then read against each other rather than each on its own: how far the two +hands are from each other, mark by mark — never average against average, since two people a fifth +of a unit apart on every rectangle in alternating directions have identical averages and have +agreed about nothing — against how far each hand is from itself, which each of them measures from +the marks they were shown twice. There is no threshold; what two hands on this screen actually +manage is the number that belongs there. Agreement inside their own wobble is what would make a +further correction something that may be applied to all 604 pages. A wider gap says the leftover +distance belongs to whoever was sitting there and may be applied to nothing — not to one of them, +not to the average of the two. + +This half is optional and it is about ten minutes of a second person's time. If nobody sits it, +whatever leftover distance the sitting banks is one hand's leftover distance, and must be +recorded in those words. + +One limit the full pass added rather than removed: taking each page's own displacement out +repairs most of this error and measurably not all of it. About one mark in five is still further +out than it should be afterwards, and the agreement reached by correcting a whole page stays +well short of what the best placement of each individual mark would reach. Whatever that +remainder is, it is not a per-page shift, and §⑦'s leftover distance is not the same quantity — +that one is a further move *of* the correction, this one is scatter the correction cannot +reach. Neither is applied. + +**Most of that remainder has since been named, and naming it changes what this question is +asking.** It is not scatter. It is organised by printed line, and correcting line by line takes +one mark in five down to one in nineteen — §⑦ carries the figures and the options. Two things +follow for this section. The generous one: nothing above needed revising, because a page that is +displaced is still displaced, and the finer model is applied on top of the per-page move rather +than instead of it, so every reading here about how the pages vary from one another still holds +exactly as measured. The uncomfortable one: **a per-line correction is many times more numbers, +and this section's question is precisely the one that gets harder the more numbers there are.** +Six hundred pages carrying two numbers each can be sanity-checked by looking at the spread of +1,200 numbers. Nine thousand printed lines carrying four each cannot be eyeballed by anybody. + +Two of the four questions §⑦'s options are scored on exist for exactly this reason, and both +were asked. Every figure quoted there is measured on marks the correction never saw — each +page's marks are split down the middle at random, the move is worked out from one half and +graded on the other — which is the only construction under which a model with more numbers can +honestly be compared against one with fewer. And every line was made to wear another line's +correction: that is **worse than applying no per-line correction at all**, six times worse than +the real one, which is what a genuine per-line signal is obliged to look like and what fitting +to noise could not have produced. + +What neither answers is the thing this section's title actually asks, and it should be said +plainly rather than left to be inferred. **Splitting a page's marks in half holds out marks, not +pages.** Every rung, including the recommended one, is still fitted on every page it is applied +to, so no page in the mus'haf is a held-out page in the sense meant above. There is no version +of this correction that generalises to a page it has not measured, because it has no model of a +page — it has a table. That is not a flaw to fix; a table measured on all 604 pages is a +perfectly good thing to ship. It does mean the four pages that carry no measurement — 1, 2, 603 +and 604 — are the whole of the generalisation question, and the fallback they get is the whole +of the answer. + +### ⑨ Why the marks were ever in the wrong place, and what has to change · **open** + +Every question above this one asks how far out the marks are. This one asks why, and the answer +turned out to be a single thing that explains all of it — including why two careful sittings +moved the number and never the problem. + +**The alignment between the two printings was built by matching ayah-end ornaments, and then +used to place text.** The ornaments are the only objects both printings label, so they were the +only thing available to match on, and matching on them works: they land within a tenth of a unit +of each other. But nothing was ever checked at the marks. On the page this document draws +throughout, the ornaments agree to **0.09 units** while the text is out by about **a whole +mark's height**. Across the whole book, how tightly the ornaments closed predicts almost nothing +about how wrong the text is — and against the downward component, which is the larger half of +the error, it predicts *nothing at all*. + +That one sentence accounts for every complaint the reader made when the sitting was stopped: the +rectangle is off its mark, the wrong size, on the neighbouring mark, and biting the mark next to +it. There is nothing between the ink and the screen except that alignment. A wrong size term +reads as the wrong size *and* as a shift that grows toward the edge of the page; a shift larger +than the gap between two marks reads as being on the wrong one. The count that rules out the +alternative is elsewhere in this document: of 326,515 marks, the number whose name is genuinely +wrong is **zero**. It is all displacement. + +**And this is a known trap with a name, in a field that has been falling into it for twenty +years.** §⑤ has the citation and the mechanism. The short version is that how well an alignment +closes at the points it was fitted to is famously uninformative about how wrong it is anywhere +else — so "the ornaments fit to 0.09" was never the evidence in our favour we read it as, and +three sessions spent arguing about that number were three sessions spent inside the trap. + +Three things are now settled, and they are what this question hands to whatever ships: + +- **A better whole-page alignment cannot fix it.** The best transform that could possibly be + fitted to a page, using the ink itself, still leaves about one mark in six badly out. Adding a + size term to it buys under two per cent and makes a third of pages worse. The whole-page family + is exhausted, and that also retires the wrong-size complaint: the size is off by about a tenth + of a per cent, which nobody can see. +- **The error is organised by printed line, and correcting line by line is most of the repair.** + Held out — fitted on half of each page's marks and scored on the half it never saw — the share + of marks badly out falls from **one in five** (18.4%) to **one in eight and a half** (11.8%) + when each printed line gets its own move, and to **one in nineteen** (5.2%) once a line is + allowed to tilt along its length. Wearing another line's correction instead leaves 31.2% badly + out — worse than doing nothing per-line at all — which is what says these are facts about lines + rather than noise being absorbed. §⑦ has the full table and what each column means. + + These figures were first worked out as arithmetic on the raw displacements and then measured + again by re-scoring every rectangle through the same instrument every other number in this + document comes from. The two ways of asking agree to within half a percentage point, which is + worth more than either on its own: the arithmetic could have been wrong about how much ink a + moved rectangle then covers, and it was not. + +- **What is left after the best correction is not simply a smaller version of the same error.** + A bell curve of the measured width would leave about 3.1% badly out and the measurement finds + 5.2%, so the remainder is a minority of rectangles wrong for some reason of their own rather + than every rectangle being slightly out. That is a separate question from this one and it is + not averaged into the figures above; naming it is what stops the next session from re-measuring + the whole book to rediscover it. + + **It was asked, and half of it now has an answer.** The rectangles still badly out are not + scattered through the book. More than half of them sit on the one printed line in fifteen that + has gone wrong as a whole, so lines go wrong rather than marks going wrong here and there. And + a rectangle at either end of a line is about twice as likely to be badly out as one in the + middle. That is what a disagreement looks like when it grows along a line but does not grow at + a steady rate — and a correction that can only tilt has no freedom to follow it, because a + slope that splits the difference is wrong in the same direction at both ends. + + So the correction was allowed to **bend** along a line rather than only tilt. Measured the same + way as everything else here, on the half of the marks the correction never saw and across the + whole book: badly out **4.95% → 4.12%**, and the typical miss 0.224 → **0.198** units. The same + control was run again and holds — a line made to wear another line's bend leaves 34.4% badly + out, against 18.2% for correcting nothing per-line at all — so a line's bend is a fact about + that line and not a shape absorbed from noise. A freer shape still — one allowed to change + direction twice along a line rather than bend once — was tried and refused: it is better on the + marks it was fitted to and worse on the marks it was not, which is what an allowance with + nothing to find looks like. The ladder stops where it stops being paid for, not where the + arithmetic stops improving. + + **The other half is still unexplained, and it is the half this started from.** Bending fixes + the middle of a line — 4.3% badly out down to 2.9% — and barely touches the ends: 6.6% → 6.0% + and 7.0% → 6.0%. A rectangle at the end of a printed line is *still* about twice as likely to + be badly out as one in the middle, so whatever is doing that is not a shape in where along the + line a mark sits, and no amount of bending will reach it. That is left standing on purpose. The + question below this one is the other candidate for it, and it is the one a reader can settle. +- **Whatever ships must move the text and leave the ornaments alone.** The question two sections + above measures that directly, on the one object the correction could not have been fitted to. + +What stays open is what to actually do about it: which correction ships, whether the word boxes +move with the marks or the two are allowed to drift apart, and what the four pages that never +carry enough marks to measure should fall back to. Those are a decision and not a measurement, +and they have now been written up as one — §⑦'s options A, B, F and G, drawn on a real page in +[`mark-placement.html`](mark-placement.html) and open in the register as `mark-placement`. The +word-box half is option G specifically, which was the half most at risk of being settled by +nobody: it costs nothing by any measurement on this page, because every measurement on this page +is made on marks. + +### ⑩ Whether what is left over is even ours to fix · **open** + +Every measurement above answers in a distance: how far the rectangle is from the ink it should +be sitting on. That is the right currency right up to the moment the correction starts placing +most rectangles from their own ink — and then it stops being, because the rectangles that are +left are not mostly *far* from their ink. A great many of them have no ink under them at all. + +The count, over the whole book. Where a rectangle was placed from its own ink, the printed +strokes fill about a fifth of it and roughly one in eighty is effectively bare. Where the search +found only a weak match, that falls to about a ninth, and **more than a quarter are bare**. Where +the best match the search could find sat on the very edge of how far it was allowed to look, it +is a twentieth, and **more than a third are bare**. So about a third of what the best correction +leaves behind is a rectangle drawn over blank paper. + +A distance cannot say why. It cannot say *there is nothing in this place at all*, and it cannot +separate a rectangle in the right place at the wrong size from one of the right size in the +wrong place — the two have different repairs in different parts of the pipeline, and a size +complaint written down as a displacement is a wrong number that looks like a right one. Nor can +it say the fourth thing, which is the one this question is really about: **that the printed page +itself does something unusual there, and nothing is wrong on our side.** We did not draw those +pages. Counted as a placement failure, a printing oddity makes the correction look worse than it +is; counted the other way, better. Both are wrong and nobody has ever been asked which. + +So the answer has to come from a reader looking at one rectangle at a time and saying which kind +of wrong it is, in words rather than in a number — and saying more than one thing where more than +one is true, because a reader forced to pick just one picks the most obvious and the second fault +is never recorded by anybody. The page that asks it, and the sitting that would answer it, are in +the validation register as *what kind of wrong*. Answers of the fourth kind — the printing, not +us — become rows of their own in [`docs/issues.json`](../issues.json), which is the only route by +which a reader's eye reaches the catalog at all. + +What would settle it: a sitting. If most of the remainder turns out to be the print, then the +placement work is finished and this document's numbers are an overstatement of our own error by +whatever that share is. If most of it turns out to be rectangles that could have been placed and +were not, the search that places them is too timid and that is a change to make rather than a +fact to accept. Until somebody sits it, both readings of the tail in ⑨ are open, and the honest +statement is that we do not know which we are looking at. + +### ⑪ Whether the reader could see the rectangle they were vouching for · **fixed** + +The page that asks ⑩ draws a crop of the print's own artwork, and that paper is deliberately +never re-themed: a page of the mus'haf stays on white whatever theme the reader's phone is in. +The two rectangles drawn on top of it were themed. Under a dark theme our rectangle's line fell +from 5.05:1 against that paper to **2.49:1**, and the reader's own rectangle from 4.89:1 to +**1.70:1**, at a constant stroke of about one and a half pixels. + +That is not an ugliness finding, and the reason is the same one that made ① of this section +worth rewriting. The commonest honest answer on that page is *nothing is wrong with this one*. +A reader who cannot make out the rectangle at all gives exactly that answer, and it arrives in +the transcript as an affirmation indistinguishable from one somebody looked at. The failure +comes back looking like success, and in the direction that flatters us. + +What closed it is a rule rather than a colour: **nothing drawn on the paper is themed.** The +paper, the ink, and the four colours of the two rectangles are stated once and never restated +for a dark theme; the controls around them stay themed, because they sit on the app's own +surface rather than on the print. The two rectangles are also told apart by a dash pattern and +not by hue alone, so the distinction survives colour blindness and survives anybody re-theming +the palette later. A test asserts that the dark theme redefines none of the six, and that both +rectangle lines clear 3:1 against the paper — twelve lines, no browser, and precisely the +invariant that broke. + +The caveat that has to travel with the sixty answers already banked: nothing in a transcript +records which theme it was sat in, and nothing does now either. Those sixty were drawn from the +population where the placement was already made from the mark's own ink, so a gross error was +structurally impossible on those cards whatever the reader could see — but that is an argument +about the cards, not evidence about the screen. + +### ⑫ Whether the placing distances were ever the reader's own hand · **fixed** + +The scorer that reads the placing answers printed **0.000 across and 0.000 down** for +twenty-six marks the reader had visibly dragged. Two faults arriving as one number. + +**It averaged increments, and increments cancel.** Every move the page records — from the nudge +pad or from a drag — is written down as a step, with the running total beside it. Taking the +middle of every step across every move is arithmetic on the wrong column: a nudge one way and a +nudge back are two steps that cancel to nothing, and one mark corrected forty-four times outvotes +twenty-five marks corrected once. + +**And the total beside it is not the hand either.** It is measured from the rectangle as +shipped, so it already contains the correction this document's own fit applied. That is a real +quantity and worth printing — where the reader put the box, against what the app draws today — +but it is not how far the reader themselves moved anything, which is that same total less the +correction. Collapsed into one number it was neither. + +Three figures, all real, and until this was fixed the page printed the first: + +| question | across | down | +|---|---|---| +| what was printed | 0.000 | 0.000 | +| where the reader put it, against what ships | −3.569 | −3.134 | +| **how far the reader's own hand moved it** | **−2.468** | **−2.010** | + +The middle row is the one worth the trouble: it points the same way and is roughly the same size +as the ink measurement in §④, where the print sets its text lower and further across than the +ornament fit predicts on 599 of 600 pages. Two instruments built from different evidence, +agreeing. A scorer printing zero said the reader's hand agreed with nothing. + +What closed it: one row per mark, reading only where the rectangle finally rests. The two +distances are printed under two separately worded sentences, and the prose says outright that +the gap between them is only the correction already applied — otherwise a reader subtracts one +from the other and believes they have found a discrepancy. It also says how many marks, and how +many separate goes those marks took: twenty-six marks over two hundred and five goes is a +finding about the nudge controls rather than about the print, and it is stated as one. Distances +and counts are never combined. The fixture that would have caught the original is now in the +scorer's tests: one mark nudged one way, back, and out again, and a second moved once, asserting +the printed middle is the distance and the count is two marks rather than four steps. + +### ⑬ Whether a sitting counts the marks it looked at or the marks it has left · **fixed** + +Every rate this section reports is a count of complaints over a count of marks looked at. The +page that asks the questions was writing the wrong number into the second one: when a reader +handed their work over, it banked how many marks were **still to go** rather than how many they +had been through. On the sitting that found it, 115 marks were seen and the file said 19. + +A denominator too small does not produce a wrong-looking number, it produces an impossible one, +and only because it was impossible did anybody notice: the rates came out at **600.0%**, +**2280.0%** and `NaN%`. Had the reader stopped four fifths of the way instead of at the end, the +same defect would have produced a plausible figure — a believable error rate over a denominator +nothing in the file admitted was wrong — and it would have been quoted. + +What closed it is the count being written from what was answered rather than from what was left, +and a second guard either side of it, because a page can always be got wrong again: both readers +of a transcript now take the number of marks actually spoken about as a floor, and when they +raise a claimed count to it they **print the file's name while doing it**. A reader who sees that +line is being told there is a defect in the page, not a quirk of one file. It is a warning rather +than a refusal on purpose — the sitting's answers are still good, and throwing an hour away over +a miscounted header would be the more expensive mistake. + +The direction is the thing to remember. A count of marks somebody has looked at can only ever go +up, so a count that falls is never a fact about the reader. + +### ⑭ Whether the marks a reader called odd are odd in the print or odd in our reading of it · **open** + +Fifteen marks, on fifteen separate pages, were set aside by a reader as *odd — I cannot say +how*: not our rectangle in the wrong place, but something about the printed mark itself they +could not read as ordinary. They are six fathas, four successive kasratans, two successive +dammatans, one damma and two hamzas — a spread across the vocabulary rather than one name +behaving badly, which is the first thing that would have made this our problem instead. + +They are held out of the denominator of that sitting's error rate, and being held out is exactly +what leaves them unsettled: they are neither counted against the correction nor evidence for it, +and a category that costs nothing either way is a category that quietly grows. Nothing here can +answer it. Every instrument in this document reads the same bytes the reader was shown, so a +reading that is wrong about what the print contains is wrong identically in all of them, and the +question is settled only by holding those pages against another copy of this print. + +What would answer it: the pages, checked by eye against a second printing. Either the oddity is +in the print — in which case it is a defect in vendored data and belongs upstream, and the marks +stay out of every rate on purpose — or it is in our rendering of it, in which case fifteen marks +are the visible end of something that is also silently wrong on marks nobody flagged. + +**Two more arrived on 2026-08-17, and they matter out of proportion to their number.** A +superscript alef on page 86 and a wasla on page 463, called odd in the same words. The first +fifteen all came from marks the correction could not place at all, which left the cheapest +explanation open: that a mark nothing can find the ink for and a mark that looks strange to a +reader are the same marks for the same reason, and the oddity is downstream of whatever defeats +the search. These two are not those. They come from marks the correction placed **from their own +printed ink**, and matched well enough to clear the bar — so whatever the reader is seeing +survives a successful match, and cannot be explained by the search having failed. Seventeen +marks now, on seventeen pages, across two populations that share nothing but the print. + +**Fifteen more on 2026-08-17, from a full part of a hundred and six**, which is the first +number here worth calling a rate: fourteen in every hundred marks looked at, against nine in +the hundred and sixty sat before it. They are three fathas, two kasras, two hamzas, two +fathatans, two successive fathatans, a shadda, a successive kasratan, a successive dammatan +and a kasratan, on fourteen pages between 60 and 593. Thirty-two marks in all now, and the +spread across the vocabulary has held through every sitting — still no name behaving badly, +still no page carrying more than two. + +That the rate did not fall as the instrument improved is the part worth noticing. Every other +figure from that sitting moved in the direction the fixes predicted, and this one did not +move at all. So whatever a reader is seeing when they say *odd* is not something the page was +doing to them, and the cost of leaving it open is now visible: at fourteen in a hundred, a +full pass over what is left would set aside something like two hundred and forty marks on the +grounds that nobody could say what was wrong with them. + +**Eleven more on 2026-08-17, and this time they are not spread across the vocabulary.** They +are five fathas, three hamzas, two kasras and one successive dammatan, on eleven pages +between 333 and 596, no page carrying two. Forty-three marks now. What is new is that ten of +the eleven are marks printed as a single piece of ink, and the sitting they came from was +half doubled marks — thirty-three doubled against twenty-eight single among the marks +answered. So *odd* was said about roughly one single mark in three and one doubled mark in +thirty, a difference too large to be nothing in a sitting this size. + +Two readings of that, and the page cannot separate them. Either single marks really do carry +more of whatever the oddity is — or *odd* is simply the button left when a reader has no word +for what is wrong, and a doubled mark always gives them one: its rectangle is visibly the +wrong size, which is a complaint with a name and its own button. On that second reading the +gap says nothing about the print and everything about which complaints were available, which +is the same instrument fault ㉗ describes from the other end. Nothing here decides between +them, and the check that would is unchanged: hold the pages against a second printing. + +### ⑮ Whether an answer given by pointing is the same evidence as one given by hand · **open** + +The sittings held so far measured what correcting a rectangle by hand costs. Of the 160 marks +the reader worked through, **158 were moved**, over **350 separate goes**, a median of **2.105** +units of travel and a worst of **12.229** on a mark that is 5.6 by 3.6. Every one of those goes +is a person doing two different jobs at once: deciding which mark is meant, which only a person +can do, and then deciding how far across and how far down, which the print already knows. + +So the second job was handed back to the print. The ink inside the window is cut into its +separate pieces, and a tap on a piece puts the rectangle round it — round both, when a mark is +printed in two parts and the reader taps each. The rectangle moves and takes its size from the +ink in one gesture instead of a dozen presses. + +**And that makes two kinds of answer that must never be pooled.** The words are the same either +way, because the reader is saying the same thing: the rectangle belongs somewhere else, and it +is the wrong size. What differs is who measured it. A hand-placed answer is an eye's estimate of +where a mark sits, and its spread is partly the eye's. A pointed answer is the reader choosing +which ink — still the judgement — with the extent taken off the printed outlines, and its spread +is the grain the ink was cut at. Averaged together, the second tightens the first, and the +tighter number would read as *the reader agrees with us more closely than we thought* when it +means nothing of the kind. Every answer now records which way it was given, and a press on top +of a pointed answer gives up the claim: the moment a reader adjusts by hand what a tap placed, +the answer is a hand-finished one again. + +Two things this does not settle, and one of them is new. The marks already answered were +answered on the older instrument, so any rate quoted across both is a fact about the mix of the +two — the same trap this section's ⑬ is about, in a different column. And making a correction +cheaper is a change to the one ratio these sittings exist to measure: complaining and affirming +still cost a single tap each, which is the constraint the page is built to, but *finishing* a +complaint no longer costs a dozen. A reader who would have shrugged and affirmed rather than +push a rectangle across the card may now say what they actually think. That is the point of the +change and it is also a reason the two halves are not one population. + +What would answer it: the same marks answered both ways, by the same reader, far enough apart +that they are not remembering. Until then the two are read apart and never averaged. + +### ⑯ Whether a finger resting on the rectangle was banked as a placement · **fixed** + +Reaching for the ink under the rectangle is the commonest gesture on that page, and until +2026-08-15 it could bank a placement nobody made. + +Two boundaries, in two different units, decided what a gesture was. A tap had to end within ten +screen pixels of where it started **and** inside 600 ms. A drag banked a placement once it had +moved more than 0.05 page units. At the framing that shows the whole word, ten screen pixels is +about **1.5 page units** — thirty times that floor — so the two boundaries left a gap, and a +finger that rested for three-quarters of a second with a few pixels of tremor fell straight into +it: too slow to be a tap, far too far to be nothing. What it banked was a placement of nearly +half a unit, on a mark that is 5.6 by 3.6, and there is no way to see it in the file afterwards. +A phantom placement is a well-formed placement. + +The repair is one boundary, in one unit, with no clock. Ten screen pixels decides, and both the +600 ms and the page-unit floor are gone. The clock never asked anything worth knowing — a slow +tap is still a tap, and a reader steadying their hand is not making a different statement — and +only distance can answer whether a finger stayed still. Screen pixels rather than page units +because a finger is the same size on every card and a page unit is not. + +**What this does not clear.** Every transcript banked before the fix carries whatever it +produced, and nothing distinguishes those placements from real ones. The affected marks are the +ones a reader reached for by tapping the ink, which is also the population ⑮ says must be read +apart from hand-placed answers — so the same caveat covers both, and the rates quoted for the +sittings sat before this date are quoted with it. + +The same audit corrected two smaller things about the same gesture. Which piece of ink a tap +reaches was decided by area alone across everything within a fingertip of slack, so a finger +squarely inside a large piece could be answered with a small piece it had merely come near: +aiming at the dead centre of a piece returned a **different** piece on 3.7% of aims at the wide +framing, and 2.3% of taps on blank paper reached past nearer ink to something smaller further +away. Ranking landed-on above came-near takes the first to 1.2% and ends the second. And a tap +that reaches no ink at all now says so, because saying nothing is indistinguishable from a tap +the page never received, and a reader who cannot tell those apart stops pointing at ink. + +### ⑰ Whether the sittings on disk are the ones anybody should still be sitting · **fixed** + +Rebuilding the unsat sittings after a round of answers is the step that makes the next hour +better than the last one, and it was the only step in that whole routine that ended in an +instruction rather than a command: *confirm the deal did not move — same number of parts, same +total, and no mark that has been answered coming back round again.* Sixteen files, by eye, at +the end of an hour of somebody else's work. In practice that meant nobody confirmed anything. + +Both ways it goes wrong are invisible from inside a sitting. If the rebuild is skipped — or run +with one handed-over transcript left out of the list — the parts still open, still count down, +still bank answers, and re-ask questions somebody already answered; a page you have seen before +looks exactly like a page you have not. If the rebuild runs against a different measurement of +where the marks sit, every rectangle in every part is drawn from displacements that are not the +ones on disk, so every answer is about a picture nobody can reconstruct afterwards. That one is +worse, because the answers are wrong rather than merely wasted. + +Neither needed a person, as it turns out. A built sitting already says what it was built from, +because it has to: the reader's place is stored under a key made of the measurements' +fingerprint, the set, the slice and the seed, and the set of answers already given is folded +into that key so that shrinking the pool cannot silently strand somebody at card ninety of a +hundred and seventeen. A part that disagrees with the tree announces itself — to anything that +asks. Nothing asked. + +**Closed by** `pnpm audit:sittings`, which reads every built part back and holds it to its own +account of itself: the measurements match the ones on disk, every part knows about every answer +given, nothing already answered is asked again, no mark is dealt into two parts and none into +nobody's, and the count the reader is shown describes the population it claims to. Eleven tests +build each of those failures on purpose, because an auditor that only passes the clean case is +indistinguishable from one that always says yes. The routine's last step now names it. + +The one reading of the word *answered* is shared with the rebuild rather than written twice — +two readings of it drift, and the drift surfaces as a mark the rebuild drops and the audit still +counts, with neither obviously wrong. Extracting it left every built part byte-identical. + +**What it found on the first run**, which is the answer to the question this entry opens with: +nothing. The sixteen parts hold 1,710 marks between them, 167 already answered, 1,877 in all, +all drawn from the same measurement now on disk. The deal had not moved. That is worth stating +because it was not knowable before, and "we assumed so" and "we checked" are different claims. + +**It is not a gate and will not become one.** The parts are build products and the answers +accumulate on whichever machine served them, so in a clean checkout it would pass by being +unable to look — which is the failure mode this repo already refuses in the register that tracks +what has been published. It runs where the evidence is. + +### ⑱ Whether pressing the same nudge button twice was answering or zooming · **fixed** + +Reported from a phone, mid-sitting: *when I click the same nudge button twice it zooms in*. +That is the browser's double-tap-to-zoom, and it fires on any two taps close enough together +in time and space — the same arrow twice, or two different arrows a centimetre apart. Between +them, that is most of what placing a rectangle consists of. + +The magnification is not the cost. The cost is that the card walks off the screen in the middle +of an answer, so the next press lands somewhere the reader did not aim and the transcript +records a placement nobody made. A reader who cannot keep the rectangle still is also a reader +who stops nudging and affirms, which biases the one ratio the sitting exists to measure. + +The page had been carrying `touch-action` since the drag was wired, but only on the stage: the +crop got `pan-y`, and `none` while a selection was being drawn. The chrome — the dock, the +buttons, the body — had none at all, so the default applied to every control on the page. + +**Closed by** `touch-action: manipulation` on the body, which is the ordinary behaviour minus +that one gesture, with three tests in `build-mark-report.test.mjs`. On the body rather than on +the buttons deliberately: two taps on *adjacent* buttons trigger it as readily as two on one +button, so a per-button rule would leave the commonest case — left, then up — unfixed. It costs +nothing anybody could use, because `touch-action` is intersected down the ancestor chain rather +than inherited, so the stage's stricter values still win, and `pan-y` already excluded +double-tap zoom on the print. Pinch still zooms everywhere, which is the gesture a reader +actually reaches for on a page of the mus'haf. + +### ⑲ Whether a sitting could be reached from anything but the machine serving it · **fixed** + +The sittings are served from one laptop, and the serving side bound the loopback address by +default — reachable from exactly that laptop and nothing else. Every sitting anybody has ever +sat on a phone therefore depended on somebody remembering to pass a flag, and **the flag was +written down nowhere**: not in the routine that runs the sittings, not in a script, not in the +Makefile, not in any document. The arrangement this project had chosen worked only for whoever +already knew the incantation, and that person was going to be asked to recall it in six months. + +Underneath it sits a sharper hazard, which is why this is a defect and not an ergonomic +grumble. A reader's place, and every rectangle they have moved, is kept by the browser against +an **origin** — scheme, host and port, with the host half compared as text. So the laptop +reached by its name and the same laptop reached by its number are two separate memories of the +same sitting, with nothing on screen to say so: the page simply opens at card one as though the +last hour had not happened. That has already happened once, and cost an hour of somebody's +confidence before the recovery was built. + +**Closed by** a module that decides the one address — `packages/etl/scripts/lib/tailnet.mjs`, +with sixteen tests — plus two named commands, `pnpm sit:serve` and `pnpm sit:index`, and a step +⑥ in the sitting routine that says to run them. The address is read off this machine's own +network interfaces rather than asked of a program, because the private network hands out +addresses from a range no home or office network uses, so an interface holding one *is* that +network. The server now binds it by default — that network only, not every interface, because a +sitting carries a write endpoint and the café's wifi is an interface too — and prints one +address with the others named as things not to use. The front door says the same thing on screen +when it notices it is being read at another spelling of this machine. + +### ⑳ Whether the front door could say which sitting you were in the middle of · **fixed** + +The front door listed sixteen parts as sixteen bare numbered tiles. Every question a reader +actually arrives with — *where was I, which of these have I finished, is there any point +opening number nine* — was unanswerable from it, so the way to find out was to open parts until +one looked familiar. On a phone that is sixteen page loads of a megabyte and a third each. + +The reason it had never been fixed is that the obvious fix does not work. A rebuild drops every +mark carrying a standing answer, so at the moment the front door is built every part is +untouched and every progress bar reads zero. The only place the live figure exists is the log +the serving side has been appending to since the first answer. + +**Closed by** a rewritten front door with eleven tests. Each tile carries the marks its part +holds, the page fetches everything this machine has heard and counts the overlap, and a banner +at the top names the part to carry on with — the one already begun and unfinished, because a +part left half-done is the only place the re-deal cannot help. Answers in the log for marks in +no part on the page are ignored rather than added: one log covers every deal there has ever +been, and counting those again would take the remaining figure below zero. + +The rule for which answers still stand is **not** restated in the generated page. That function +is written closed over nothing for exactly this reason and its own source text is inlined, so +there is one reading of the word *answered* running in two places; a test pulls the copy back +out of the built page, evaluates it with nothing in scope, and makes it agree with the module it +came from. The same discipline put the reading of a built sitting into one place shared with the +auditor, so the front door and the audit cannot disagree about how many parts exist. + +### ㉑ Whether a mark reached by pointing can ever be called only one kind of wrong · **confirmed** + +Sixty marks were sat on 2026-08-17, in +[two sittings](../validation/rulings/2026-08-17-placement-what-kind-of-wrong-placed.seed23.settled.json), +and every one came back saying both *the +rectangle is in the wrong place* and *the rectangle is the wrong size*. Sixty out of sixty, for +both words, with no mark anywhere saying one without the other. That is not what the reader was +asked and it is not what they did. + +Nearly all of them were answered by pointing at the ink. Pointing is one gesture and it +sets the whole rectangle at once — it moves to the ink and takes its size from the ink in the +same act — so afterwards both the position and the size differ from what was drawn, essentially +always, because two rectangles fitted independently to the same ink are never identical to the +last decimal. The page files a complaint for each of them whenever the difference is anything +other than exactly nothing, and *exactly nothing* here means the smallest difference a +computer can represent. So both words fire on every point, together, forever. + +**This is a row rather than an ergonomics note because it made a number wrong.** The +mark-level rate is fine and stands: the reader has confirmed that every one of those taps was +a correction they meant, so sixty of sixty marks carrying a fault is a real measurement of +those two bands. What is not a measurement is the split between the two words. In the weaker +band alone, four of the thirty were filed as the wrong *size* while their size changed by less +than a twentieth of a unit, and four were filed as in the wrong *place* while moving less than +a twentieth. Anyone reading "thirty-seven wrong-size statements" as thirty-seven marks judged +the wrong size is reading an artefact of the gesture. + +**Why the fix is not a threshold.** The tempting repair is to pick a distance below which a +change does not count, and it would be wrong twice over. It would be a number chosen rather +than derived — the extents a point snaps to are taken from the printed outlines and are not +rounded to any grain, so there is no natural floor to borrow. And it would still be answering +the wrong question, because a reader who points at ink has not made two statements that we are +failing to tell apart. They have made **one**: *the rectangle belongs around this ink.* The +vocabulary has no word for that, so the page spends the two words it has. That is the same +observation ⑮ makes about mixing pointed answers with hand-placed ones, one level further in: +⑮ says the two kinds cannot share a rate, and this says the words themselves stop meaning +different things once the answer was pointed. + +**What would close it:** a word for what a point actually says, with the existing two reserved +for what a hand says, and the settler reading pointed and hand-placed answers under separate +headings rather than pooling their words. Until then, the word-level counts from any sitting +worked by pointing are not quoted, and the mark-level rate is. + +**[A hundred and six more marks on 2026-08-17](../validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json) +said how big the distortion is, and it is not +fixed.** That sitting worked the marks the correction could not place at all, where the +rectangle starts far from the ink rather than nearly on it, and the artefact all but vanished: +one mark in a hundred and six was filed as in the wrong place while moving less than a +twentieth of a unit, and seven in a hundred and four were filed the wrong size while their +size changed by less than that — against four in thirty for each of those in the bands. Two +marks came back saying *moved* and not *wrong size*, which are the first anywhere to carry one +word without the other and which retire the word *forever* above. + +So the size of the distortion is a property of **how close the rectangle already was**, not of +the gesture. Point at ink two units away and both words are earned; point at ink a hundredth +of a unit away and both words fire on a mark nobody would say was wrong. That sharpens the +open question rather than closing it — the reader still made one statement and there is still +no word for it — but it says where the damage is, and it is the opposite of where a reader +would guess: the sittings whose numbers are least trustworthy are the ones where the +correction was already working. + +Two consequences follow and are worth stating before somebody reads a rate the wrong way. The +word-level counts from the fallback sittings can be quoted with the caveat attached, and those +from the band sittings still cannot. And a sitting drawn from marks the correction places well +cannot be scored on words at all — which is most of the book, and is the population any future +sitting checking a *good* rule would have to be drawn from. + +### ㉒ Whether the weakest matches go wrong by class rather than at random · **open** + +The correction places each mark from its own printed ink and accepts the match when the +rectangle and the ink overlap by more than 0.55. The marks that clear that bar only just have +never been looked at as a group, and §④ now records what happened when +[sixty of them were](../validation/rulings/2026-08-17-placement-what-kind-of-wrong-placed.seed23.settled.json), in +two bands of thirty: **every hamza in both, eleven of eleven, was about a page unit across in +the same direction with its size correct** — a median of 1.077 units in the weaker band and +0.573 in the better one, against two thousandths of a unit for every other mark. Fatha and +kasra came back too short in both, and by less in the better one. + +That is a per-class error in a population where §④'s own refutation of per-class error does not +reach, and it matters because of its shape rather than its size. A residue spread evenly over +every class is the price of correcting by a median and there is nothing to do about it. A whole +class in one direction is either a bug in what the search matches against or a case for a +per-class term, and both are actionable. The halving between the two bands is the strongest +thing here: it ties the error to how sure the search was, which is what an argument about the +search predicts and what a fixed per-class offset does not. + +**What would answer it.** Two things, in order, and neither needs a reader. First, and nearly +free: re-measure the sideways displacement for hamza alone across the whole book rather than +across these eleven, which separates a property of the weak tail from a property of hamza. Then, +if it is the tail, test whether the search is settling on the letter a hamza sits on rather than +on the hamza — which §④ names as the mechanism and does not test. + +What this must **not** do is become a per-class correction fitted to sixty marks. Eleven hamzas +from two bands are enough to say *look here*; they are not enough to edit the placement of +326,515 marks. + +### ㉓ Whether the marks we cannot place from ink are wrong in one way or in two · **answered** + +[A full part of a hundred and six was sat on 2026-08-17](../validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json) — every mark in it one the correction +could not place from its own ink, so the rectangle was inherited from the printed line +instead. All hundred and six came back faulted, which is what that population is selected to +produce and says nothing. What it did produce is a split nobody had looked for: **the two +things a rectangle can get wrong are not going wrong on the same marks.** + +Across the whole sitting the reader moved the rectangle a median of 1.479 units and changed +its size by 0.130 — an order of magnitude apart on a box about 6.3 by 3.4. The rule inherited +from the line is getting the size very nearly right and the position badly wrong. But sorted +by the name of the mark, that single sentence turns into three different findings: + +- **The single marks are misplaced and correctly sized.** Fatha, kasra, damma, shadda and + hamza — seventy-eight of the hundred and six — moved about a unit sideways and barely at all + downward, with their size left alone to within a tenth of a unit. Hamza moved least of the + five, which is the opposite of what the bands found in ㉒ and is the first evidence that + ㉒'s hamza finding belongs to the weak tail of the *placed* population rather than to hamza. +- **The marks that sit above the letter are misplaced downward too, and by much more.** Sukun, + twelve of them, moved 2.280 across and 2.151 down; the two rounded zeros moved 2.079 and + 3.043; the two maddahs 0.214 and 1.254. Every other class moved downward by less than half a + unit. Whatever the printed line is being used for, it carries no information about how high + above the letter a mark that is not a vowel sits. +- **The doubled marks are placed nearly right and drawn far too small.** The ten tanwin and + successive marks moved a few tenths at most and were grown by 0.5 to 1.4 units in width and + height — the only marks in the sitting whose size changed by more than a fifth of a unit. + A doubled mark is two of something, and the box is being drawn for one. + +The third of those is the most actionable and the least dependent on the reader's aim: growing +a box is a statement about extent, which the print itself can be asked about, and it does not +need anybody's opinion of where the centre is. + +**Why this is not yet a fix.** Ten doubled marks and sixteen above-the-letter marks are enough +to say *look here* and nowhere near enough to edit the placement of anything, which is the same +line ㉒ draws for the same reason. And a hundred and six marks from one part of sixteen is one +draw from a shuffle: this could be a property of the population or a property of these pages. + +**What would answer it, and none of it needs a reader.** Re-measure size against the ink for +every doubled mark in the book, which is the whole claim of the third finding and is free. +Then check whether the marks that sit above the letter have a different vertical relationship +to the line than the vowels do, over all 326,515 rather than over sixteen. If either holds at +book scale, the fallback rule needs splitting by what kind of mark it is placing rather than +tuning; if neither does, this was one part of sixteen telling us about itself. + +**Answered on 2026-08-17, and it was the second thing: this was one part of sixteen telling +us about itself.** The two position findings are gone; the size finding survives and has moved +to ㉖. Both halves of the test above were run, and everything above this paragraph is left +standing on purpose, because the theory was reachable, internally consistent and wrong, and +the next person to read a sitting will reach for it again. + +Fitting the line correction over all 326,515 marks and measuring what it leaves behind, class +by class, leaves nothing anywhere: + +| class | n | residue across | residue down | +| --- | ---: | ---: | ---: | +| fatha | 121,277 | 0.011 | 0.013 | +| sukun | 36,616 | −0.022 | −0.016 | +| superscript alef | 9,358 | −0.032 | −0.054 | +| successive fathatan | 2,897 | 0.001 | −0.001 | +| every class together | 322,011 | 0.004 | 0.002 | + +Every class within nine hundredths of a unit of zero, on a box about 6.3 by 3.4. There is no +per-class offset in this print, and in particular the marks that sit above the letter do not +have a different vertical relationship to the line — the second finding above is contradicted +directly, on 36,616 sukun against twelve. + +**So where did a pattern that clean come from?** From the selection, which is the general trap +and is worth naming: **this sitting is drawn from the marks the search refused, so the first +thing any pattern in it describes is the refusal.** A mark is refused for one of two unrelated +reasons — the ink was further away than the search was allowed to look, or the search reached +it and did not like the match — and those two are not evenly spread across the classes. Sorting +that population by the name of the mark sorts it, mostly, by which of the two refusals it +suffered. The reader's three groups are three views of the refusal rule, seen from inside it. + +That relocation is the useful half of this item: the fault is not in what the rectangle knows +about each kind of mark, it is in the rule that decides a rectangle cannot be placed at all. +㉔ and ㉕ are what was found there, and both are fixed. + +### ㉔ The search was not allowed to look as far as the marks actually are · **fixed** + +Relocated from ㉓ and then measured against the one thing that can settle it: 272 marks a +person placed by hand. + +The search looks three units in each direction for the ink a rectangle should be sitting on, +and gives up if the best it finds is out at that boundary. **The reader's answers say the +marks it gives up on are a median of 4.292 units away, and 230 of the 272 are further out than +three.** The search was being asked to find something it was forbidden to reach. Where it ran +out of room it was already pointing the right way — on a hundred such marks the direction it +was straining in agreed with the reader 92 times across and 99 times down — so it knew where +the answer was and was stopped short of saying so. + +Scored against those 272 hand-placed marks, as distance from where the reader put the box: + +| what places the box | median | within a unit | within half | +| --- | ---: | ---: | ---: | +| the raw rectangle | 4.292 | — | — | +| the printed line, as we ship it | 1.974 | 31% | 19% | +| its own ink, searched three units | 1.816 | 40% | 28% | +| its own ink, searched eight units | 0.099 | 84% | 83% | + +**Why the fix is not simply a wider search.** Widening it for every mark keeps 99.82% of them +accepted, and moves 4.11% of the already-accepted ones — 2,722 marks — by more than two units, +worst 13.528. A wider window finds a better-scoring match that is the *neighbouring* mark's +ink, and does it confidently. There is no ground truth on those and there never will be, so +they cannot be adjudicated, only avoided. + +**Closed by escalating instead of widening.** Search three units as before; only where that +refuses, search again at eight. Marks the ordinary search placed keep their measured +displacement byte for byte, so the risk to the 99.43% is not small, it is zero by construction +— checked on 14,398 accepted marks over 28 pages, of which 14,046 came back identical and the +other 352 were all marks the search had reported *outside* its own boundary, which is ㉕. The +cost is near nothing, because the wide search runs on the half-percent that failed rather than +on the book. + +One thing that guarantee does **not** cover, and it is worth stating rather than being found +later. The correction is *fitted* from these measured displacements, so a mark whose +displacement was previously pinned at the boundary was contributing a truncated number to the +fit and now contributes a true one. That moves the fit, and therefore moves where every mark +on that printed line is drawn — by very little, since the correction is a median over roughly +thirty-six marks a line and this changes well under one in a hundred of them, but not by +nothing. The bit-for-bit claim is about what the search measured. What the reader finally sees +is that measurement plus a fit, and the fit got better inputs. + +**What recomputing the corpus costs, and it is not nothing.** The two rulings a person has +already given are stamped with a fingerprint of the displacements they were given against, and +the scorer refuses to read a ruling against a different set — correctly, because a verdict about +where a box should go means nothing if the box has moved underneath it. Replacing the +displacements changes that fingerprint. The old file is kept here under its own fingerprint so +those two rulings stay scorable on this laptop, but the file is far too large to commit and +never was committed, so on any other machine the only way back to those bytes is to check out +the code as it stood before this fix and re-run the sweep, which is an hour and a half. The +answers themselves are safe either way: what a person said about a mark is stored against the +mark, not against the measurement, so a new deal inherits every answer already banked. + +It still refuses honestly, which is the point of keeping it: on those pages 1,227 marks are +refused by the first look, 1,124 are placed by the second, and the 103 left over match at a +median of 0.894 against the 0.909 a good match scores — they are marks whose ink is genuinely +eight or more units from where the rectangle claims to be, not marks the rule has quietly +started accepting. The refusals stay the population worth a person's hour. + +*Closed by `packages/etl/scripts/lib/mark-ink.test.mjs` — six assertions that a wider answer is +taken only where the first look refused, only when the wider look does not itself refuse, and +only when it matches better, and that a mark the first look placed is returned unchanged.* + +### ㉕ The search could report an answer it had never checked · **fixed** + +Found while fixing ㉔, and it is older and quieter than ㉔ is. + +The search runs in two passes: a coarse sweep of the whole allowed region, then a fine sweep +around whatever the coarse one liked. The fine sweep was not bounded by the region — so when +the coarse winner sat on the boundary, the refinement was free to step a quarter unit past it, +and the answer that came back was one the search had never scored the surroundings of. It +could not have known it was the best, because it never looked further out. + +The damage is not the quarter unit. It is that **the only way anything downstream can tell a +mark ran out of room is that its offset came back sitting exactly on the boundary** — so every +mark that slipped past became invisible as a refusal. In the shipped corpus that is 2,252 +marks, of which **1,923 were being accepted and shipped as placed from their own ink**, at a +median match of 0.859 against the 0.909 a good match scores. Nearly three times the size of +the edge population we were counting, and all of it counted on the wrong side. + +*Closed by clamping the fine sweep to the region, so an offset on the boundary means what +everything downstream reads it as meaning, and by the same test file: four cases asserting the +answer never exceeds the distance given, at four different distances, plus one asserting it +lands on the boundary exactly when the ink is out of reach.* + +### ㉖ The doubled marks are drawn too small, and no amount of moving them will help · **confirmed** + +The one finding of ㉓ that survived the corpus, and it survived because it is not about +position at all. Carried out of ㉓ so that the refuted part and the confirmed part stop +travelling together. + +A doubled mark is two of something, and the rectangle is being drawn for one. That shows up +as a mark that is refused far more often than any other and matches worse even when accepted: +they are refused at 2.42% against a base rate of 0.45 to 0.54%, and where they are accepted +they match at 0.889 against 0.909. Where they are refused they match at 0.457 — half of what +a good match scores, and a number no rectangle in the right place can produce. + +**It is a size fault, and the evidence that it is one is that distance was never the problem.** +These marks need 2.14 units of movement, against 4.292 for the refused population as a whole, +and only 17% of them ever reach the search boundary. ㉔'s escalation recovers most of the +book and recovers only 45% of these, exactly as it should: a rectangle too small to contain +its mark caps the overlap below the floor wherever you put it, so a fix that only moves things +cannot reach it. + +**What would answer it, and it does not need a reader.** Measure the extent of the ink under +every doubled mark in the book against the width and height we draw, which the print can be +asked about directly. If the shortfall is a consistent multiple, the box for a doubled mark is +being computed from one component of it and the repair is arithmetic; if it varies, the extent +has to be measured per mark. Either way this is a change to what the rectangle *is*, not to +where it goes, and it belongs in a different part of the pipeline from ㉔. + +#### A reader answered the question above on 2026-08-17, and the answer is *it varies* + +[Ninety of these marks were put in front of somebody](../validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json), drawn from the badly-matched population +of the recomputed corpus; sixty-one came back with a correction, thirty-three of them doubled +marks and twenty-eight single. That link is the sitting in its finished, ninety-of-ninety form — +㉚ below says what changed once it was sat the rest of the way; the sixty-one-mark counts in +this item are left standing as they were first read, not edited to match. This is the first time this item has had ground truth rather +than an inference from match scores, and it settles the branch above and breaks the headline. + +**The population claim gets stronger, not weaker.** Doubled marks are 8,554 of the 326,515 +marks in the book — 2.6% — and 160 of the 329 the search refuses for matching badly, which is +48.6%. Nineteen times their share. The rates, one row per kind, against a base rate that is +essentially zero for anything single: + +| mark | in the book | refused | rate | +| --- | ---: | ---: | ---: | +| successive kasratan | 1,935 | 61 | 3.15% | +| fathatan | 734 | 21 | 2.86% | +| kasratan | 599 | 11 | 1.84% | +| successive fathatan | 2,901 | 45 | 1.55% | +| successive dammatan | 1,807 | 20 | 1.11% | +| fatha | 122,948 | 67 | 0.05% | +| kasra | 45,970 | 25 | 0.05% | +| sukun | 37,148 | 1 | 0.00% | +| shadda | 22,678 | 1 | 0.00% | + +**The heading of this item is wrong and is left standing.** *Drawn too small* was a prediction +from the shape of the theory — two of something, a box for one — and the reader's rectangles do +not support it. Of the thirty-three doubled marks, fourteen were made wider and nineteen +narrower; the middle change in width is −0.13 units while the middle change *ignoring direction* +is 0.57. The error is four times larger than its own average, which is another way of saying +there is no average. Height is the same shape: +0.23 against a magnitude of 0.51. + +So it is the second branch. **The extent is not a multiple of anything and has to be measured +per mark**, and the arithmetic repair the paragraph above hoped for does not exist. + +**Where in the rectangle the fault sits, which nobody had asked.** Measure each edge separately +and the two populations come apart cleanly. For a doubled mark the near edge is nearly right and +the far edge is not: + +| | left edge | right edge | top | bottom | +| --- | ---: | ---: | ---: | ---: | +| doubled marks | 0.209 | 0.549 | 0.220 | 0.471 | +| single marks | 0.672 | 0.586 | 0.211 | 0.447 | + +A single mark's whole box slides — both side edges move about the same distance, which is what +a placement error looks like. A doubled mark's box is anchored where it should be and runs to +the wrong place, its far edge wrong by two and a half times its near edge. That is an extent +fault in the plainest form the measurement can produce, and it is the strongest evidence this +item has. Whether the near edge is the one the reading starts from is a guess worth checking +rather than a claim; the geometry above does not know which way the line is read. + +**What each kind of fix could possibly buy, scored against the rectangles the reader settled +on.** Every row is a ceiling — three of them are built from the reader's own answer and so +cannot be reached by any rule, which is the point: they bound what is worth building. + +| what we draw | doubled | single | +| --- | ---: | ---: | +| the printed line, as we ship it today | 0.788 | 0.623 | +| our rectangle, given the reader's size about our centre | 0.796 | 0.722 | +| our rectangle, moved perfectly, at our size | 0.852 | 0.744 | +| our rectangle, given the reader's size from our near edge | **0.868** | 0.711 | + +Read the columns and the split is the whole finding. **For a doubled mark the best thing that +can be done is to fix the size and leave the position alone; for a single mark it is the exact +reverse**, and a rule that treated the refused population as one thing would spend its effort +on the wrong half of it either way. + +**And the refusal itself is vindicated.** The answer the search found and rejected is *worse* +than what we ship in its place — 0.596 against 0.715 in overlap, and its centre 1.065 units +from the reader's against 0.664. The search is not failing to notice a good answer here. It +looked, found nothing it liked, and said so, and the rule that draws from the printed line +instead was right to. These are hard marks and the queue is honest. + +**Two theories this sitting suggested and the corpus killed, both worth naming because both +were reachable.** The thirteen hamza in the sitting all shipped at one size, 4.00 by 3.90, +while hamza ships twenty-one sizes across the book — which looked like a class drawn from a +constant. It is not: that size is 16,214 of the 16,385 hamza in the book, so the sitting was +uniform because the book is, and that size is refused at 0.43%, which is the class's own rate +to two decimal places. The second was the same idea one level down — that some particular size +is the one that fails. Broken out by size, nothing stands out anywhere; successive kasratan +ships 323 distinct sizes across 1,935 marks, which is very nearly one per mark, and the sizes +that fail worst are the ones with the fewest marks under them. Both theories were the selected +population describing its own selection, again. + +**One thing this cannot say.** Every measurement here is over marks the search refused. That +the size is noisy among *refused* doubled marks does not establish it is noisy among the +accepted ones, and no reader can settle that — it needs the ink extent measured under all +8,554 of them, which is the free half of the paragraph above and is still owed. + +### ㉗ Counting what a reader pressed stopped meaning anything when tapping the ink was added · **confirmed** + +[The sitting above](../validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json) +reports eighty-five presses of *the rectangle is the wrong shape*, across +sixty of the sixty-one marks somebody answered. Read as opinions that is overwhelming, and it +is the first thing anybody reading the summary sees. + +Fifty-seven of those sixty marks also carry a tap on the ink. Tapping the ink puts the +rectangle around what was tapped, which changes its size, and a size that changed is recorded +as the shape having been called wrong — so the press was the tap, not a second judgement. +**Three of the sixty are a shape complaint somebody made on its own.** Ninety-two placements +split the same way: sixty-five came from a tap and twenty-seven from the arrows. + +This is not a reason to distrust the sitting, and the distinction matters. The rectangles the +reader settled on are exactly as good as they were — a tap is a statement about where the box +goes, made faster. What is broken is the **tally**: every count of how often a fault was named +is now a count of how often a gesture was used, and the two stopped being the same thing the +day the tap landed. The summary printed at the end of settling says *60 were reshaped, over 85 +separate goes* and means something much weaker than it sounds. + +**What would answer it.** Record what the reader did *and* how they did it, and count them +apart — a shape called wrong by hand is a different row from a size that moved because a tap +put the box somewhere. The transcript already carries the distinction on every placement; it +is the shape events that do not, and nothing downstream separates them. + +**Why this is worth an item rather than a footnote.** A count that overstates a fault by twenty +to one is the kind of number that gets quoted into a decision, and this one very nearly was: +the doubled-mark finding above was reached from the geometry of the corrections, but *the rate* +at which shape was called wrong is what suggested looking there, and on its own it would have +pointed at every mark in the sitting equally. + +### ㉘ A rule that reaches for the ink, rather than resizing toward it, was checked against the whole book · **confirmed** + +Item ㉖ found that doubled marks want their size fixed and their position left alone, and +scored candidates that resize the shipped rectangle toward the reader's own. But fifty-seven +of the sixty-one marks in +[that sitting](../validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json) +were placed by the reader *tapping the ink itself* — +which means the reader's rectangle was already close to a much simpler rule: **take the ink +under the window, not a resized guess about it.** + +That rule was checked two ways. First, against what the reader actually chose: of the pieces +of ink in each mark's window, taking every piece whose middle falls inside the shipped +rectangle reproduces the exact set the reader tapped on 96% of the fifty-seven — 94% of the +doubled marks, 100% of the single ones. The rectangle it draws scores 0.847 against the +reader's own on doubled marks and 0.769 on single ones, both above what ships today (0.788 and +0.623) and above every resized-guess candidate ㉖ scored — because this one is not guessing a +size, it is reading one off the print. + +Second, against the marks nobody is arguing about: the same rule was run over every mark on +sixty pages spread through the book — 31,805 marks, about a tenth of the corpus, of which +31,773 are marks the ink search already places with confidence today. On those, the rule's +answer sits a median of 0.077 units from what ships, with 99.2% moving less than half a unit — +close enough to call it the same answer. The 791 already-accepted doubled marks among them +show the same thing from the other side: their size barely moves (width −0.075, height −0.088, +against the several-unit corrections the refused doubled marks needed), which is the direct +answer to what ㉖ left owed — the accepted doubled marks were not secretly wrong all along, so +a fix aimed at the refused eighteenth of a percent will not quietly move the rest. + +The 32 refused marks in the same sixty pages moved the way ㉖ predicted: a median of 1.47 +units, four in five moving more than half a unit. + +**What this is not.** It is not a shipped fix, and it is not scored on marks nobody has looked +at by eye except through this rule's own selection — the check above says the rule *agrees +with the reader where a reader has looked*, not that it is right where nobody has. It has not +been run as an escalation (fire only where the current rule refuses) and diffed against the +production output the way ㉔ and ㉕ were, which is the check that would make the safety claim +about the accepted 99.9% a guarantee rather than a sample. That is the next step, tracked +alongside the rest of this item's population split. + +### ㉙ Run as an escalation over every mark the book currently refuses, the rule mostly holds up and one way it does not · **confirmed** + +The step ㉘ left owed. The current corpus refuses exactly 329 marks — the same population ㉖'s +table counts, so nothing has drifted between the two — and the rule was run as an escalation: +fired only where the shipped rule already refuses, never touching the other 326,186. That gate +is a fact about the code, not a sample of it, so **the accepted population's answers are +unchanged by construction**, not by having been checked and found unchanged. + +Of the 329, the rule finds ink to draw from for 304. The other **25 have nothing under them +that the rule can point to** — no piece of print whose middle falls inside the rectangle we +already ship — and would have to keep today's rectangle rather than take a guess. Eleven of +those twenty-five are hamza, out of seventy-one hamza in the refused population; four are marks +already named in the print-oddities list above, which is the same kind of cross-check ㉖ ran +against its own two killed theories — the marks the rule cannot answer for are disproportionately +the marks something is already known to be wrong with, not a random slice. + +**Checked against the one place a reader's own answer exists for this exact population** — the +thirty-three doubled marks from +[㉖'s sitting](../validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json), +matched mark for mark rather than compared as two +separate averages. Typical agreement is close: half of the thirty-three land within 0.064 units +of what the reader drew, on both width and height together. But five do not, and one is not a +near miss — on one mark the reader barely touched the size (0.04, −0.20) and the rule proposes +growing it by (4.79, 12.35), more than a whole extra mark's worth of print. That is the union +sweeping in a piece of ink the reader did not mean to include, and nothing in the rule as +written would have stopped it. Four more disagree by two to four units, in both directions — +sometimes the rule grows where the reader shrank. + +**What this changes about shipping it.** The zero-regression claim now holds as a guarantee, not +a sample — that part of ㉘'s open question is answered. The other part is not: the rule needs +something that refuses a piece union that has grown implausibly large relative to what we +already ship, the same way it already refuses to guess when it finds no ink at all, before the +outlier above stops being a risk on every mark rather than a known one on this one. + +### ㉚ The same sitting, sat the rest of the way, says the outlier in ㉙ was not one mark · **confirmed** + +The sitting behind ㉖ and ㉙ was only two thirds sat — sixty-one of its ninety marks. It has +since been finished: the same reader, the same report, continued rather than re-dealt, +[ninety of ninety now settled](../validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json) — +the file this paragraph names is that finished state, and everything from ㉖ through ㉙ above +was written against the sixty-one-mark state it replaces. **Do not re-read ㉙'s numbers as wrong** — they were an honest count of +a smaller population, and this is what the fuller count says instead. + +The doubled marks with a reader's answer in this population grew from thirty-three to +forty-five. Matched the same way, mark for mark: the worst case is the same mark ㉙ already +named (`274:59`, reader (0.04, −0.20) against the rule's (4.79, 12.35)) — nothing new sat +disagreed worse than that one already on record. But it is no longer alone in the way ㉙ +suggested. Where ㉙ counted five of thirty-three disagreeing by two units or more, the finished +count is **seventeen of forty-five** — the union sweeping in ink the reader did not draw +happens on more than a third of doubled marks, not one in six. Single marks stayed as solid as +before: two of forty-four disagree by that much, and the typical single mark is within half a +unit on eighty-nine of them. + +**What this changes.** ㉙ already said the rule was not shippable without a guard against an +implausibly large piece union. This does not change that conclusion — it changes how load- +bearing the guard is. A problem on one mark in six can be shipped cautiously and watched; a +problem on more than one mark in three cannot ship at all until the guard exists, because it is +no longer the tail of the distribution, it is a third of it. + +**The same sitting also caught four more marks a reader called odd in the print** rather than +in our rectangle — one successive dammatan, one hamza, one small yeh, one successive +fathatan, on four pages between 115 and 260. Folded into the running note on item ⑭ above, +which they extend rather than reopen: nothing here says the answer, only that it is still being +asked in the same shape. + +### ㉛ The marks that ran out of room and still shipped as placed were never looked at, until now · **confirmed** + +㉕ found the code bug: a mark whose search ran out of room could still slip past the check meant +to catch that and ship as if its own ink had confidently answered. The bug is fixed, but fixing +it never asked the question it makes askable for the first time — of the marks that still run +out of room and still clear the bar today, are the rectangles actually right? Nothing had looked. +That population, on the corpus as it stands today, is 339 marks. + +Two sittings, drawn the same way as ㉖'s and ㉙'s, forty marks each, eighty of the 339 seen so +far. **Every one of the eighty carries a complaint.** Unlike ㉗'s tally, where a tap set both +words at once and inflated the count, here the movement is mostly real: seventy-nine of the +eighty moved the rectangle by half a tenth of a unit or more, seventy-three had their size +genuinely changed, and seventy-two needed both. This is not the tap artefact ㉗ named — it is +eighty separate readers' judgements that the box was actually wrong. + +**But it is a small kind of wrong, not the fallback population's kind.** The reader moved these +by a median of 0.676 units, worst 3.231 — real, but far short of the fallback set's multi-unit +corrections. The size change is smaller still: a median of 0.07 to 0.09 units either way on +boxes some five units across. Nobody called a page odd in the print, and nobody banked a mark as +unclassifiable. These are rectangles that are a little off, on almost every mark, not rectangles +that are badly out on a few. + +**What this says about ㉕'s own number.** ㉕ reported that the boundary-hit marks it had been +undercounting matched at a median of 0.859 against 0.909 for a mark the search is happy with — +close enough to read as *nearly as good*. A reader now says that gap is not nothing: it is a +rectangle every one of eighty checked wanted moved and reshaped, just not by much. A search +score near the floor was never a guarantee, and this is the first direct evidence of what the +gap between 0.859 and 0.909 actually costs a reader. + +**What is not yet known.** 259 of the 339 have not been sat. Whether the fault holds at the same +rate and the same size across the rest of the population, or whether these eighty happened to be +the worst of it, is exactly what the next sitting would answer — and unlike the fallback +population, which is already known to be almost entirely wrong, this is a population that ships +today as trusted and has never had a rate put on it at all. + +**Where the eighty answers actually are.** +[`docs/validation/rulings/2026-08-17-placement-edge-still-placed.seed23.settled.json`](../validation/rulings/2026-08-17-placement-edge-still-placed.seed23.settled.json) +carries all eighty, mark by mark — the box we ship, the box the reader settled on, and how far +apart the two are. It has no ink and no scripture in it, only page numbers, mark indices and +offsets, which is what a ruling is allowed to hold. It cannot show the rectangle sitting on the +mark; only a page that draws the print itself could do that, and the next section says plainly +that no such page exists yet for this document to point to. + +### ㉜ What should refuse a piece union too large to trust · **open** + +**None of the rule ㉘–㉚ describe has been written yet.** Everything above was scored from a +script that was run twice and never checked in — once as a sample, once as an escalation — so +there is nothing today for a guard to attach to. Designing the guard means designing the whole +rule's shape at the same time, not adding a clause to something already shipping. + +**Why this cannot wait for someone to feel like writing it.** ㉙ already said the rule was not +shippable without a guard against a piece union that has grown implausibly large. ㉚ found that +this is no longer a rare failure to watch for — it is what happens on more than a third of +doubled marks. Of the 229 marks nobody has ever sat, this rule already answers 216 without a +person, but every one of those answers is only as trustworthy as the guard that has not been +designed yet. Leaving it undesigned does not leave the rule cautious; it leaves it unusable, +because there is no way today to tell a good answer from one that swept in a neighbour's ink. + +**The shape of the guard.** The rule already has one refusal built into its own description: when +no piece of ink under the search window has its middle inside the rectangle we ship today, there +is nothing to point to, and the rule must keep that rectangle rather than guess. Twenty-five of +the 329 marks the corpus refuses fall into exactly that case. The size guard is the same shape of +decision, aimed at the opposite failure — a piece was found, but the box drawn around it grew far +past anything the rectangle we ship today would suggest. When that happens, the guard should +throw the answer away and keep the shipped rectangle, exactly as if no piece had been found at +all, rather than let a reader see a box that swept in ink they never meant to include. + +**What "far past" should mean.** A fixed number of units is the wrong measure on its own, because +a doubled mark's box and a single mark's box are not drawn at the same size to begin with — the +same growth that is enormous on one is ordinary on the other. Measured instead as how many times +larger the candidate's area is than the rectangle already shipping, the numbers already collected +point at a gap rather than a line: the forty-five doubled marks with a reader's own answer agree +with the rule at a median of 0.064 combined units when they agree at all, close enough to call the +same box, while the worst case (`274:59`) grew from a barely-touched correction to a candidate +(4.79, 12.35) units larger on a box some five units across — several times its own shipped area. +Between those two there is room for a guard; where exactly in that room is not yet known, because +the sitting behind ㉚ recorded whether each of the seventeen worst marks disagreed by two units or +more, not the size of the candidate box itself, so the individual area ratios for anything but the +worst case have never been pulled out and looked at. + +**What would confirm this or kill it.** Before this ships as a threshold rather than a shape, it +needs checking the way ㉖'s own theories were checked, not assumed: run a candidate area-ratio +cutoff against all forty-five doubled marks that already carry a reader's answer, and see two +things separately — does it catch `274:59` and the other marks ㉚ counted as disagreeing by two +units or more, and does it stay quiet on the marks that already agree closely, the ones ㉙ called +the accepted population's safety guarantee. A cutoff that does both is a candidate to escalate the +way ㉙ escalated the rule itself, over the full 329; a cutoff that has to choose between the two — +catching the bad marks only by also refusing marks that were already fine — means area alone is +not separating the two populations, and the guard needs a second measure alongside it, not a +bigger number on the same one. + +**What this does not settle.** A shape and a starting number to test are not a shipped guard, and +this item settles neither the exact cutoff nor the re-deal question left open above — whether to +run the rule, once guarded, on the 216 of 229 never-sat marks it could answer, or to wait until +the guard itself has been checked against ground truth first. Both remain for whoever writes and +checks the rule this designs. + +### ㉝ The guard ㉜ describes catches one disagreement in four; read both ways, it catches three · **confirmed** + +**What was run.** The piece-union script ㉜ needed but never had was rebuilt from ㉘'s own description +rather than recovered, since nothing of the twice-run original survives, and scored against all 89 marks +that carry a reader's settled answer from the sitting behind ㉚ — not only the 45 doubled marks ㉜ names, +because ㉜'s own test has a second half, staying quiet on the marks that already agree, and those marks +are not all doubled. The corpus has moved since ㉜ was written, the same drift ㉛ already found once: the +89 split 40 doubled and 49 single, not the 45 doubled the earlier text assumed, reported as found rather +than forced to match, the way ㉚'s own artifact reported 40 against a stated 45. + +Seventeen of the 89 — 16 of them single marks — have no piece of ink for the rule to point to at all, and +are out of scope for an area-ratio cutoff by construction: nothing was united, so there is no ratio to +take. The other 72 (39 doubled, 33 single) each carry a candidate box and a ratio against the rectangle +already shipping. + +**The guard exactly as ㉜ describes it — kill.** ㉜ proposes refusing a candidate only once it has grown +past the shipped rectangle. Tried at every ratio from 1.5 to 3, that catches at most 5 of the 20 marks +that disagree with the reader by two combined units or more — one in four — because 15 of those 20 +disagreements are candidates that came out *smaller* than the shipped rectangle, not larger. A guard that +only watches for growth cannot see a shrink no matter where its number is set, and most of what ㉜ needs +caught is a shrink. + +**The same measure, read both ways — confirmed.** Refusing a candidate whenever its area sits far from +the shipped area *in either direction* — grown past roughly double, or fallen under about half — catches +15 of the 20 disagreeing marks (three in four) while wrongly refusing only 1 of the 52 marks that were +already fine (the single false refusal is `578:260`, a fatha mark whose grown candidate the reader +accepted anyway). Doubled and single marks hold up close to evenly: 10 of the 13 disagreeing doubled +marks are caught, with no false refusals among the 26 doubled marks that already agreed; 5 of the 7 +disagreeing single marks are caught, against that one false refusal among the 26 singles that agreed. + +**What this settles, and what it does not.** ㉜'s own question is answered: area alone does separate the +two populations, but only once the guard is symmetric rather than one-sided — the shape ㉜ proposed was +built to catch a mark growing implausibly large, and never asked what to do with one that shrank, which +turns out to be most of the failure. It does not pick the exact cutoff; where the ratio has to grow past +roughly double or fall under about half is where this run's own 89 marks happen to sit a line, not a +number checked against any wider population, and moving it trades a few points of one figure for the +other rather than changing the picture. Nor does it touch the re-deal question ㉜ left open — whether to +run a guarded rule on the 216 of 229 never-sat marks it could answer, or wait for the guard itself to be +checked against ground truth first. This is that check. Running the result on the untested population is +still someone else's call. + +### ㉞ Doubling the ground truth moves the cutoff, not just confirms it · **confirmed** + +**What was run.** ㉝ scored 89 marks and said plainly its cutoff was "a line, not a number checked +against any wider population." A second wrong-size sitting has since finished — +[weak-size-part3](../validation/rulings/2026-08-20-placement-weak-size-part3.seed23.settled.json), +91 more marks drawn from the same 329-mark refused/weak population weak-size-part1 itself came +from — so this reruns ㉝'s own check against both sittings together, 181 marks with no id repeated +between them, in place of 89. The comparison this time comes from a script committed rather than +run once and lost — `score-piece-union-guard.mjs` — for the reason ㉜'s and ㉝'s own numbers had to +be reconstructed from scratch-directory tables rather than rerun: the script behind them never +survived. + +**The shape holds at twice the size.** 145 of 181 marks carry a candidate box; the other 36 have no +piece to point to, the same "nothing united" case ㉜ already excludes by construction. Of the 145, +39 disagree with the reader by two combined units or more — proportionally close to ㉝'s 20 of 72 — +and 106 agree closer than that. At ㉝'s own cutoff (ratio ≥ 2 or ≤ 0.5) the guard catches 30 of the +39 (77%, against ㉝'s 75%) while wrongly refusing 2 of the 106 (1.9%, against ㉝'s 1 in 52). Nothing +about the earlier finding breaks under twice the ground truth. + +**But the cutoff itself was not as low as it could go.** Sweeping the same ratio from 1.5 to 4 turns +up a number that strictly beats ㉝'s: at 1.75 (grown past three-quarters again, or fallen under +four-sevenths) the guard catches 34 of the 39 disagreeing marks (87%) for the same 2 false refusals +out of 106 that 2.0 costs — doubled marks 21 of 24 caught against 1 false refusal in 46, single +marks 13 of 15 caught against 1 in 60, both a shade ahead of what 2.0 caught on the same split. +Looser than that, 1.5 buys four more catches (38 of 39, 97%) but nearly quadruples the false +refusals, to 7 of 106 (6.6%) — a worse trade, not a better one. 1.75 is not a guess between two +round numbers; it is the point past which loosening further starts costing more than it buys. + +**What this settles, and what it does not.** The cutoff moves from ㉝'s 2/0.5 to **1.75/0.571**, on +double the ground truth ㉝ itself asked for. It does not touch the re-deal question either item left +open — whether to run the now-twice-checked guard on the 216 of 229 never-sat marks it could answer, +or hold for weak-size-part2's roughly 90 still-unsat marks first, which would put a third of the +329-mark population's own ground truth behind the number rather than a little over half. + +### ㉟ The re-deal, made: run the checked guard now, and record what it decides · **confirmed** + +**The call ㉜, ㉝ and ㉞ each left open is now made: run the guard.** Three items in a row said the +same thing — the guard has been checked, but running its verdicts on the marks nobody has ever sat is +"still someone else's call." That someone has answered: run it now, rather than hold for another +wrong-size sitting first. The reasoning is the one ㉞ already put on the table and did not act on — +the cutoff held under a *doubling* of the ground truth it was fitted on, so a third sitting would move +it less than the second did, while it is itself a person's twenty minutes that blocks nothing else. +Waiting buys a sliver of certainty at the cost of leaving the rule unusable, which ㉜ already called +the worse of the two. + +**What running it decides, counted.** The guard was applied at 1.75/0.571 to every refused mark that +carries a piece-union candidate and that no reader has ever sat. "Never sat" has two honest readings +and the answer barely moves between them: counting only the wrong-size sittings drawn from this +population, **195 marks were never sat, and the guard trusts the candidate on 142 of them (73%) and +keeps the shipped rectangle on 53 (27%)**; counting *any* sitting that has ever touched a refused mark, +131 were never sat, and the split is 98 to 33 (75% / 25%). The accept rate holding at roughly +three-quarters whichever way "never sat" is read is itself the reassurance — the yield does not depend +on where the line between sat and unsat happens to fall today. + +**The refusals are catching what the guard exists to catch.** Of the 53 it refuses under the first +reading, 38 are candidates that *shrank* under 0.571× and 15 that *grew* past 1.75× — the same +shrink-dominated shape ㉝ found, not a growth story. And the worst of them are not marginal: the five +most extreme refused candidates carry area ratios of 29.6, 20.5, 18.2, 14.2 and 7.7 — a shadda and a +run of fathas whose union plainly swept in a neighbour's ink, exactly the failure ㉚ named. These are +the 53 marks where, without the guard, the rule would have drawn a box nobody meant; with it, they +fall back to the rectangle the app ships today rather than a wrong one. + +**What this settles, and the one thing it deliberately does not.** It settles the re-deal: the guard +runs, the 142 accepted marks are the corrections it endorses without a reader, the 53 refused marks +keep their shipped rectangle, and the earlier framing — "hold for weak-size-part2" — is closed, more so +than ㉞ knew, since far more of the population has since been sat than the two sittings ㉞ counted (209 +refused marks now carry a reader's answer, not 145). It does **not** wire these 142 corrections into +what the app draws. That is the same act as adopting option H of the still-open `mark-placement` +decision — putting each mark where its own ink is — and that decision is the owner's to rule, not this +measurement's to pre-empt. This item is the evidence that ruling needs: the guard's yield, on the marks +no human will ever check, is 73% trusted and 27% caught. Wiring waits on the ruling; the number does not. + ## How can someone look at this for themselves? The measurement writes a page of evidence: the worst verdicts first, each one drawn at a size @@ -873,14 +2943,46 @@ only honest answer. The scorer reports all four numbers, and refuses to score at displacements were re-measured after the page was built. ``` -node packages/etl/scripts/probe-mark-ink.mjs --sample 4000 --seed 7 --pages-n 40 -pnpm adjudicate:marks --seed 11 --count 100 -# work packages/etl/out/mark-adjudication.html in a browser, save the ruling it offers -pnpm adjudicate:score ~/Downloads/mark-ruling-11.json +pnpm adjudicate:marks --seed 11 --count 100 \ + --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json \ + --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json --pages 40 \ + --out packages/etl/out/mark-adjudication.heldout.html +# work that page in a browser, save the ruling it offers +# then move it into docs/validation/rulings/ and score it from there +pnpm adjudicate:score docs/validation/rulings/ \ + --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json ``` -Like the evidence page, it is written to the untracked output directory and never committed, -for the same reason: it draws the mus'haf's own ink. +**Read the middle line carefully, because everything else inherits what it decides.** A session +can only offer a mark on a page that has a proposed move, so for as long as the correction +covered forty pages, every by-eye answer anybody could give was about those forty — the pages +the correction was fitted to. That is the same limit §⑧ exists to fix for the placing session, +and it applies here just as hard: the correction is going to ship on six hundred and four +pages, so the useful question is whether a reader prefers it on pages the fit has never seen. +Holding out the fitted forty and keeping forty of the rest is what those two flags do. + +Both commands name the corrections file rather than taking whatever is loose in the build +directory, and they name the same one. A ruling's answers belong to the displacements it was +worked against; both scorers refuse a ruling whose displacements have moved underneath it, and +the scorer refuses a second time if the file it is handed has no row for a page the sitting +actually used — a file can carry the right fingerprint and still be the wrong file for these +pages. + +That fuller pass has since run over all 604 pages, so a session can now be built on pages the +correction was never checked against: the placing builder takes the pages to draw from as an +argument, and will hold out the pages an earlier session used by reading that session's own +record of which pages it drew from. Its scorer replays that recorded list rather than choosing +again, so improving how pages are picked can never quietly re-score a sitting somebody has +already worked. It refuses on either of two fingerprints — the displacements, and the pages — +and refuses a third time if the displacements it is handed have no row for a page the sitting +used, because a file can carry the right fingerprint and still be the wrong file for these +pages. + +Like the evidence page, that page is written to the untracked output directory and never +committed, for the same reason: it draws the mus'haf's own ink. What a reader *answers* does +come home, to [`docs/validation/rulings/`](../validation/rulings/) — page numbers, mark indices +and offsets, no ink and no scripture — because a verdict whose working lives in one person's +downloads folder is a verdict nobody else can argue with. ## Where does this live? @@ -898,24 +3000,88 @@ readable without them. no answer key is ever written down. - `packages/etl/scripts/build-mark-adjudication.mjs` — renders that session to one page, carrying no verdicts, no answers and a fingerprint of the displacements it was built from. + `--exclude` names the sittings or fits whose pages must be held out and `--pages` how many of + the rest to keep, both handed to `selectPages` with `even` rather than `extremes`; the list it + settles on goes into the page's own head with its own fingerprint, and that fingerprint is + folded into the resume key and the download name, because two sittings from one seed that + asked about different pages would otherwise share both. - `packages/etl/scripts/score-mark-adjudication.mjs` — rebuilds the key from the seed and - reports the four numbers. This is the first moment anybody knows what the answers were. + reports the four numbers. This is the first moment anybody knows what the answers were. It + replays the recorded page list rather than choosing again: the trials are rebuilt here, so a + session narrowed at build time and scored unnarrowed would put every trial index against a + different mark and throw nothing. - `packages/etl/scripts/lib/marks.mjs` — one reader for a page's marks, shared by the measurement and the session so the two cannot disagree about what a mark is. - `packages/etl/scripts/lib/adjudication.mjs`, `planNudge` — the placing session: which marks, how far out each rectangle starts, and which ones come round twice. Same shape as the session above, and for the same reason: rebuilt from the seed, so the correction it is measured against is never written down anywhere the reader could reach. +- `packages/etl/scripts/lib/adjudication.mjs`, `selectPages` — which pages a sitting may draw + from: hold out the ones an earlier sitting used, then take from the ends of the spread on + both axes rather than at random, because leverage and not sample size is what makes the size + of the correction readable. The pages it returns are written into the session and replayed + when it is scored, so changing this function cannot re-score a sitting somebody has worked. - `packages/etl/scripts/build-mark-nudge.mjs` — renders that session to one page. It carries the ink, the rectangle and where the rectangle starts; the proposed correction reaches no - markup, no attribute and no comment on it. + markup, no attribute and no comment on it. `--exclude` takes the sittings whose pages must be + held out, `--pages` how many to keep; it writes a companion file naming the pages it drew + from, in the same shape a displacements file uses, so the next sitting can exclude it with no + second format. The pages chosen carry their own fingerprint, folded into the key the browser + resumes from — two sittings from the same displacements and seed but different pages would + otherwise have stacked one's answers onto the other's trials. `--reader` names whoever is + sitting it and is the only thing here that never reaches a trial: a second hand is only + evidence if it saw the same marks in the same order from the same starting points. It is + folded into that same resume key and into the name of the file that lands in the downloads + folder, for the reason the pages were — otherwise the second person resumes into the first + one's answers and their download arrives as a copy of it. - `packages/etl/scripts/score-mark-nudge.mjs` — subtracts the proposed correction from where each rectangle was put and reports what is left, beside the precision of the hand that put - them, so a residual smaller than the wobble is called what it is. + them, so a residual smaller than the wobble is called what it is. It opens with **how many + of the 604 pages the correction covers**, and whether any placement was made on a page + outside them, because §⑧ is the limit every other number it prints has to be read against. + Each residual is reported twice, once counting every placement as its own fact and once + counting pages, and the verdict at the foot reads the second — see the library below. It + also reports whether the correction is the right *size*, with the spread of the proposed + moves beside it so a meaningless estimate is legible as one, and it prints what was ruled + out, so nobody buys those answers twice. Handed a second person's sitting on the identical + build, it also reports how far the two hands are from each other — mark by mark, against each + hand's own wobble, with no threshold — because that comparison is the only thing that + separates a print that is off by this much from one reader who places rectangles this way. It + refuses a second file from a different build, and refuses the same reader twice, which would + compare a hand with itself and come back looking like perfect agreement. +- `packages/etl/scripts/lib/placement-stats.mjs` — the arithmetic, kept apart so it can be + tested against data whose answer is known by construction. Two marks on one page are not two + facts about the fit: they share that page's frame, so an interval that counts them separately + is narrower than the truth. The measurement above had said so about its own sampling since + the day it was written; the scorer was built without it, and a residual was banked as real on + an interval that counting pages puts across nothing. `agreementOf` is the one estimator here + that compares two people rather than describing one, and it takes their difference mark by + mark for a reason: an average against an average calls two hands that alternate either side + of each other perfectly agreed. If either of them repeated nothing there is no wobble to read + the gap against, and it says so rather than inventing one. +- `packages/etl/scripts/lib/adjudication.mjs`, `sameBuild` — what has to match before two + sittings may be compared: same seed, same length, same displacements, same pages. It names + every mismatch rather than the first, and it refuses two sittings by the same person, which is + a hand compared with itself and the one failure here that nothing in the printed output would + betray. +- `packages/etl/scripts/lib/placement-stats.test.mjs` — where that is proved rather than + asserted: on ten pages of six identical values the honest interval must come out about two + and a half times the naive one, and on values that share no page the two must agree. Two more + are built to fail the tempting implementation of the agreement number — a disagreement that + alternates in sign must not average itself away, and a hand whose wobble was never measured + must come back as *cannot say* rather than as agreement. - [`docs/validation/ledger.json`](../validation/ledger.json), `placement-correction-by-eye` — the runbook of record for §⑩ ①, and where its verdict gets banked. - [`docs/validation/ledger.json`](../validation/ledger.json), `placement-residual-by-hand` — the same, for §⑩ ⑦. +- [`docs/validation/ledger.json`](../validation/ledger.json), + `placement-holds-off-its-own-pages` — the same, for §⑩ ⑧, and the one place its five + predictions are written down before anybody places a rectangle. +- [`docs/validation/rulings/`](../validation/rulings/) — the answers themselves, and beside + them the two displacement files a verdict has to be scored against: the forty-page + measurement both earlier sittings were built from, and the full 604-page pass everything + built afterwards uses. Committed because a ruling that can only be re-scored by somebody who + can first rebuild a gitignored file is a verdict nobody else can check. - `packages/etl/scripts/lib/mark-shape.mjs` — the canonical examples and the shape comparison, shared with the naming measurement so that both use one library of examples rather than two that could drift apart. This document uses it only to decide which marks are eligible. @@ -924,11 +3090,22 @@ readable without them. drawing of each mark beside the rectangles `readDiacritics` returns, sharing the same walk so the two cannot drift. - `packages/etl/data/pages/word-boxes.pin.json` — where a per-page correction would be - recorded under Option B. + recorded. A per-line one does not fit in it and needs a table beside it; §⑩ ⑨ holds that open. - [`sub-word-marks.md`](sub-word-marks.md) §⑦ — the question this answers. - [`encoding-inspector.md`](encoding-inspector.md) — the existing surface, and why this is beside it rather than inside it. - [`docs/map.json`](../map.json), feature `word-geometry` — the code pointers of record. +- `packages/etl/scripts/probe-piece-union.mjs` — ㉘'s rule, rebuilt: unions the ink pieces whose + middle falls inside a mark's shipped rectangle into a candidate box, over the same search + window `probe-mark-ink.mjs` used. `--set refused|weak|edge|all` scores a population; + `--ids page:k,...` scores an explicit list, for checking the rule against a sitting's own + ground truth rather than whatever the corpus currently calls that population. +- `packages/etl/scripts/score-piece-union-guard.mjs` — ㉝'s and ㉞'s check, kept runnable: reads + one or more settled rulings and a piece-union file, compares each mark's reader-settled size + change against the rule's proposed one, and reports a cutoff's recall against the marks that + disagree and its false-refusal rate against the marks that already agreed. `--sweep` prints + that pair across several candidate cutoffs at once, which is how ㉞ found 1.75 rather than + assumed it. Reproduce every number in this document with: diff --git a/docs/design/page-bar-options.html b/docs/design/page-bar-options.html new file mode 100644 index 00000000..e27c4a78 --- /dev/null +++ b/docs/design/page-bar-options.html @@ -0,0 +1,173 @@ + + + + + +Two questions about the page bar + + + +
    +

    Two decisions, still open

    +

    Two questions about the page bar

    +

    The bar along the bottom of the app is how a reader scrubs through the whole printed book. It grew thirty green marks, one where each juz begins, and a bubble that names the juz and the surah under the thumb while you drag. Two things about those marks were built one way without anyone choosing, and this page draws the choices at the size the bar is really used.

    + +

    A few words, defined once

    +
    +
    Juz
    One of the thirty roughly equal parts the book is divided into for reading it over a month. A juz begins at a fixed verse, not at a page.
    +
    Marker
    The short green tick on the bar at the page a juz begins on. Thirty of them.
    +
    The bubble
    The dark label that floats above the thumb while you drag: the page number, then the juz and surah of that page, then — if the page is not in this build — the nearest page that is.
    +
    Landmark, magnet
    A landmark is something you steer by; a magnet is something that catches you. The first question is which of these a marker is.
    +
    + +

    How wide is a page on the bar?

    +

    The bar spans all 604 pages of the print. Read off the app's own stylesheet, the thumb travels:

    + + + + + + +
    WindowTrackOne pagePages under a fingertip (26 px)
    Laptop, 1280 px wide (the bar is held to 960 px)832 px1.34 pxabout 19
    Phone, 390 px wide262 px0.4 pxabout 65
    +

    So a drag cannot be aimed at one page on either device, and on a phone it cannot be aimed at one juz's neighbourhood either. That number is most of the argument on this page.

    + +

    1 · When a reader lets go near a marker, should the bar pull the page onto it?

    +

    Today it does not. The markers are drawn under the thumb and cannot be touched; a release lands on the page the thumb is over, and the bubble says which. There are three exact roads to a juz that do not involve the bar: on a laptop, the wheel with Shift held jumps a juz; the revision map has a cell per juz that opens it; and the jump box takes "juz 9". The question is whether the drag should be a fourth.

    +

    Each option is drawn at the laptop width and the phone width, released at page 60 — two pages before juz 4 begins on page 62.

    +
    + +
    +
    A

    A marker only marks

    today
    +
    +

    What the bar does now. Letting go lands on the page under the thumb — 60, two before juz 4 — and the marker is a landmark you steer by, not a thing that catches you.

    +
    takes
    Nothing.
    gets
    Every page is reachable by drag, and a release means exactly where the thumb is.
    costs
    You cannot reach a juz opening by dragging: a page is 1.34 px wide on a laptop and 0.4 px on a phone, so the drag ends somewhere within a fingertip — about 19 pages either way on a laptop, 65 on a phone. Three other roads land exactly (below).
    +
    Page 60 of 604Juz 3 · Ali 'Imran
    Released at page 60, two before juz 4: lands on 60
    Page 60 of 604Juz 3 · Ali 'Imran
    Released at page 60, two before juz 4: lands on 60
    +
    +
    +
    +
    B

    A marker pulls, a few pages either side

    +
    +

    Let go within 3 pages of a marker and the bar lands on the juz's first page instead, and the bubble says so before you let go — the same sentence it already uses when a page is not in this build.

    +
    takes
    A reach in pages, and the bubble's existing "nearest page" line reused for "juz 4 begins on page 62".
    gets
    A juz opening becomes reachable by drag, at the size a finger can actually hit.
    costs
    6 pages around each of 30 markers — 180 pages, 30% of the book — cannot be landed on by drag; the arrows still reach them. On a phone the reach is 2.39 px wide, a tenth of the thumb, so on a phone it is closer to "the marker wins whenever you are near it".
    +
    Page 60 of 604Juz 4 · Ali 'ImranNearest juz · Page 62
    Released at page 60: pulled onto page 62, and told so first
    Page 60 of 604Juz 4 · Ali 'ImranNearest juz · Page 62
    Released at page 60: pulled onto page 62, and told so first
    +
    +
    +
    +
    C

    A marker is a button; the drag is unchanged

    +
    +

    Tap or click a marker and the book opens at that juz, with the announcement the wheel already makes. A drag released beside a marker still lands where the thumb is.

    +
    takes
    Thirty small buttons on the rail, each named, and the marker's hit area widened to the touch minimum — which on a phone overlaps its neighbours' drag space.
    gets
    An exact road to a juz that is visible on the bar, without changing what a release means.
    costs
    A 2 px mark with a 44 px hit area, thirty times, on a track where a page is 0.4 px: thirty touch targets want 1320 px of a 262 px phone track, 5.0 times what there is, so on a phone every drag starts on a button. The bar already has a keyboard road and the map already has a cell per juz.
    +
    Juz 4 · page 62
    Marker 4 pressed: the book opens at page 62
    Juz 4 · page 62
    Marker 4 pressed: the book opens at page 62
    +
    +
    +
    + +

    What would change this answer?

    +
      +
    • A reader saying they reach for the bar to get to a juz and miss. Nobody has yet; the bar is a week old.
    • +
    • The phone. Everything above is arithmetic on a 390 px window; a hafiz's thumb on a real one is the measurement.
    • +
    • The map growing a hizb layer would make the bar's markers the coarse one twice over, and a magnet on thirty marks is a different thing from a magnet on sixty.
    • +
    + +

    2 · When a juz begins partway down a page, which juz is that page in?

    +

    Twenty-six of the thirty juz begin at the top of a page. Four begin partway down one, so that page carries the end of one juz and the start of the next:

    + + + + +
    JuzBeginsOn pageAyat of the juz beforeAyat of this juzThe page
    4Ali 'Imran 3:936218
    7Al-Ma'idah 5:8212142
    11At-Tawbah 9:9320161
    26Al-Ahqaf 46:150255
    +

    The app answers this two ways today. The bar's bubble says the page is the juz that begins on it, because that is the juz whose marker sits on that page. The offline-pack shelf and the wheel's "no juz that way" message say it is the juz that was already running, the lowest juz with any verse on the page. On 600 pages the two agree; on these 4 they do not.

    +

    Each option is drawn as the bubble over page 62, which carries 1 verse of juz 3 and 8 of juz 4.

    +
    + +
    +
    A

    The juz that begins on it

    today, in the bar
    +
    +

    The bar's answer today: page 62 is "juz 4", because juz 4 opens there and that is what its marker marks. The page carries 1 ayah of juz 3 and 8 of juz 4.

    +
    takes
    Nothing in the bar. One line elsewhere, to make the pack shelf and the wheel's edge message agree with it.
    gets
    The marker, the bubble and the juz jump all name the same page the same way.
    costs
    On page 201 the bar would say "juz 11" for a page that is 6 ayat of juz 10 and 1 of juz 11.
    +
    Page 62 of 604Juz 4 · Ali 'Imran
    Page 62 under the thumb
    +
    +
    +
    +
    B

    The juz that was already running

    today, in the pack shelf
    +
    +

    The other answer the app gives today: page 62 is "juz 3", the lowest juz with any ayah on it, which is how the offline-pack offer and the wheel's "no juz that way" message read it.

    +
    takes
    One line in the bar, and the marker for a mid-page juz then sits on a page the bubble calls the previous juz.
    gets
    Whatever is at the top of the page is what the page is called — the reader opening it meets that juz first.
    costs
    The marker and the bubble disagree on four pages: the tick says "juz 4 starts here" and the line under the thumb says "juz 3".
    +
    Page 62 of 604Juz 3 · Ali 'Imran
    Page 62 under the thumb
    +
    +
    +
    +
    C

    Both, on those four pages

    +
    +

    Say what is true: "juz 3 → 4" on a page that carries the seam, and one juz everywhere else.

    +
    takes
    A second way of writing the line, in both languages, for four pages of 604.
    gets
    No page is misnamed, and the seam is visible from the bar without opening the page.
    costs
    A wider bubble on four pages, and the pack shelf and wheel still need to pick one of the two for their own sentences.
    +
    Page 62 of 604Juz 3 → 4 · Ali 'Imran
    Page 62 under the thumb
    +
    +
    +
    + +

    What would change this answer?

    +
      +
    • What the printed mus'haf itself says. The Madani print names the juz in the running head of every page, and on these four pages it must have picked one; the owner has a physical copy, and nobody has yet turned to page 62 and looked. Whatever it says is the convention a hafiz already carries.
    • +
    • A hizb layer. Hizb boundaries fall mid-page far more often than juz ones, and an answer that is a special case for four pages will not survive sixty.
    • +
    + +

    What is this page not settling?

    +
      +
    • Whether the bar should show hizb marks at all. That is a bigger question, and the map answers it first.
    • +
    • What a marker looks like. The tick's size and colour were chosen so the thirty read apart from the inventory rail, not for this.
    • +
    • Which page a juz jump lands on. It lands on the page the juz begins on, and this page takes that as given.
    • +
    + +

    Built from the vendored print's page-of-every-verse table, the juz table, and the bar's own stylesheet; every number above is recomputed when the page is rebuilt. Nothing on it is scripture — the bar is chrome, and it is drawn in the app's English mode.

    +
    + + diff --git a/docs/design/revision-record.md b/docs/design/revision-record.md index 81447a21..963ca0a6 100644 --- a/docs/design/revision-record.md +++ b/docs/design/revision-record.md @@ -337,3 +337,21 @@ failure: dropping the call from `RevisionMap` fails only the page-scope test, an the `scope !== "page"` guard so it filters everywhere fails only the juz one. The fixture uses `quran/hafs-indopak/2:30` on purpose — an ayah inside hizb 1 and juz 1, divisions this build does hold paper for, so `absent` cannot answer for either assertion. + +### ④ Is the rule "nothing leaves the device," or "nothing leaves unless it serves the reader"? · **open** + +The privacy invariant and the *Export or sync* line under **Deliberately out of scope** both +state the stance as an absolute: nothing leaves the device. Reviewing the confusion-map export +decision, the owner sharpened it — the durable rule is not that nothing leaves, but that nothing +leaves *unless it is in the reader's interest, and under their control*. A flat *nothing leaves* +is a promise that breaks the first time the reader's own interest points outward — wanting a +hard-won record to survive a lost phone — and once broken it reads as abandoned; naming the test +instead keeps the promise. This does **not** loosen `gate:revision-privacy`: the gate exists to +make anything leaving a *deliberate* act rather than a convenient one, and "in the reader's +interest, under their control" is exactly the shape of a deliberate act — the gate stays, the +sentence it defends gets the truer wording. **What would answer it:** adopting the sharper +wording here and in the invariant's prose, so a future export/sync question is weighed by whether +it serves the reader rather than refused by reflex. The confusion-map export decision +(`confusion-map-export`, and open question ② in the confusion-points design) is the first place +this is being applied; this item exists so the revision-record framing is revisited against it +rather than left standing as an absolute the app no longer holds. diff --git a/docs/design/sitting-hosting.data.json b/docs/design/sitting-hosting.data.json new file mode 100644 index 00000000..ec91e388 --- /dev/null +++ b/docs/design/sitting-hosting.data.json @@ -0,0 +1,57 @@ +{ + "$comment": "Measured findings behind the sitting-hosting options page. Written by scripts/build-sitting-hosting-options.mjs --extract, which reads build products and a running answer log that are deliberately not committed. Hand-editing this would make the page lie about its own evidence.", + "measured": "2026-08-17", + "deal": { + "parts": 16, + "pool": 1710, + "population": 1877, + "alreadyAnswered": 167, + "perPart": 106, + "seed": 23 + }, + "bytes": { + "total": 21357369, + "min": 1258110, + "max": 1425214, + "median": 1337951, + "limit": 16777216 + }, + "answers": { + "statements": 578, + "marks": 167, + "kinds": { + "wrong-shape": 201, + "placement": 360, + "looks-right": 2, + "print-defect": 15 + }, + "first": "2026-08-14T04:05:24.478Z", + "last": "2026-08-15T16:42:01.125Z", + "bytesPerStatement": 193 + }, + "card": { + "id": "20:132", + "page": 20, + "line": 4, + "name": "fatha", + "box": [ + 313.5, + 124.2, + 6.3, + 3.4 + ], + "at": [ + 310.754, + 122.357, + 6.3, + 3.4 + ], + "vb": [ + 296.75, + 108.36, + 34.3, + 31.4 + ], + "svg": "" + } +} diff --git a/docs/design/sitting-hosting.html b/docs/design/sitting-hosting.html new file mode 100644 index 00000000..51233df2 --- /dev/null +++ b/docs/design/sitting-hosting.html @@ -0,0 +1,504 @@ + + + + + +Where should the sittings live? + + + +
    +

    Decided on 2026-08-17 — the first option

    +

    Where should the sittings live?

    +

    + 1,710 marks are waiting to be looked at by a person, in 16 sittings of about + 106 — roughly 16 hours of somebody's attention. Every one of those hours needs a + particular laptop to be awake, in a particular house. This asked whether it should. +

    + +
    +

    Answered by Omar on 2026-08-17

    +

    So what was decided?

    +

    + The first option. The laptop keeps handing out the sittings, and the reader reaches them + from a phone over the household's private network. +

    +

    + What settled it was not a fresh weighing of three routes. The one thing that option genuinely costs — the + reader has to be somewhere that network reaches — was judged acceptable. The two things that made it + feel worse than it is turned out to be work nobody had done rather than facts about the + arrangement: reaching it from a phone at all depended on remembering something written down nowhere, and + the machine answers to two spellings of its own name, which a browser treats as two different sittings — + so a reader who came back at the other one was dropped at the first question with an hour apparently gone. + Both are closed. There is now one address, worked out rather than typed, and the page a reader lands on + says so when it notices it is being read at the wrong one. +

    +

    + Two things came with the choice, because a sitting on a phone is a sitting on a phone and both were broken: + the way in now says which sitting you are in the middle of and how far through it you are, and pressing the + same nudge twice is two answers rather than a zoom. +

    +

    + The other two options are left standing below unbeaten rather than beaten. Neither was tried; the first was + made good enough that neither had to be. What would reopen this is unchanged, and the cheapest of them is + still a sitting actually attempted away from home. +

    +
    + +
    +
    +
    Mark
    +
    One of the small signs printed above or below a letter — a fatha, a sukun, a madd — that tell a reader how the word is said.
    +
    Sitting
    +
    One session's worth of questions: about a hundred marks, each drawn on the real printed page, each asking whether the rectangle the app puts around it is in the right place.
    +
    Answer
    +
    What a reader says about one mark — that it looks right, that the rectangle is the wrong shape, that it is in the wrong place and here is where it should be, or that the print itself is odd here.
    +
    +
    + +

    What is one of these questions, actually?

    +

    + This, at about the size it appears on a phone. The printed ink is the mus'haf's own; the dashed rectangle is + what the app has decided the mark occupies. The reader is asked whether that rectangle is right, and can move + it, resize it, or say the print itself is unusual here. +

    +
    + + + + +
    + One card from the first of the 16 sittings — a fatha on page 20, + line 4. There are 1,710 more. +
    +
    + +

    Why is this being asked now?

    +

    + Because the work is about to get long. 167 marks have been looked at, over two days, + on a laptop in the same room as the reader. 1,710 are left, and asking for another 16 hours + of the same arrangement means asking somebody to be in one place, with one machine running, every time they + have twenty spare minutes. +

    +

    + Nothing is broken. The arrangement works and has been made steadily harder to get wrong. The question is + whether it will still be the right arrangement across the next 16 hours, or whether it is quietly the + reason those hours do not happen. +

    + +

    What would have happened if nobody had decided?

    +

    + The sittings would have carried on exactly as they are, and the cost is real but small: they happen when the + reader and the laptop are in the same house, and not otherwise. Nothing else was waiting behind this. No + feature was blocked, no other question depended on it, and the answers already given were not at risk under + any of the three. +

    +

    + Which is most of why the first option won. The remaining hours are going to happen at a desk + at home, so the honest choice was the one already working — and the work that went with it was spent making + that arrangement reach a phone properly rather than replacing it. +

    + +

    What does it cost today, measured?

    +
    +
    1,710marks still to be looked at
    +
    16sittings, about 106 each
    +
    167looked at so far
    +
    578things said about them
    +
    3.5said per mark, on average
    +
    1.3 MBone sitting, as a single file
    +
    +

    + Two of those numbers matter more than the rest. 3.5 statements per mark + is what a reader actually does: they nudge a rectangle a step, look, nudge it again, and each nudge is a + separate thing said. Anything that banks answers one at a time is doing it several times a mark, not once. +

    +

    + And 1.3 MB is the whole of a sitting — the questions, the printed ink around + each one, and the page that asks them, in one file that needs nothing else to work. That is what makes the + other two options possible at all: they are 1.2 MB to 1.4 MB apiece against a + 16.0 MB ceiling, so publishing one is not a technical question. +

    +

    + What has been said so far, by kind: 360 placement · 201 wrong shape · 15 print defect · 2 looks right. Measured over the answers given between 2026-08-14 and 2026-08-15. +

    + +

    What do other people do about this?

    +
    +

    + Nobody looked, this time. The comparable question — how projects that ask many people to + judge many pictures get those judgements home safely — has a large and well-worn literature behind it, in + crowd-judged science, in map correction, and in the tools built for labelling images. None of it was + consulted before this page was written, and the page is worse for it. +

    +

    + This was decided without it, which is worth saying plainly rather than leaving to be noticed. What is + worth looking up, if the question is ever reopened: how those tools handle a judgement given while the + device is offline, and whether they bank each judgement as it is given or in batches. That single detail + is what separates the three options below, and somebody has certainly measured it. +

    +
    + +

    What have we already decided that this has to live inside?

    +
      +
    • + The sittings exist to answer one open question — how the app should line up what it draws + against the printed page. Everything here is in service of that, and if that question were settled tomorrow + this one would evaporate. +
    • +
    • + What a reader does inside the app never leaves their device. That is settled, and it is + about the app. A sitting is not the app: it is a maintainer's instrument, shown to a handful of people who + know what they are agreeing to. But the same instinct should be brought to it deliberately rather than + forgotten, and the third option is the one that has to answer for it. +
    • +
    • + An answer already given is never at risk. Whatever is chosen, the 578 + statements already banked stay where they are, in a file that is only ever appended to. +
    • +
    + +

    What were the options?

    +

    + Each one is drawn as the journey a single answer takes, from the reader's finger to a line checked into the + project. The amber steps are the ones that can fail. Every option shows the reader the same card — what was + being decided is what happens in the seconds after they touch it. All three are left here as they were + written, the two that lost included: they are the reason the choice was a choice, and anybody reopening this + will want them. +

    +
    +
    + A +

    Leave it as it is — one laptop hands out the sittings over a private network

    + chosen +
    +

    The reader opens a sitting by its address on the household's private network. Every answer is posted back the instant it is given and appended to a file on that laptop, one line per statement — 578 lines so far.

    +
      +
    1. + The reader's phone + Opens the sitting by its address on the private network +
    2. +
    3. + The laptop + Hands over the page and a one-off pass to post back with +
    4. +
    5. + The reader's finger + Answers; the page posts it immediately +
    6. +
    7. + The laptop + Appends one line and flushes it before replying +
    8. +
    9. + The repository + A settling run turns the lines into one row per mark +
    10. +
    +
    +
    What it takes
    The laptop awake and on the network, and the reader on the same network. Nothing installed on either.
    +
    What it gets
    Every answer is banked the moment it is given, so a closed laptop or a killed terminal costs at most one statement. The count of what is left falls as the reader works. Rebuilding what nobody has sat is live immediately — the laptop re-reads the files per request.
    +
    What it costs
    It is one machine, awake, on one network. Restarting it mints a fresh pass and every page a reader already has open quietly stops banking — the answers keep appearing to send and stop arriving. And the reader must be somewhere that network reaches, which is the constraint this whole question is about.
    +
    +
    +
    +
    + B +

    Publish each sitting as a page, and hand the answers over at the end

    + +
    +

    Each sitting is already one self-contained file — 1.3 MB in the middle of the sixteen, against a 16.0 MB ceiling — so it can be published as a page and opened from a link, anywhere, with nothing running at home.

    +
      +
    1. + The reader's phone + Opens a link. No network to be on, no machine to be awake +
    2. +
    3. + The reader's finger + Answers; the page keeps it in that browser's own store +
    4. +
    5. + The same browser + Holds every answer until the reader reaches the end +
    6. +
    7. + The reader + Presses hand over, and moves the file back by hand +
    8. +
    9. + The repository + A settling run turns the file into one row per mark +
    10. +
    +
    +
    What it takes
    Nothing running anywhere. One link per sitting, and a way to get the finished file back.
    +
    What it gets
    The sitting can be done from any device in any place, and nobody has to keep a machine awake for it. Sixteen links is the whole of the hosting.
    +
    What it costs
    This is what was in place before the laptop was taught to receive, and it cost a reader an evening twice over. A browser's store belongs to one address: the same machine reached two ways is two memories of the same sitting, and neither can see the other. Nothing is banked until the end, so a cleared browser loses the hour. The count of what is left cannot fall until a file comes back.
    +
    +
    +
    +
    + C +

    Publish each sitting as a page, and let it bank each answer to a hosted table

    + +
    +

    The same published page, but each answer is written straight to a hosted table through the reader's own connection to it — no laptop in the path, and nothing waiting for the end of the sitting.

    +
      +
    1. + The reader's phone + Opens a link. No network to be on, no machine to be awake +
    2. +
    3. + The reader's finger + Answers; the page writes it to the table at once +
    4. +
    5. + The hosted table + Holds it. 578 statements so far, about 193 bytes each +
    6. +
    7. + Someone at a keyboard + Pulls the rows back down into the same running log +
    8. +
    9. + The repository + A settling run turns the log into one row per mark +
    10. +
    +
    +
    What it takes
    A table to write to, a shape agreed for what a statement looks like, and a step that pulls the rows back — none of which exists yet. The reader must have the connection to that table set up on the account they read with.
    +
    What it gets
    Everything the first option gets — answers banked as they are given, at most one statement ever at risk — with none of what it costs. Any device, any place, no machine awake at home.
    +
    What it costs
    The answers now live on somebody else's host, and a sitting is a scarce person's judgement about a printed page. A page that asks for that connection cannot be handed to just anybody — it is granted by the reader, per reader. It is the only option here that is not already working, and the one whose failure mode nobody has met yet.
    +
    +
    + +

    What else could be considered, and why is it not here?

    +
      +
    • + Publish the page, and have it post back to the laptop. The obvious middle, and it cannot + work: a published page is served under a policy that blocks it from reaching any other host at all. It + would not fail loudly — it would simply never arrive. +
    • +
    • + Put the sittings on the open web with a small service behind them. A bill, an account, and + a thing to keep running, for an instrument that will be used by one or two people for perhaps + 16 hours in total. This project ships static files on purpose. +
    • +
    • + Send the sitting as a file and get it back as a file. This already works, and it is what + the second option becomes when nobody publishes anything. It is not listed separately because the only + difference is whether the reader receives a link or an attachment, and everything that matters after that + point is identical. +
    • +
    + +

    What would reopen this?

    +
      +
    • + A sitting done away from home. If the reader tries and cannot, the first option has been + measured rather than assumed, and this stops being hypothetical. +
    • +
    • + The remaining hours not happening. If 1,710 is still 1,710 in a + month, the arrangement is the suspect, whatever anybody thinks of it in principle. +
    • +
    • + A second reader. Every option here assumes one person answering. Two people make the + first option awkward and the third one obvious, because two readers on one laptop's network is not an + arrangement anybody would choose. +
    • +
    • + The placement question being settled. If it is answered on the evidence already in hand, + the remaining marks stop being urgent and this can be closed unmade. +
    • +
    + +

    What is this not settling?

    +
      +
    • + Whether the rectangles are right. That is the question the sittings exist to answer, and + it has its own page. +
    • +
    • + What a hosted table would actually hold. The third option describes a shape that does not + exist. Choosing it is choosing to design it, not choosing a design. +
    • +
    • + Anything about the app itself. No option here changes a single thing a reader of the + mus'haf would see. This is about the instrument, not the thing it is measuring. +
    • +
    + +
    +

    + Every number on this page was counted out of the sittings and the answers themselves on + 2026-08-17, and can be recounted. Nothing here is transcribed and nothing is a mock-up: the ink in + the specimen is the printed page's own. +

    +
    +
    + + diff --git a/docs/design/track-b-native.md b/docs/design/track-b-native.md index 15b7f78a..ebe92093 100644 --- a/docs/design/track-b-native.md +++ b/docs/design/track-b-native.md @@ -95,6 +95,13 @@ store binary it is the thing that has to be licensed, and the licence has a hole **This row should be corrected regardless of whether Track B ever happens**, because it is wrong today. It is ⑦ ① below. +**Corrected 2026-08-15.** The row now names both parents, and `LICENSES.md` carries a section +saying how the second one arrived. Two things the correction did not settle went to ⑦ ⑤ and +⑦ ⑥: the adjacency shards still ship no `NOTICE.txt`, and nothing anywhere would notice the +next asset tree that gains an upstream this way. The paragraphs above are left in the present +tense on purpose — they are the record of what was wrong, and rewriting them into the past +would leave this document unable to say what it caught. + ## ④ What Track B was actually for, and how much of it survives The stated payload in `PLAN.md` is: bundle the corpus offline, native share sheet, @@ -150,17 +157,24 @@ knowing before anyone budgets Track B as "wrap and ship". ## ⑦ Open questions, and what would answer each -### ① `LICENSES.md` buckets `assets/adj/**` under one of its two upstreams · **confirmed** +### ① `LICENSES.md` buckets `assets/adj/**` under one of its two upstreams · **answered** The shards carry `span` / `toSpan` values computed by `sharedRuns` out of the GPL'd QAC morphology (`build-adjacency.mjs:45`, `:287`, `:342`), and the row names only Waqar144's mutashabihat terms. Independent of Track B and wrong today. -**What would answer it:** an edit to the row, naming both parents — and a decision about -whether `gate:license-copy`'s reach should extend to `LICENSES.md`'s bucket table, since the -drift here was introduced by a *feature* (`word-D1`, the spans) rather than by anyone -touching a licence file. A gate that notices "an asset tree gained a new upstream" is the -shape that would have caught it. +**Answered 2026-08-15.** The row was corrected, and the claim it rested on was re-measured +before anything was edited rather than taken from this document on trust: **2,544 of 3,002 +edges across 114 shipped shard files carry a span**, and 85 of the 114 files contain a +`toSpan`. That is the common case, not an edge case, so the strict reading applies and the row +now reads GPL-3.0 *and* free-use-with-attribution, naming both parents. `LICENSES.md` gained a +section explaining how the second parent arrived — as a feature, with no licence file touched +— because a two-parent row with no story behind it invites someone to simplify it back. + +Two things the edit did not settle, and they are not the same question, so they are split: +the notice does not travel with the adjacency data the way it does with the root data (⑤), +and nothing would catch the next one (⑥, which is the gate question this item originally +carried). ### ② Whether the GPL/App-Store reading in ①–③ is right · **open** @@ -178,10 +192,19 @@ upstreams by name. Until then Track B stays gated and this document stays the re the reachable order is Android first. `PLAN.md:1297` cites research §5 for the deep-link caveat, which is §6. -**What would answer it:** editing both, in a change that also links here — deliberately -*not* done in the commit that introduced this file, because a roadmap edit that changes a -loop's stated order should be its own decision with its own record, not a footnote to a -scoping doc. +**The citation half is fixed, 2026-08-15.** `PLAN.md:1297` now cites §6. It was checked +rather than trusted first: §5 of the research note is the offline finding — quota is not the +problem, eviction is — and §6 is the iOS deep-link caveat, so the item was right about which +number was wrong. That half was safe to take alone because it is a pointer, not a plan: it +changes nothing about what Track B does or in what order. + +**The order half stands, and stays blocked on ②.** It is still `open` for that reason and +that reason only. + +**What would answer it:** the licensing opinion ② is waiting on. A roadmap edit that changes +a loop's stated order should be its own decision with its own record, and if the opinion comes +back saying the §7 route is open after all, the order it dictates changes again — editing the +roadmap twice is worse than editing it once. ### ④ Whether Track B should exist at all after ④ and ⑤ · **open** @@ -194,3 +217,101 @@ gap as a documented limitation the onboarding already respects (research §6 say **What would answer it:** web v1.0 shipping and someone using it. Track B was always gated on that, and the gate has done its job — a year of platform work was not spent on a wrapper whose reason has since been half-dismantled by the web build itself. + +### ⑤ The adjacency shards ship without the notice their upstream requires · **fixed** + +`LICENSES.md` names two consequences of a bucket being a GPL derivative: the corresponding +source reaches recipients, and *the notice travels with the data*. Once ① is taken, the +adjacency bucket owes both, and it discharges only the first. + +Measured, not assumed: `apps/web/public/assets/roots/hafs-kfqc/NOTICE.txt` and +`apps/web/public/assets/skins/hafs-kfqc/NOTICE.txt` exist and are emitted by their builders +(`build-roots.mjs:271`, `build-tajweed.mjs:211`); `apps/web/public/assets/adj/hafs-kfqc/` +holds 114 shard files and nothing else. So two of the three derived trees keep the convention +and the third never joined it — which is what makes this a defect rather than a difference of +opinion. It was invisible for the same reason ① was: the spans arrived as a feature, and a +feature has no reason to look at what a sibling build step writes beside its output. + +**What would answer it:** `build-adjacency.mjs` emitting a `NOTICE.txt` beside the shards, +quoted verbatim from the source header the way `build-roots.mjs` does it, and naming both +upstreams rather than the one the file is mostly about. That is a build change and a rebuild +of a shipped asset tree, which is why it was not folded into the licence-file correction. + +**Fixed 2026-08-16.** `build-adjacency.mjs` now writes the notice, and it names both parents +— the failure available here was fixing the one-parent error in the other direction, by +discharging the GPL half and dropping Waqar144's attribution. The corpus's copyright block is +no longer copied: `morphology.mjs` gained `copyrightBlock()`, `build-roots.mjs` was moved onto +it, and the two trees now reproduce one quotation rather than two copies that can drift. The +root shards rebuilt **byte-identical**, which is what makes that extraction a refactor rather +than a claim. `gate:notices` is the test that would fail if the notice went away. + +### ⑥ Nothing would notice the next asset tree that gains an upstream · **fixed** + +This is the general form of ① and ⑤, kept separate from both because it outlives them: ① +corrects one row, ⑤ writes one file, and neither makes the next occurrence any more visible +than this one was. The drift here ran for the whole life of the spans feature, and every +mechanism that could have caught it was pointed somewhere else. `gate:license-copy` pins the +licence text quoted in `SOURCES.md` against the copy in the colophon — a strong check on +whether a quotation drifted, and no check at all on whether the *set of things being quoted* +is still complete. Nothing in the repo greps for `NOTICE` at all: two builders write one by +convention and nothing verifies either. + +The honest difficulty is that "an asset tree gained a new upstream" is a fact about an import +graph, not about a directory. A gate that could see it would have to trace what each builder +reads — transitively, since `build-adjacency.mjs` never touches the morphology file itself and +reaches it two hops out through `morphology.mjs`. That is real work and a real chance of a +gate that is wrong in a way nobody notices, which is worse than none. + +**What would answer it:** deciding which of the two shapes is worth building — the cheap one +(every directory under `assets/` that a builder writes must carry a `NOTICE.txt`, and the set +of upstreams named across them must match `LICENSES.md`'s bucket table), or the real one (the +import trace). The cheap one would have caught ⑤ and would not have caught ①. Not obviously +worth it either way, which is why this is a question and not a task. + +**Fixed 2026-08-16 — both shapes, because the cheap one alone re-runs this item.** The +question above frames it as a choice, and building it showed the choice was false: the cheap +half catches a missing notice and is blind to the wrong bucket row, which is the defect that +actually happened. `gate:notices` does three ties. The table against the shipped bytes — every +inherited row must ship a notice naming what the row names, in both directions, so a new +inherited bucket cannot be added to the table without arriving in the gate. The names against +`SOURCES.md`, so a renamed project fails instead of matching nothing. And the trace: each +builder is walked through its transitive relative imports, and every vendored input the +resulting module graph reads must carry a verdict. **An input the trace finds and the +declaration does not mention fails the build** — so the next time a feature reaches a new +upstream, the person who wrote the feature is the one who has to say what it means. + +The import trace was the half that looked too expensive to be worth it. It was not, because +the scope that matters is small: relative imports inside one package, and string literals +matched against the vendored files that actually exist. What it cannot see is written into the +gate's own header rather than left to be discovered — it does not resolve dynamic paths, does +not follow `@hifth/core`, and treats `*.pin.json` and `*.probe.json` as this project's own +artefacts, so a builder that reaches a new upstream *only* through a pin is invisible to it. + +The exemption list is the part worth defending, since `gate-gates.mjs` says in as many words +that an allow-list is how this goes wrong every time. This one cannot go quiet: a deferred +input must name an issue id that exists in `docs/issues.json` **and is still open**, so closing +the question breaks the build until the declaration is revisited. It found ⑦ on its first run. + +### ⑦ A third upstream reaches the adjacency shards · **open** + +Found by ⑥'s trace on the first run it did, which is the strongest thing that can be said for +building it. `build-adjacency.mjs` reads `data/pages/ayah-pages.json` — the ayah→page table — +and writes `page` and `dPage` into **every** edge in all 114 shards. That table is derived from +the KFGQPC page corpus, and `LICENSES.md` buckets that corpus separately as *not ours to +relicense*, naming terms neither of the adjacency row's two upstreams grants. + +So the shards may have three parents rather than two. They may equally have two: a table of +which page each ayah falls on is arguably a fact about a printing rather than expression, and +this repo already treats it as a measurement — the same table is used in reverse to *identify* +which print a corpus follows, which is not a thing one does with someone else's authorship. The +adjacency shards also carry no page image, no glyph and no text: they carry the integer. + +**This is not decidable here, and guessing costs more than waiting.** Naming KFGQPC on the row +without grounds overstates what the app owes and would propagate into the notice shipped to +every reader; leaving it unnamed understates it if the answer is the other way. Both are worse +than an open question with the number attached. + +**What would answer it:** the same licensing opinion ② is already waiting on, with this added +to its scope — it is a fourth question for a reader who is being asked three. Until then +`gate:notices` carries the deferral explicitly and will not let it be forgotten, since the +deferral is only valid while this item is open. diff --git a/docs/design/what-we-depend-on.md b/docs/design/what-we-depend-on.md new file mode 100644 index 00000000..71d1ddc6 --- /dev/null +++ b/docs/design/what-we-depend-on.md @@ -0,0 +1,603 @@ +# What does this app depend on, and whose terms come with it? + +A dependency audit run on 2026-08-16, in five lanes: the installed package tree, the +vendored corpora, what could replace each of them, what the licences of those replacements +actually say, and what other Quran apps have done about the same problem. + +It was commissioned to answer one question — *can Hifth move to more lenient terms, swap a +dependency, or write the thing itself without losing a feature?* — and the short answer is +that the question has a different shape than it looked. Swapping sources does not buy what +it appears to buy, one corpus is doing two unrelated jobs and only one of them is hard to +replace, and three places where somebody else's terms reach a shipped file had never been +written down anywhere. + +Numbers below were measured against committed bytes on the branch named at the top, not +read off an upstream README. Where a claim came from a source rather than a measurement, it +is linked. Where nobody looked, it says nobody looked. + +Related: [Track B and the store question](track-b-native.md) holds the analysis this audit +was meant to serve; [the licensing map](../../LICENSES.md) is the register these findings +change; [the source register](../../SOURCES.md) is the authority on every upstream's terms. + +--- + +## What does the package tree carry? + +Nothing that blocks anything. This lane closed clean and it is the least interesting of the +five. + +565 package entries across 12 licence buckets — MIT, ISC, Apache-2.0, BSD-2-Clause, +BSD-3-Clause, CC0-1.0, MIT-0, BlueOak-1.0.0, MPL-2.0, CC-BY-4.0, Python-2.0, and one +dual `MIT OR CC0-1.0`. **Zero copyleft, zero source-available, zero unstated, zero +missing.** The four non-permissive-looking buckets are all several hops inside the lint, +test and build toolchain and never reach a browser. + +What actually ships, traced three independent ways — the production filter, React's own +copyright banners in the emitted bundle, and the bundle's sourcemap: `react`, +`react/jsx-runtime`, `scheduler`, `react-dom`, `@use-gesture/core`, `@use-gesture/react`. +MIT throughout. + +Two things worth keeping even though the licence answer was "fine": + +- **A development dependency is not the same as a build-only dependency.** `workbox-window` + is declared as a dev dependency and ships anyway, pulled in through the offline-support + registration in `apps/web/src/pwa.ts`. So does the rest of the service-worker stack, and + so does `idb`, which no manifest in this repo declares at all. Everything involved is + permissive, so nothing is wrong today — but any check written over production + dependencies would have a hole exactly the shape of the code that ships. +- **Nothing checks any of this.** `gate:license`, `gate:license-copy` and `gate:notices` + were each read in full; none of them looks at `node_modules`. Every licence check in this + repo is about vendored *data*. A copyleft transitive dependency could arrive tomorrow and + no gate would say so. + +## What do the vendored corpora actually contribute, and what dies without each? + +Six vendored sources, and their replaceability is almost inverse to their licence quality. + +| Source | Terms | Shipped weight | What ends without it | +| --- | --- | --- | --- | +| KFGQPC page artwork | The Complex's own grant | 26.7 MB gz — 94% of the payload | There is no app | +| Ligature corpus (word geometry) | Use, modify, distribute, commercial; attribution not required | 885 KB gz | Word selection and word geometry | +| Arabic Corpus morphology | GPL, plus a no-changes clause | 518 KB gz | The root lens; word-level hop refinement degrades | +| Tajweed spans | CC BY 4.0, by one README sentence | 242.6 KB gz | The tajweed colouring | +| Mutashabihat pairings | "Free to use as you see fit"; no licence file | 73.6 KB gz | The hop — 3,002 edges become 22 | +| Structural metadata | CC BY 3.0 | Numbers only, hand-carried into code | Nothing at runtime; the check that they are right | + +**The corpus with the friendliest terms has the worst replacement story.** Rebuilt without +the mutashabihat pairings, the hop falls from 3,002 edges on 1,521 ayahs to **22 edges on 8 +ayahs** — a 99.3% loss of the reason this app exists. Its licence is one README sentence +and it carries an undocumented third-party provenance behind that. Nothing about that is +comfortable, and no alternative improves it. + +**The corpus with the hardest terms is doing two unrelated jobs.** The morphology is read +for root and lemma annotation, and — entirely separately — as a word-segmented text of the +Quran. Everything the adjacency tree owes it comes from the second job only: the shared +word runs are computed over reconstructed word skeletons and never touch a root tag. + +That matters because **a permissively-licensed substitute for the second job is already +vendored and already read at build time.** The ligature corpus carries per-word text under +a grant allowing use, modification and distribution for any lawful purpose including +commercial, with attribution not even required, and the tajweed fold already reconstructs +ayah text from it at 99.86% against an eighteen-rule oracle. + +So the adjacency tree could plausibly stop being a copyleft derivative without losing a +feature, by computing its shared runs over the print's own words. It would also delete the +index-conversion step, because the runs would already be in the only index a word box +carries. **What nobody has measured:** the print splits 9,533 proclitics that the morphology +joins, so run lengths change and the uniqueness rule would keep a different number than +2,544. That is an ETL measurement, and until it is run this option has no cost attached and +cannot honestly be drawn on an options page. + +The root lens has no such escape. Dropping the morphology entirely removes 1,642 roots, +4,644 lemmas and 44,431 root-ayah pairs, and degrades word-level hop refinement from 15.3% +unplaced to 100% unplaced. **The hop itself survives.** No replacement root corpus exists +(below), and there is no check on root correctness at all, so a wrong root would produce a +plausible lens entry nobody could tell from a right one. + +## Where do somebody else's terms reach a file nobody would expect? + +This is the lane that found things. Each row below was verified by counting shipped bytes. + +| Output | A reader would assume | Actually carries | Known before? | +| --- | --- | --- | --- | +| Adjacency word ranges | Pairings only | Morphology, GPL — 2,544 of 3,002 edges | Yes, fixed 2026-08-16 | +| Adjacency page fields | Pairings and morphology | Page-corpus-derived table — 3,002 of 3,002 | Yes, open | +| **Root shard page numbers** | Morphology only | The same page-derived table — **44,431 of 44,431 occurrences, all 604 pages** | **No** | +| **The asset manifest** | Not covered by any row of the licensing map | The complete 6,236-entry page table, byte-identical to the vendored file | **No** | +| **Adjacency juz flags** | Pairings and morphology | Structural metadata, CC BY — **510 of 3,002 edges** | **No** | +| **The app bundle itself** | No scripture anywhere — this project states that as a rule | Twelve verses, hand-typed, fully vowelled — **1,414 characters, in the build made today** | **No** | +| **The transliteration table** | Our own plumbing | A table its own comment calls copied verbatim from the copyleft corpus's website | **No** | + +Five of seven had never been recorded, and the manner of each miss is the finding: + +- The root shards were **asserted not to carry a page number**, in a comment in the gate + that checks exactly this, written the day before this audit. Every occurrence tuple + carries one, and the builder's own header says so. The gate defers this identical + question to an open issue for the adjacency tree and answers it wrongly for the root + tree. Whatever the pagination opinion turns out to be, **it applies to more than one + tree**, which is the part the open issue is currently scoped too narrowly to say. +- The asset manifest is not in the licensing map's table at all. The table names the four + asset directories; the manifest sits beside them, at the root, matching no row. +- The juz flags are **invisible to the trace by construction**. The gate does not follow + imports into the shared core package, and its own header says so. This is the first + demonstration that the stated blind spot is a real one and not a theoretical one. +- The twelve verses are the one that should not have been possible. They are dealt with + under [what we hand people](what-we-distribute.md), because the fact that matters about + them is that they *go out*, not where they came from. +- The transliteration table is different in kind from every other row: nobody derived it, + somebody typed it. Every argument this project makes about upstream terms runs through a + build step, and a table typed into a file goes around that argument rather than through + it. It is fifty-one letter pairs and it may well be a scheme rather than a work — but the + question has never been asked, and the comment beside it uses the word *verbatim*. + +## How much of each source survives being transformed? + +Every argument here rests on the word *derived*, and that word covers two very different +things. Measured, on what actually ships: + +| Source | What the transform does | What comes out the other side | +| --- | --- | --- | +| Morphology | Throws away the grammar, keeps the index | **All 1,642 roots**, 4,644 of 4,817 dictionary forms, **all 44,431 word-to-verse links** | +| Pairings | Re-files them and adds word ranges | **All 2,516 pairings**, recoverable one for one | +| Tajweed | Renames eighteen rules into seven, re-files | **60,057 of 60,057 annotations. Nothing dropped.** | +| Page artwork | Nothing — it is passed along | The work itself. This is not a derivation at all | +| Word geometry | Measures where ink sits on the page | Nothing of the source. 91,451 rectangles of our own numbers | + +Two of these are **lossless**: a change of filing system, not a summary. Give someone the +tajweed tree and the seven-name mapping and they can rebuild the source. Give them the +pairings tree and they have the pairings. Whatever "derived" means, it does not mean +*diminished* for those two, and any argument that leans on the word should say which row it +is talking about. + +The last row is the strongest position this project holds — where ink sits on a printed +page is about as close to a plain fact as this subject offers — and it is also the tree +with no row in the map, no notice, and nothing checking it. **The best-defensible output is +the least documented one**, which is a common shape and worth fixing precisely because the +fix is cheap. + +The first row deserves one caveat against itself, because the notice shipped beside it says +the shards carry annotation only and no scripture: **693 of the 4,644 dictionary forms are +spelled exactly like whole words of the text.** A dictionary headword is often the word. On +one reading that is a dictionary and stays one; on another it is 693 words of scripture in a +tree whose notice says there are none. The notice does not distinguish, and neither can we. + +## Where does somebody else's work reach our own code? + +The licensing map answers this by pointing at the boundary between the app and the data: +the app fetches the shards over the network and never reads a corpus. **That is true** — it +was checked, and there is no read of a vendored file anywhere in the app or the shared +logic. The conclusion drawn from it is that nothing upstream reaches our source. + +That does not follow, because the content did not come in over the boundary being guarded. +Somebody typed it. Four routes, worst first: + +| What is in our code | Where it came from | How defensible | +| --- | --- | --- | +| Twelve vowelled verses | Unrecorded. Not any source vendored here | **Indefensible as written** — see the other document | +| A fifty-one letter transliteration table | The copyleft corpus's website, per its own comment | Genuinely arguable both ways | +| One hundred and fourteen romanised names | Argued to be proper nouns, source unrecorded | Probably fine; unrecorded is the problem | +| Three hundred and eighteen structural numbers | The share-alike metadata, and **re-derived from it on every build** | Fine, and this is the model | + +The last row is how the others should look. Those numbers are checked against their source +by an automated re-derivation every time the project builds, so they are demonstrably facts +verified against a source rather than a block that was copied. That mechanism exists, it +works, and it is applied to exactly one of the four. + +The reason this matters beyond tidiness: it decides whether our own code could ever be put +under more permissive terms. Under a strict reading it could not be, as written. Under a +lenient one only the twelve verses are a real problem — and the twelve verses are a problem +under **both** readings, which is why they lead. + +## Does moving to a more permissive source solve the store problem? + +No, and this is the single most decision-relevant finding in the audit. + +The store exposure has been framed as a copyleft problem. It is not — it is a +*downstream-restrictions* problem, and the share-alike family has the same clause. +**CC BY 4.0 §2(a)(5)(B)** forbids applying effective technological measures where doing so +restricts the rights the licence grants, and **CC BY 3.0 §4(a)** says the same in older +words. Store-applied copy protection is such a measure. + +[Track B](track-b-native.md) already knows this about the tajweed spans. What this audit +adds is the reach: **every candidate replacement morphology is CC BY**, and the structural +metadata this app already uses is **CC BY 3.0**. Trading the morphology for a "more +permissive" corpus buys nothing at all on the store axis. It would swap one blocked tree +for another blocked tree and cost a feature on the way. + +There is a second, quieter finding underneath it. The morphology's no-changes sentence is +**near-verbatim the Quran-text-integrity sentence** from the text project it is built on, +with "this file" substituted for "the Quran text" — and the same clause asks that its +notice be reproduced "in all works derived from" the file, which presupposes derivatives +are permitted. It reads as an inherited text-integrity rule applied to a whole file rather +than a considered restriction on annotation data. That is an argument a lawyer evaluates, +not one this document settles, but it belongs in the record because it materially changes +how hard the hardest blocker is. + +Also settled, and worth recording so nobody re-runs it: **the additional-permission route +has no precedent.** The two projects usually cited for it — a messaging app's crypto +libraries and a media player — both got onto stores by *the copyright holder relicensing* +to a weak-copyleft licence, not by bolting a permission onto a copyleft work. A third +frequently-cited project's licence contains a restriction rather than a grant and does not +support the claim at all. And the morphology's copyright now sits with an estate, which is +who such a permission would have to come from. + +## What could replace each source? + +Three of the five are cleanly solvable. One is solvable by writing it. One is not solvable. + +**Structural metadata — solved outright.** A public-domain dedication (the Unlicense) over +a metadata file that is a strict superset of what this app uses: 604 pages, 556 ruku, 240 +quarters — which divide into the 60 half-parts the current source is used for — 30 parts, +7 manzil, and 15 prostrations with their obligatory/recommended flags. No attribution, no +share-alike, **no technological-measures clause**. This removes an exposure the repo had +not counted as one, and it costs a build step. + +**Tajweed — a real alternative exists, with an adoption risk stated plainly.** An MIT-licensed +engine ships a corpus-free core of about 34 KB: no annotation data and no Quran text is +vendored at all, the output is the same codepoint-range shape, and it **models pause**, +which a precomputed span dataset structurally cannot — a functional upgrade for an app +whose reader stops mid-page. It agrees with the current source at 99.80% across all 6,236 +ayahs, with every divergence catalogued. Against that: it was created five weeks ago, is at +version 0.1.1, has two stars and one author. It is MIT, so vendoring the core outright is +permitted and is the obvious mitigation. + +The incumbent is also worse than the record says. Its grant is one README sentence covering +*"this data file"*, singular — the classifier that generated it carries no licence at all, +so "just regenerate it ourselves" is **not** an available escape. + +**Page geometry — already better than assumed.** The overlay project's own licence splits in +two, and the part this app uses — the ayah hit polygons and per-page geometry — is +**dedicated to the public domain**. The artwork grant beneath it reserves only commercial +*physical printing*, and has no technological-measures clause and no copyleft. The standing +caveat holds: the Complex's own site refused connection again, so that grant is still read +secondhand, which is an open confirmation item this repo already tracks. + +**Mutashabihat — write it.** No licensed alternative exists. The largest candidate is bigger +but its data licence is unstated, so adopting it swaps an unstated licence for an unstated +licence. Repeated word runs over the consonantal text are mechanical, and the curation — +which pairs actually trip a hafiz, and the note saying what differs — is the genuinely +authorial part, authored here. That is the strongest licence position available for any +source in this repo. Nobody appears to have published such a pipeline. + +**Roots — not solvable.** No complete, permissively-licensed, root-bearing morphology exists +that is not derived from the one already vendored. Four projects come close and each fails +on a different axis: two carry no roots at all (lemma and part-of-speech only); one has +roots but is unreleased pending peer review with no licence announced; and the obvious +lemma-to-root lexicon that would complete the join is published under a **no-derivatives** +grant on the page it is actually downloaded from, which would forbid the join. The +ecosystem norm is the finding: a search for root datasets returned fifteen repositories and +**every one of them was unlicensed**. The only fully permissive route is an algorithmic +stemmer, trading accuracy — against a feature with no correctness check to catch the trade +going wrong. + +## Which of these could check our work rather than replace it? + +Every source above was assessed as a candidate *replacement*, and that framing threw away +most of their value. A corpus this project cannot ship can still be read at build time, and +**reading a corpus to check a number is a different act from distributing it** — the +tajweed engine named above establishes the pattern from the other side, verifying itself +against a share-alike dataset it does not ship, so no obligation attaches to what it hands +a user. + +This matters here because the strongest results this project has came from a second +witness, not from a better first one. The structural tables are hand-typed constants and +are checked by re-deriving them from a vendored file, because that class of error is +invisible — every number stays in range, the list stays ascending, and one corpus shipped +off by one for four loops. And the page-geometry bug on the second-to-last page was +invisible to the check designed for it: the ink was covered, just by the wrong ayah's +polygon, and only a second independent print of the same mus'haf could see it. + +Three places where the audit found a witness and nothing is using it: + +**The page table has one witness, and a public-domain second one exists.** The table of +which page each ayah falls on is the single most load-bearing derived artefact here — it +reaches three shipped outputs, and whether it carries its source's terms forward is the +open question everything else is waiting on. It is currently derived from one upstream and +corroborated by nothing. The public-domain metadata file named above carries a page number +per ayah, compiled independently. If the two agree across all 6,236, that is worth having +twice over: it is a real correctness check on a table nothing checks today, **and** two +independent compilations agreeing is what a fact about a printing looks like, as against an +authored arrangement — which is the substance of the question being asked. If they +disagree, that is more valuable still and nobody currently would ever find out. + +**The word-segmentation disagreement has two witnesses and both are parties to it.** Whether +the shared word ranges can be recomputed from the print turns on 9,533 places where the +print splits a word the morphology joins. Today there are exactly two opinions on that and +they are the two being compared, so a disagreement says only that they differ, never which +is unusual. Two independently annotated corpora surfaced by this audit carry word +segmentation and are not derived from the morphology — neither has roots, which is why both +were set aside as replacements, but segmentation is precisely what they do have. A third +and fourth opinion turns an undecidable comparison into a measurement, and that measurement +is the thing blocking the cheapest licence improvement available. + +**Nothing measures what the hop misses.** The pairings corpus is deliberately not +exhaustive — it is the pairs that most commonly confuse huffaz, and that curation is the +reason to prefer it. But there is no number anywhere for what it leaves out, so "3,002 +edges" cannot be read as good or bad. A larger curated collection exists whose data licence +is unstated, which rules it out as a dependency and rules it in as a ruler: comparing +against it yields a recall figure without shipping a byte of it. That number would say more +about whether the hop is finished than any other measurement available. + +Two limits worth stating rather than discovering later. Validation use is *lighter* than +distribution, not exempt — a no-derivatives grant still governs what may be published from +a comparison, and a repository with no licence at all grants nothing, so nothing of this +kind gets vendored into this repository or shipped. And a second witness that turns out to +be derived from the first is not a second witness; the audit already found that trap, in a +project whose annotations were partly scraped from the corpus it claimed to be independent +of, so provenance has to be established before agreement means anything. + +**A third limit, and it is the one that changes how this work should be done.** The research +lane turned up a rule that runs against the whole instinct behind these three ideas: **a +published agreement report can be the evidence against you.** The case that establishes it +is a chip-cloning dispute where the cloner's independently-built part was found to contain +instructions matching ones the original had *deleted* — the court's line was that +unnecessary agreement suggests copying rather than independent creation, and it sits in a +long line of "the shared mistakes are the strongest proof" reasoning. So a report saying +*we recomputed this without the corpus and got 99% the same answer* is, on that authority, +an argument that we copied it. + +The way out is not to stop measuring. It is to be careful **which axis** the agreement is +published on, and the model is the metrically-compatible typefaces: they prove they match on +the measurements, which are compelled and not protectable, while looking visibly different +everywhere the designer had a choice. **Prove agreement only where the mus'haf compels the +answer; where the annotator exercised judgement, divergence is the useful result and it is +free to produce.** + +Concretely for the three above: page-table agreement is agreement about where ink physically +sits in a printed book, which is the compelled axis and exactly the right thing to publish — +and it is *also* the substance of the open question, since two independent compilations +agreeing is what a fact looks like. Segmentation and recall are the opposite: they are +someone's editorial judgement, so the number is worth computing and worth acting on +internally, and a triumphant published diff against the corpus is the thing not to write. +Worth noting that the research found **no clean-room project anywhere has ever published an +agreement report**, which nobody had explained before and this now explains. + +## What this audit did not look at + +- **Whether a root, a lemma, or a table of which page an ayah falls on is copyrightable + expression.** That is the opinion this project is already waiting on. This audit adds + rows to its scope and answers none of it. +- **Whether the app still behaves correctly with a tree removed.** The "N of M is lost" + figures are counts over shipped bytes, not observations of a running app. +- **The accuracy of any hypothetical in-house stemmer.** There is no probe for it and none + was invented. +- **Whether upstream terms have changed since they were vendored.** The audit of the + vendored corpora read this repo's own quotations of them; the alternatives lane read live + sources for the *replacements* but did not re-verify the incumbents. +- **Anything about the validation pages.** They embed real page ink and carry no notice, but + they are excluded from the repository and whether they are reachable by anyone but the + reader they were built for was not determined. + +## Open questions, and what would answer each + +### ① Whether the pagination question covers three outputs rather than one · **open** + +The open question about the adjacency shards carrying a page-derived table is scoped to one +tree. This audit found the identical table reaching **two more** shipped outputs — every one +of 44,431 root-shard occurrence tuples, and the asset manifest verbatim at 6,236 entries. +The licensing opinion being awaited is the same opinion; only its scope is wrong. + +**What would answer it:** the opinion already requested, asked about all three outputs +rather than one. Nothing in code changes until it arrives, but the notice and the licensing +map both understate their subject until it does. + +### ② The gate states a fact about the root shards that is false · **answered** + +The notices gate declares the page table as this project's own for the root bucket, on the +stated ground that no page number reaches a root shard. Measured: **44,431 of 44,431 +occurrence tuples carry one**, matching the vendored table on every one, spanning all 604 +pages. The builder's own header says it doubles as the page table. + +A gate asserting a falsehood is worse than no gate, because it is read as having checked. + +**What would answer it:** change the declaration to defer to ① as the adjacency bucket +already does, so the exemption expires when the question closes. Independent of the opinion. + +*Closed 2026-09-01, by reading the gate rather than this page.* The declaration was changed +the day after the audit: the roots bucket's read of the page table now defers to ①, in the +same words the adjacency bucket uses, with the false sentence quoted beside the deferral so +the drift stays visible. This item then sat marked confirmed for two weeks, which is the +failure the issue catalogue was built to catch and did not, because the marker and the code +are compared by nobody. It is *answered* and not *fixed*: the deferral itself is guarded — the +gate fails if the question it defers to closes or disappears — but nothing would fail if +someone flipped the verdict back to "ours", and a word that claims a test has to name one. + +### ③ Structural metadata reaches the adjacency shards and nothing names it · **open** + +**510 of 3,002 shipped edges** carry a same-part flag computed from the CC BY structural +tables. Neither the licensing map's row nor the shipped notice names that upstream. The +trace cannot see it: it does not follow imports into the shared core package, by design and +by its own admission — so this is the blind spot the gate declared, demonstrated. + +Attribution rather than copyleft, and the app does credit the source in its colophon, so +the exposure is small. The precedent is not: **a builder can reach a new upstream through +the core package and nothing will notice.** + +**What would answer it:** name it in the row and the notice; and decide whether the trace +should follow the core package, which is a real design question because that graph is large +and mostly uninteresting. + +*Half closed 2026-09-01.* The row and the notice now name Tanzil: the adjacency bucket in +the notices gate declares the structural metadata as a third source by hand, so the row's +"whose" cell and the shipped notice must both carry the name or the gate fails, and the +adjacency builder writes a paragraph into the notice saying what the flag is derived from. +What stays open is the other half — the trace still does not follow the core package, so the +hand-written declaration is the only thing standing between the next upstream reached that +way and silence. That is a design question, not a defect, and the marker says so now. + +### ④ Two shipped things are covered by no row of the licensing map · **fixed** + +The manifest ships the complete 6,236-entry page table, byte-identical to the vendored +file. The licensing map's table names four asset directories; the manifest sits at the root +beside them and matches none of them. + +**Widened 2026-08-16.** There are six things shipped and the map has rows for four. The +second uncovered one is the word-geometry tree — 604 files, 2.8 MB, shipped since the +seventh loop — which has no row, no notice, and no mention in either register. It is the +tree with the *least* to worry about, and that is exactly why nobody wrote it down. + +The manifest is the more urgent of the two for a reason unrelated to its terms: it is stored +on the visitor's device the moment they install the app, while the artwork it was derived +from is only stored when they actually open a page. **The derived table travels further than +the thing it was derived from.** + +And the check that exists to catch this cannot: it compares two hand-written lists against +each other and never looks at what is actually in the folder. Anything absent from both +lists is invisible to it by construction, which is why two trees could ship uncovered for +loops without a single failing build. + +**What answered it, 2026-08-16.** Both have rows now. The word tree is ours, and the map says +why in a paragraph of its own: no byte of the print those rectangles were fitted from ships, +and the grant behind that print obliges no attribution, so the credit in the colophon is a +courtesy the record marks as one. The manifest is ours too, with the one thing it cannot yet +settle stated rather than glossed — the page table inside it is the same table, and the same +question, as ①. + +And the check reads the folder now. Every entry under the shipped assets directory has to be +either a declared tree or a named declaration, and the map has to mention it either way. It was +broken on purpose both ways before being left green — an empty directory added, and the word +tree's row removed from the map — because a check that has only ever passed is a comment. + +### ⑤ Nothing checks the licences of the installed package tree · **open** + +Every licence gate here is about vendored data. The package tree is unchecked, and the +audit that checked it by hand found it clean — which is the good time to add the check +rather than the bad one. + +**What would answer it:** a gate over the licences of what actually *ships*, not what is +declared as a production dependency, since the offline-support package is declared for +development and ships anyway. It would need a proof that it can fail, per the convention +the notices gate set. + +### ⑥ Whether to move the structural metadata to a public-domain source · **open** + +A superset of what this app uses exists under a public-domain dedication, with no +attribution and no technological-measures clause. The current source is CC BY 3.0, whose +§4(a) carries the same store exposure as the tajweed spans. + +**What would answer it:** this is a decision, not a task, and belongs in the decision +register with its options drawn — the numbers agree, so the choice is about trusting a +compiler's dedication versus a named project's licence. + +### ⑦ Whether to compute the adjacency word ranges from the print instead · **open** + +If the shared runs were computed over the print's own words rather than the morphology's, +the adjacency tree would stop being a copyleft derivative and the index-conversion step +would disappear. The substitute corpus is already vendored under the friendliest terms in +the repo. + +**What would answer it:** run the measurement. The print splits 9,533 proclitics the +morphology joins, so the uniqueness rule keeps a different number of runs than the current +2,544 — and until somebody knows whether that number is 2,400 or 900, this option has no +cost attached and cannot be drawn honestly. + +### ⑧ Whether the tajweed spans should move to a pause-aware engine · **open** + +An MIT engine with a corpus-free core would mean vendoring no annotation data and no Quran +text, agrees with the incumbent at 99.80%, and models pause, which the incumbent +structurally cannot. It is also five weeks old with one author, and the incumbent's +regeneration path is closed because its classifier carries no licence. + +**What would answer it:** a hafiz's judgement on the 108 catalogued divergences, which is +the same judgement the colouring is already waiting on — and a decision about depending on +something this new, which vendoring the core would largely dissolve. + +### ⑨ The page table is derived from one source and corroborated by none · **open** + +The table reaches three shipped outputs and sits at the centre of ①, and nothing checks it. +A metadata file under a public-domain dedication carries a page number per ayah, compiled +independently of the corpus this project derives its table from. + +**What would answer it:** compare the two across all 6,236 ayahs. Agreement is a correctness +check on an unchecked table and evidence bearing on ① — two independent compilations +agreeing is what a fact about a printing looks like. Disagreement is worth more and would +never otherwise surface. The source is public domain, so nothing about this is constrained. + +### ⑩ The segmentation disagreement has no disinterested witness · **open** + +Whether ⑦ is possible turns on 9,533 places where the print splits a word the morphology +joins. The only two opinions are the two being compared, so a disagreement cannot say which +is unusual. Two corpora surfaced by the audit annotate word segmentation independently of +the morphology — both were set aside as replacements for having no roots, which is +irrelevant to this use. + +**What would answer it:** segment agreement between each pair, over the disputed positions +only. It converts ⑦ from an argument into a measurement, and ⑦ is the cheapest licence +improvement available. Both are share-alike, so they can be read at build time but must not +be vendored here or shipped. Provenance first: one project the audit examined claimed +independence while having partly scraped the corpus it was being compared to. + +### ⑪ Nothing measures what the hop does not contain · **open** + +The pairings corpus is deliberately not exhaustive and that is the reason to prefer it, but +no number exists for what it omits, so 3,002 edges cannot be read as sufficient or thin. A +larger curated collection exists under an unstated data licence — unusable as a dependency, +usable as a ruler. + +**What would answer it:** a recall figure against it, computed at build time, shipping +nothing. It bears directly on whether the hop is finished, which no other measurement here +speaks to. + +### ⑫ A table in our own code is described in its own comment as copied verbatim · **fixed** + +Fifty-one letter pairs, used to turn the corpus's ASCII spellings back into Arabic, sit in +one of our build scripts under a comment naming the copyleft corpus's website as the source +and using the word *verbatim*. Every other claim this project makes about that corpus runs +through a build step and rests on the shards being derived rather than copied. This one goes +around that argument, because it is not derived from anything — somebody typed it in. + +It may well be fine. The scheme it encodes was published by someone else years before the +corpus adopted it, it has very few ways it could reasonably be written given that it has to +be typeable and reversible, and a mapping between two alphabets is close to an idea. But +that is an argument nobody has made, and the comment as written concedes the opposite. + +**What answered it, 2026-08-16.** The comment did. It named the corpus's own web page as the +source and used the word *verbatim*, and neither was the right description of what is in the +file. The scheme is the original publisher's, released with a widely-used morphological +analyser; the rows past the plain letters are its ASCII-safe variants together with the extra +symbols the corpus needs for a mus'haf's pause, madd and small-letter marks, which the corpus +documents. What sits in our script is a correspondence between two alphabets that has to match +theirs exactly or the roots come out wrong — a constraint, not a copy. The comment says that +now, and the concession it used to make is gone. Whether our code could be put under more +permissive terms at all is still ⑭'s question; this closes one of the four routes it turns on. + +### ⑬ The artwork pipeline is documented as applying two changes and applies three · **fixed** + +The register says the page pipeline applies exactly two declared changes; the pipeline's own +header says three, and all three are real — an optimiser that rewrites every coordinate at +reduced precision, two identifier repairs, and **23 shape repairs across 19 pages**. The +licensing map separately calls the artwork reproduced unmodified, which at reduced coordinate +precision it is not. + +This is worth more than a typo, for one specific reason. The whole answer to the corpus's +no-changes clause is that this project vendors upstream files untouched and derives at build +time. That answer depends on *vendored verbatim* being a practice a reader can trust. A +second tree that says verbatim and is not undermines the practice rather than the tree — the +artwork's terms are not the corpus's terms and nothing here breaches them. + +**What answered it, 2026-08-16.** Both sentences say three and list them — the optimiser at +its pinned settings, the two identifier repairs, and the twenty-three shape repairs across +nineteen pages, with what each is for. The register says outright that it claimed two for as +long as there were three, and that the pipeline's own header was right throughout. The +licensing map no longer calls the artwork unmodified; it says what is applied and keeps the +promise that was actually being made, which is that nothing is hand-edited — and that is the +promise the corpus's no-changes clause rests on, so it comes through the correction intact. + +### ⑭ Whether our own code could be put under more permissive terms at all · **open** + +Worth separating from every other question here, because it is the one somebody may want an +answer to for reasons that have nothing to do with app stores — a reader wanting to build a +different mus'haf on this engine, for instance. The map's stated reason to think the answer +is yes rests on a boundary that the content did not cross. What actually decides it is the +four typed-in routes above. + +Note what the choice is not. Moving our code to permissive terms does nothing about the data, +and permissive code sitting beside copyleft data in one deploy is precisely the unresolved +question of whether the two are one work or two things shipped together. It is also worth +knowing that a middle option exists and is usually skipped: terms that keep changes to *our* +files coming back while letting somebody embed the engine in something closed. If the motive +is "let people use this" rather than "let people take this", that is the shape that fits. + +**What would answer it:** ⑫ resolved, the twelve verses removed or sourced, and then a +licensing opinion — which is the same opinion ① and the store question are already waiting +on, and worth asking as one question rather than four. diff --git a/docs/design/what-we-distribute.md b/docs/design/what-we-distribute.md new file mode 100644 index 00000000..169922d8 --- /dev/null +++ b/docs/design/what-we-distribute.md @@ -0,0 +1,399 @@ +# What do we actually hand people, and through which door? + +A licence governs *conveying* — handing someone a copy. So the question "may we ship this?" +has no answer until you say **ship through what**. This project already hands things to +people through two different doors with substantially different contents, and the licensing +map describes only one of them. + +This document is about the shape of what gets distributed, as distinct from +[what we depend on](what-we-depend-on.md), which is about where the inputs come from. The +two are easy to confuse and the confusion is expensive: an input's terms constrain the door +it goes through, not every door. + +Related: [the licensing map](../../LICENSES.md) is the register this affects; +[Track B](track-b-native.md) is where a third door is being considered. + +--- + +## Which doors are open, and what goes through each? + +Measured on the current branch, 2026-08-16. + +| Door | What goes through it | Vendored corpora included? | +| --- | --- | --- | +| **The repository** — public, cloneable | 17 vendored input files, 12.3 MB, verbatim; every builder; all our code | **Yes, all of them** | +| **The deployed site** — public, fetched by browsers | ~100 MB of derived assets; the app bundle at 118.3 KB gz | **No. None of them.** | +| **A store binary** — does not exist yet | Would be the site's contents, wrapped | Would inherit the site's answer | + +The two live doors carry almost disjoint content, and **that is the fact the whole question +turns on.** + +The two most awkward inputs — the morphology corpus at 6.3 MB under a copyleft grant with an +added no-changes clause, and the tajweed annotations at 5.6 MB under a share-alike grant — +are tracked in the repository and **never reach a browser**. What reaches a browser is +derived: 518 KB gz of root shards, 242.6 KB gz of tajweed spans, 73.6 KB gz of pairings, and +26.7 MB gz of page artwork. + +Also worth stating because it is easy to assume otherwise: the largest thing this project +reads does not go through either door. The word-geometry work reads a 378 MB page cache that +is excluded from the repository, so it exists only on the machine that built it. It is +already, incidentally, exactly the pattern this document is about. + +## Which door does each obligation actually attach to? + +Separating the doors makes several previously tangled obligations fall apart cleanly. + +**The copyleft obligation is discharged by the repository door, not the site door.** The +requirement is that corresponding source reach the people receiving the derived shards. The +repository carries the builder and the pinned input it derives from; the site tells a +reader where the repository is and pins the commit it was built from. This is already how +`LICENSES.md` argues it, and the door framing is why the argument works: the shards go +through one door and their source through another, and the obligation is satisfied by the +pair, not by either alone. + +**The store blocker attaches only to the third door.** Both blocking clauses — the +copyleft licence's prohibition on imposing further restrictions, and the share-alike +licence's prohibition on applying technological measures — are triggered by something the +distribution channel does to the recipient, not by anything the data is. The same bytes go +through the site door today with no such problem, and would go through a store door with +one. **Nothing about the data changes; the door changes.** This is why the audit's +conclusion that swapping to a "more permissive" source does not help is correct — the +problem was never the source's permissiveness. + +**The verbatim-vendoring strategy is a door decision that was already made.** The repository +carries each corpus unmodified and derives at build time. That satisfies the no-changes +clause on the repository door, and means the site door carries no copy of the corpus to +satisfy anything about. It was recorded as a way to honour two contradictory readings at +once; it is also, in this framing, the first deliberate separation this project made. + +## What goes out through the site door that nobody counted? + +Having drawn the doors, the obvious next question is whether the inventory of each one is +right. It is not. Three things go out through the site door that no register here mentions, +and one of them contradicts a rule this project states in twenty different files. + +**Twelve verses of scripture are in the app itself.** Not in a data file — in the code, +hand-typed, fully vowelled, forty-eight fragments and 1,414 characters of Arabic. Every one +of them was found byte-for-byte in the build made today. They are not left-over: two live +parts of the app render them, so they survive into every visitor's browser. + +The rule they contradict — that no Qur'anic text enters this project — is stated **22 times +across 20 files**, including three of the notices that ship beside the asset trees and a +settled decision where it is the entire answer to a no-derivatives clause. It is the most +repeated factual claim in this repository and it is false. + +Two things make it worse rather than better. **Nothing checks it**: the only gate whose name +suggests it might is about something else entirely — that the page drawings use outlines +rather than live text, which is a rendering requirement, not a licensing one. And the +spelling used is not the spelling any corpus here vendors, so the text did not come from any +of them. Where it did come from is unrecorded; the comment above it says only that it is +demonstration data from the third loop, hand-verified, awaiting replacement by a real source +that never arrived. + +This is the only finding in either document that is a defect under every reading of every +licence involved, and it is also the cheapest to fix: the feature is a leftover demonstration +serving two popovers, and the text can be removed or re-sourced without anyone noticing. + +**Two shipped trees are covered by no row of the licensing map** — the word-geometry tree and +the asset manifest — and the check built to catch that cannot see them, because it compares +two hand-written lists to each other and never reads the folder. That is carried under +[what we depend on](what-we-depend-on.md), item ④. + +The shape all three share is worth naming, because it is the argument for organising the map +by door in the first place: **each one is something that goes out, found by looking at what +goes out.** No amount of reading the map would have produced any of them. + +## What separation buys, and what it cannot buy + +The reason to be careful here is that the same manoeuvre is sound in one direction and +wishful in the other. + +**What it genuinely buys.** If an encumbered input is read only to *check* an output, and +never to produce it, the output is not derived from it in any sense and the input goes +through no door at all. The audit already relies on this: three corpora that cannot be +depended on are nonetheless usable as instruments, and the tajweed engine surfaced there +does exactly this from the other side — it verifies itself against a share-alike dataset it +does not ship, so nothing attaches to what it hands a user. **A validator is not a +dependency.** That distinction is real and this project should use it much harder than it +does. + +**What it cannot buy.** Moving the *production* of an artifact behind a door does not make +the artifact unencumbered. If a shard is computed from a corpus, it is computed from that +corpus whether the computation happened on a build machine, in a separate repository, or in +somebody else's continuous integration. Build-time derivation changes who holds a copy of +the input; it does not change what the output is made of. This project's derived shards +carry terms forward precisely because that is true, and any argument that starts "but the +corpus never ships" has already been answered — the notices shipped beside those shards +exist because it was answered. + +So the honest form of the technique is narrow and worth stating in one line: **separation +helps when the encumbered thing is an instrument, and does not help when it is an +ingredient.** Every candidate manoeuvre should be tested against that sentence first. + +**And the research sharpened it in a way worth having.** Every organisation that has written +this down draws the line in the same place, and it is not where the intuition puts it: +**nobody treats "it is only used for testing" as a category that means anything.** The +category every one of them actually uses is **"it is not handed to anybody"**, and build or +test scope is just a convenient stand-in for that. One large company's policy proves it by +contradiction: it permits the copyleft licences internally because their obligations bite +only on things delivered to outsiders, and then bans outright the one variant whose +obligation is triggered by network use instead — same data, same internal use, opposite +answer, because the trigger moved. + +That is the same claim this document already makes about doors, arrived at independently, +and it means the instrument/ingredient test is really a proxy for a blunter question: +**does a copy leave?** Which is worth knowing, because the moment a manoeuvre is judged on +whether a copy leaves rather than on what the thing is called, most of the clever ones stop +being available. + +## What are the established ways of doing this? + +Surveyed, against primary documents rather than summaries. Each one below comes with what +defeats it, because a technique recorded without its defeat is not research. **None of this +is legal advice, and the survey has real holes, listed at the end.** + +The single most useful thing it produced is a **better test than the one this document had.** +The instrument-and-ingredient line is right but it is hard to apply, because "did this input +contribute?" answers *yes* for everything and decides nothing. The test that six separate +bodies have independently converged on is sharper: + +> **Can somebody recover the input's protected content out of the thing you handed them?** + +Not *did it come from there* — **can you get it back out**. That is the question being asked +by the map-data community's rule for telling a finished work from a database, by the US +copyright office and by two European courts about models, and by two of the newer data +licences. They did not coordinate, and the disagreements between them are about outcomes +rather than about the test. Applied here it stops being a philosophical question and becomes +a per-tree measurement — which is exactly what open item ② needs, and it is a measurement we +already have: the tajweed tree is **fully recoverable** and the word-geometry tree contains +nothing of its source at all. + +### What each technique is, and what kills it + +**Reading something only to check the answer.** The strongest pattern by a distance, and the +only one with something like agreement behind it — one large foundation states it in a single +sentence, and one distribution builds its whole archive layout around the distinction. **What +kills it:** believing the build machine is the boundary. It is not. The pattern is safe +because *no information flows from the input into the output* — a checker reads the corpus +and emits a yes or a no. The moment the check's result changes a single value in what ships, +you have left the pattern and are somewhere else entirely. + +**Shipping the recipe instead of the result.** We already do this, and it does real work — it +is what discharges the obligation to make source available. **What kills it** is expecting it +to do more than that: it does not remove terms from the derived trees, which are still made +of what they are made of. And it has one concrete failure mode worth designing against. In a +recipe-only design **the pins and pointers are the only thing you ship, so they are the only +thing anybody examines.** The most instructive case in the whole survey is a takedown aimed at +a project that contained no copyrighted recording at all — the notice pointed at its **test +fixtures**, because the fixtures named specific works. The hosting platform's stated rule is +that it distinguishes code that *can* be used a certain way from code that is +*preconfigured* to be. So: **describe an input by its checksum, not by its address.** That is +strictly safer and costs nothing. + +**Conveying the parts separately.** Half-fits. The clause people reach for turns on whether +the pieces are combined into a larger program — a question about how they relate at runtime, +not about which server they came from or who typed the download command. It supports the +door framing at the level of *what conveying means*; it does not supply a manoeuvre. + +**Having the reader fetch it themselves.** **Strike this one.** A large project proposed it +for precisely our reason and rejected it, recording objections that all apply here with full +force — no network in some settings, institutional download policies, a major redesign for +nothing. Both mobile stores forbid it outright, which is fatal given that the store is the +only door with a blocker in the first place. And recipes rot silently: one long-running +example already carried *twelve* fallback addresses per file, all twelve were dead, the +report was closed as unreproducible, and nothing anywhere was watching. Where this pattern +does work, it works because **the rightsholder drew the line**, not because the packager did. + +**Clean-room rebuilding, and proving a licence-clean replacement agrees.** Surveyed, and the +finding is sharp enough that it has been written into +[what we depend on](what-we-depend-on.md) instead, because it changes the validation plan +rather than the distribution plan: **a published agreement report can be the evidence against +you.** Also worth knowing here: a clean room is *not* required — an appeal court called it +clearly erroneous to demand one — and it protects against neither a patent nor a promise. + +### The three defeats that apply to us whatever we choose + +**"We ship no bytes of the source" is an argument that has been made and lost.** A publisher +shipped level files for a game containing no art, no code, and no bytes of anyone's work — +numbers only. It lost, because the numbers described the copyrighted result in exact detail; +the court's analogy was sheet music, which contains no sound. The escape the same judgment +leaves open is the useful part: the description has to be **meaningful against something other +than the one work**. That is a real sorting criterion and it splits our trees cleanly. Root +identifiers keyed to a standard scholarly scheme mean something against any Arabic text — on +the safe side. **Page numbers and bounding boxes derived from one specific printed mus'haf +are useful only against that mus'haf** — on the exposed side. Which is a second, independent +reason the pagination question is the one everything is waiting on. + +**"They are only numbers" fails when a person decided them.** Two courts have held numbers +protectable because they were *estimates* rather than observations — the reasoning being that +the compiler did much more than discover and report. So the question is never *are these +facts*; it is **did somebody choose, among more than a few defensible alternatives?** A +morphological parse is a judgement. A tajweed span is a judgement. **Where ink physically sits +on a page is an observation** — which is the strongest thing anyone has yet said in favour of +the tree we have documented least. + +**Contract beats all of the copyright analysis, and this is the finding that most deserves to +be here.** A court once assumed a database was uncopyrightable *and enforced the licence over +it anyway*, reasoning that copyright is a right against the world while a contract binds only +its parties — so it is not the same kind of thing at all. A European ruling reaches the +matching result from the other side: the user protections that override restrictive terms +apply only to databases that are protected in the first place. **Winning the argument that +something is just facts can strengthen the claim against you rather than defeat it.** Ours is +not a hypothetical: the no-changes clause on the morphology corpus is a term we accepted in +order to obtain it, and whether a downstream recipient could strip it is a *different* +question from whether it binds us. The only judicial word anywhere on stripping such a clause +is an unpublished decision on a different statute, routinely cited for far more than it holds. + +### One argument we can make that nobody else seems to have made + +Available from the grants we already hold, in their own words. Both the morphology and the +structural metadata require their notice to be reproduced in all works **derived from** the +file. So the terms **contemplate derived works and impose only a notice obligation on them** — +while the no-changes clause attaches, by its own sentence, to *this file*. Those are two +different objects, and the grant itself distinguishes them. This is close to what our own +licensing map already does in practice, and it is worth putting to whoever gives the opinion, +because it is an argument from the text rather than from our convenience. + +### What this survey did not cover, and one thing it could not reach + +Said plainly rather than papered over, and none of it should be treated as checked. + +**Not covered at all:** the app store that builds every submission from source and labels its +encumbrances — which is **the distributor closest to our situation** and the largest hole +here; three other package channels; and one industry licensing standard. **Partly covered:** +how one distribution treats *data* as against *code*, where the archive rule is media-neutral +and the specifically-data material was not reached. + +**Unreachable:** the mus'haf printer's own site refused every connection, so their actual +grant is still unread and everything this project says about the artwork's terms still comes +from a third party's summary. This is not a gap in the survey — it is already carried as a +standing human check, on the finding that the host is unreachable from automation at the +network layer rather than behind anything a request could talk past. **The research is now +the third unrelated network to fail the same way**, which has been recorded against that +check; it is the strongest confirmation yet that it will only ever be done by a person with +an ordinary browser. Separately, one licence steward's site was unreachable all session, so +**its positions on build tools are simply absent** from this survey rather than negative — +nobody should later fill that gap from memory. + +**Things that appear not to exist**, having been looked for: no decided case anywhere on +whether a positional index of a text is a derivative work; none on morphological or +grammatical annotation of a text; none on bounding boxes or character offsets taken from a +printed page; none on a copyleft grant with a no-changes clause bolted on. **And no +distributor anywhere writes down a legal theory for build-time separation** — every policy +found states the rule and not the reason. That is worth knowing before leaning hard on any of +this: the practice is settled, the theory behind it is nobody's. + +### And a discrepancy found in passing + +The structural metadata's grant is stated three times on three surfaces and the three do not +match — the terms page names one licence, the download page shows the same permission text +with no licence named at all, and the notice inside the copy we vendored names a *third*, +more restrictive one. Not our defect, but it is the file our own register describes with a +version number, and it bears on the question of moving to a public-domain source. Recorded +against that question rather than opened separately. + +## Open questions, and what would answer each + +### ① The licensing map describes one door and is read as describing all of them · **answered** + +The map's table is organised by path, which silently means *the site door* — the derived +asset trees. It says nothing about the repository door, which is where every vendored +corpus actually is and where the copyleft obligation is discharged, and nothing about a +store door. A reader asking "may this be distributed?" gets an answer without being told +which distribution it answers for. + +This is not a hypothetical confusion: the audit found the asset manifest, shipped through +the site door, covered by no row at all, and the argument that the corpus never ships is +sound about one door and irrelevant to the question of what the shards are made of. + +**What would answer it:** a map organised by door and then by path, so each row says what +goes out, through which channel, under whose terms. No opinion needed; it is a restatement +of facts already established. + +*Closed 2026-09-01.* The licensing map is now three tables under three doors — repository, +deployed site, store — each row saying what goes out, through which, under whose terms, with +this document's measured numbers in it. Answered rather than fixed: the notices gate still +parses the rows and refuses an inherited tree or a shipped path with no row, but nothing +checks that the rows stay sorted by door, and a later edit that collapsed them back into one +table would pass every gate. + +### ② Which encumbered inputs are ingredients and which are only instruments · **open** + +The distinction above is the whole of the technique, and this project has never sorted its +inputs by it. Some are unambiguous — the pairings and the page artwork are ingredients, the +comparison corpora identified in the audit are instruments. At least one is genuinely +undecided: the morphology corpus is an ingredient for the root lens and, for the adjacency +tree, may be replaceable by an instrument if the recomputation the audit proposes works. + +**What would answer it:** classify each input, and for the undecided one, the measurement +already tracked. The classification is cheap and would say immediately which of the +separation patterns are even available. + +### ③ Whether a store build should carry every tree · **open** + +The store door is the only one with a blocker, and it does not have to carry what the site +door carries. The audit has the feature-loss numbers for omitting each tree, and they are +very uneven — one omission costs a whole lens, another costs colouring and nothing else, +and a third would end the app. + +**What would answer it:** this is a decision, not a task, and needs its options drawn +against real feature loss rather than argued. It should not be opened until the +classification in ② exists, because ② may remove the blocker from one of the trees +entirely and change what the options are. + +### ④ Twelve verses of scripture go out through the site door · **fixed** + +Described above. Forty-eight fragments, 1,414 vowelled characters, in the shipped build, +rendered by two live parts of the app, against a rule stated 22 times across 20 files — +including three shipped notices and the load-bearing sentence of a settled decision. +Provenance unrecorded and not matching any source vendored here. Nothing checks the rule. + +**The research raised this from a tidiness problem to the most urgent item in either +document.** A scripture publisher ran a takedown campaign of eleven notices over two months +last year, and roughly half of them were **scoped to a path inside a code repository** — +naming a data directory, and in one case a **test-fixtures directory**. So the thing that +determines the blast radius of a complaint is not what the project is or what the text is +for. **It is which directory the text sits in.** Twelve verses living in the app's own source, +rather than in a data tree with a notice beside it, is precisely the shape those notices +reached — and unlike every other question here, this one does not wait on an opinion from +anybody. + +**What answered it, 2026-08-16.** They are gone, and the comparison view they existed to +demonstrate is better than it was. The transcription was never needed: every look-alike pair +already ships with the matching word run recorded on both sides, in the same numbering the +page's own word boxes use, so the panel now crops both ayahs straight out of the printed page +and washes the words outside the shared run. It covers 2,544 pairs where the table covered +twelve, and it fixes a defect the table had all along — the typed spelling was plainer than +the mus'haf on screen, so a reader comparing the two saw different letters in each. + +**A second site, which this item did not know about.** The check written for this one was run +over the whole tree before anything was fixed, rather than at the file that motivated it, and +it named a second: a pipeline test holding a four-word phrase as a fixture. It does not ship — +and by the research above, that is not the point. The blast radius follows the directory, and a +test-fixtures directory is one of the places those notices actually reached. Fixed the same +day, by giving the test bare letters, which is all its arithmetic ever reads. + +**The check.** A run of three or more consecutive fully-vowelled Arabic words anywhere in the +tree now fails the build. Three is not a taste call: measured across every file, the two +offenders ran to eleven words and five, and everything else in the repository — our own +interface Arabic, the domain words, the single specimen words a pipeline test needs — sat at +one or two. **Nothing at all sat at three or four.** The threshold is placed in a gap the tree +itself provides, and the header records the measurement so a later reader can re-derive the +number rather than nudge it. Single-specimen files are named in an allowlist with a reason +each, so ninety-odd lone vowelled words read as reviewed rather than unnoticed — and the +phrase rule overrides the allowlist, so no entry in it can ever admit a passage. + +**The one remaining Arabic that goes out was measured rather than assumed.** Each look-alike +pair ships with a short hand-written note beside it, and some of those notes name the two words +that differ — so they are Arabic, and they cross the site door. They were written here, by us, +to say something the source dataset does not say; and across all 114 shipped shards the check +scores **zero**, because they are written without vowel marks. Not one word in them reaches even +the specimen threshold, let alone the passage one. That is a measurement, not a reassurance: the +same run that named the two offenders was pointed at every shard that ships. + +**And the rule itself was rewritten**, in the nine places that stated it without a scope. It +had been *"there is no Quran text in this repo and there will not be"*, which was false about +the repository and true about the thing it was defending. It now says that nothing this project +vendors and nothing it ships is Quran text, and every site names the check. The settled decision +that leans on it was re-read as this item asked: its sentence is about what the shards emit, +which was scoped and true throughout, so nothing there needed to move. diff --git a/docs/design/where-you-slip.html b/docs/design/where-you-slip.html new file mode 100644 index 00000000..8e9e786b --- /dev/null +++ b/docs/design/where-you-slip.html @@ -0,0 +1,1177 @@ +Where You Slip + + + + + + + +
    +
    +
    Hifth · Design review
    + +
    +
    + +
    +

    A feature for huffaz · five decisions open

    +

    The app remembers
    where you slip.

    +

    Not which verses could be confused — which ones you keep confusing.

    +

    Every memoriser has three or four seams in the whole book where their own tongue takes the wrong turn, every time, and the correction never quite sticks. That set is small, personal, and in no dataset — because it is a fact about one memory. This is the design for capturing it in a single gesture, and being warned at the seam ever after. Read it, then leave your call on the five open questions in the comments.

    +
    + design only · nothing built + 5 decisions to make + reuses what the app already carries +
    +
    + +

    + + +
    +
    +

    Why build it

    +

    What turns a reference into something you rely on?

    +

    Today the app can already tell a hafiz which verses resemble the one in front of them — a fact about the Qur'an, true for everyone. The thing a hafiz revising alone at night actually needs is narrower and entirely their own.

    +
    +
    +
    + What it is today +

    A dictionary of resemblances

    +

    It reads a shared corpus of look-alike verses that ships with the app and shows what resembles what. Accurate, public, the same for every reader — and silent about the one thing that would help most.

    +
    +
    + What this makes it +

    A coach who has heard you recite

    +

    It learns where you slip and says so — quietly, before you get there. "Careful, this is the one you always miss." The sentence a teacher would say, said by the app, at the moment it matters, because you put it there weeks ago.

    +
    +
    +
    + + +
    +

    A few words, defined once

    +
    +
    Ayah
    A verse, named by its surah and number — e.g. the 48th verse of surah 2.
    +
    Mus'haf
    The printed Qur'an; the actual pages the app draws on the screen.
    +
    Mutashabihat المتشابهات
    Verses that resemble each other across the book. Two passages a page — or twenty juz — apart can open with the same words and then part.
    +
    A slip
    An involuntary wrong turn — your memory runs into the wrong verse because a near-identical passage begins the same way. The error you want warned about.
    +
    A jump
    A leap from one place to another, usually where the same phrase recurs. Maybe a mistake, maybe a connection you noticed — the neutral sibling of a slip.
    +
    The seam word
    The exact word where two passages part — the last they share, or the first that differs. It's where a slip or jump actually happens.
    +
    The state of a seam
    How live a slip is right now, and it changes as you drill it: one you catch every pass, one sometimes, one you used to and have beaten, one you've dismissed. A note you own — never a score.
    +
    +
    + + +
    +
    +
    +

    The whole journey, screen by screen

    +

    Where do you start, and what happens next?

    +

    Follow a single slip from the moment you open the app to the day it warns you — every screen you'd actually touch, drawn at the size you'd use it. Each screen is numbered; leave feedback on any one of them in the comments.

    +
    + +
    + + +
    +
    +
    +
    hifth
    +
    +
    +
    LAST REVISED · 2 DAYS AGO⚠ 3 SPOTS YOU SLIP
    +
    +
    ذَٰلِكَ ٱلْكِتَٰبُ لَا رَيْبَ ۛ فِيهِ 2:2
    +
    +
    +
    ↕ Go to a surah or verse…
    +
    +
    +
    +
    + 01 +
    You open the app
    +
    +
    The page
    The mus'haf, where you left off — it reopens to your last place, not to a home screen or a menu.
    +
    You do
    Start reciting. Nothing to set up first.
    +
    You see
    One quiet line at the top: when you last revised, and how many spots you've flagged as confusing. A "go to…" bar waits at the bottom for when you want to jump.
    +
    +
    +
    + + +
    +
    +
    +
    the page · 2
    +
    +
    +
    +
    وَٱتَّقُوا۟ يَوْمًا لَّا تَجْزِى نَفْسٌ 2:48
    +
    +
    +
    +
    ↻ Look-alikes4
    +
    ⚠  I slipped here
    +
    +
    +
    +
    +
    + 02 +
    Your memory jumps the rails — you select the verse
    +
    +
    The page
    Same page; the verse you were reciting is now lit in amber.
    +
    You do
    Select it — the same tap you already use to read. (Or, if you know the exact word, press and hold that — the precise path shown just below this walkthrough.)
    +
    You see
    A rail beside the verse: the look-alikes the app already knows (4), and one new control — I slipped here.
    +
    +
    +
    + + +
    +
    +
    +
    i slipped here
    +
    +
    +
    +
    I slipped at 2:48
    + source +
    +
    My memory ran toward…
    +
    +
    +
    +
    2:123 · identical wording
    +
    وَٱتَّقُوا۟ يَوْمًا لَّا تَجْزِىعَدْل
    +
    opens the same, parts at one word
    +
    +
    +
    +
    +
    +
    2:281
    +
    وَٱتَّقُوا۟ يَوْمًا تُرْجَعُونَ فِيهِ
    +
    opens the same, then parts
    +
    +
    +
    +
    Pick another verse…
    +
    ? Not sure yet
    +
    +
    +
    +
    +
    +
    + 03 +
    You name where it went
    +
    +
    The page
    The capture sheet, opened already knowing where you started (2:48).
    +
    You do
    Tap the verse your memory ran toward — usually already in the list, because look-alikes are exactly what pulled you off course.
    +
    You see
    Candidates ranked with identical wording first; the shared opening shown, the divergence marked. Two escape hatches: pick another verse, or "not sure yet" to finish later.
    +
    +
    +
    + + +
    +
    +
    +
    the page · 2
    +
    +
    +
    +
    وَٱتَّقُوا۟ يَوْمًا لَّا تَجْزِى نَفْسٌ 2:48
    +
    +
    +
    ✓ Saved · 2:48 → 2:123Undo
    +
    +
    +
    +
    + 04 +
    It's logged — you're already reciting again
    +
    +
    The page
    Back to the same page, instantly.
    +
    You do
    Nothing more. Total cost so far: two taps, no typing.
    +
    You see
    A brief confirmation you can undo. Hit the same slip again next week and it deepens the one you have — it doesn't pile up duplicates.
    +
    +
    +
    + + +
    +
    +
    +
    the page · 2
    +
    +
    +
    +
    وَٱتَّقُوا۟ يَوْمًا لَّا تَجْزِى نَفْسٌ 2:48
    +
    +
    recorded slipevery pass
    +
    You slip at يَوْمًا toward 2:123
    +
    5 times · last time last week
    +
    ↔ compare the seams✓ beat it
    +
    +
    a quiet dot rests after the word — tap it for the details above
    +
    +
    +
    +
    +
    + 05 +
    Days later — a quiet marker at the seam, details when you want them
    +
    +
    The page
    The same verse, on a later pass — the payoff for everything above, and it stays out of your way.
    +
    You do
    Recite as usual. A small breakpoint dot rests right after the word you slip on; it doesn't interrupt. Tap it only when you want to dig in.
    +
    You see
    On tap, the recorded slippage: where it goes (2:123), how often and how recently, its state badge (every pass), and two actions — compare the seams, or mark it beaten. The dot's colour alone already tells you how live it is.
    +
    +
    +
    + + +
    +
    +
    +
    compare
    +
    +
    +
    every pass
    +
    +
    + 2:48 · you are here + وَٱتَّقُوا۟ يَوْمًا لَّا تَجْزِى نَفْسٌشَفَٰعَة +
    +
    ↕ confused with
    +
    + 2:123 + وَٱتَّقُوا۟ يَوْمًا لَّا تَجْزِى نَفْسٌعَدْل +
    +
    +
    +
    +
    +
    +
    + 06 +
    You compare the two seams, deliberately
    +
    +
    The page
    Tap compare in the marker's details → the two verses side by side.
    +
    You do
    Read the seam on purpose — the long run they share, and the one word where they finally part.
    +
    You see
    Shared opening in one colour, the divergence marked in another — the exact correction a teacher would walk you through, drawn from links the app already carries.
    +
    +
    +
    + + +
    +
    +
    +
    confusion points
    +
    +
    +
    +
    Still live — drill these
    +
    ×5
    2:48 → 2:123slip · last week
    every pass
    +
    ×3
    6:141 → 6:151jump · 3 days ago
    sometimes
    +
    ×1
    2:281 → 2:48slip · 2 weeks ago
    sometimes
    +
    Beaten — you've got these now
    +
    ×4
    3:7 → 3:29slip · quiet 6 weeks
    used to
    +
    +
    +
    +
    +
    +
    + 07 +
    Before a session — your weak points at a glance
    +
    +
    The page
    Your confusion points, reached from the app's own menu — never from a shared link.
    +
    You do
    Glance before you start; tap any row to jump to either end and drill it. Tap a badge to change a seam's state — or mark one beaten the day you stop slipping on it.
    +
    You see
    Every slip and jump with its state badge — live seams up top (every pass, then sometimes), and a beaten group below for the ones you've conquered. Each row also says whether it was a slip or a jump, and how often you hit it.
    +
    +
    +
    + + +
    +
    +
    +
    my map
    +
    +
    +
    +
    nonesometimesevery passused to
    +
    +
    +
    +
    +
    + 08 +
    Now and then — the map of your own seams
    +
    +
    The page
    A picture of the whole mus'haf, coloured by where you slip.
    +
    You do
    Notice the shape — "my confusions are all in the last third," say, or that they cluster around the stories that repeat.
    +
    You see
    Your weak seams as a map — the personal twin of the look-alike map the app draws for everyone. Hot where you slip every pass, calm teal where you used to and have since beaten it, so the map shows your wins too. The most speculative surface; the warning and the list carry the feature without it.
    +
    +
    +
    + +
    +
    +
    + + +
    +
    +

    A more precise way in

    +

    What if you know the exact word?

    +

    The control in screen 02 above asks only which verse — the fast, discoverable path. But often you know more: the exact word where your tongue took the wrong turn, because that word is the moment it happened. Press and hold that word — reusing a gesture the app already has, so it doesn't fight the phone's own text menu — and a small menu opens where two different things you might mean finally part.

    +
    + +
    +
    +
    the page · 2
    +
    +
    +
    +

    ↓ press and hold the word

    +
    + وَٱتَّقُوا۟ + يَوْمًا + لَّا + تَجْزِى +
    +
    +
    ⚠ I slipped herepulled onto a look-alike
    +
    ↷ I jumped from hereleapt to the same phrase elsewhere
    +
    ✎ Mark this worddecide slip or jump later
    +
    +
    +
    +
    +
    + +
    +
    + Slipped +

    An involuntary wrong turn — resemblance pulled your tongue onto a look-alike verse.

    +
      +
    • the sheet ranks resembling verses first
    • +
    • becomes a private warning on the page, weeks later
    • +
    +
    +
    + Jumped +

    A leap to another place — usually where the identical phrase recurs. Maybe a mistake, maybe just a connection you noticed.

    +
      +
    • the sheet ranks the word-for-word twin first
    • +
    • a real path your memory takes — the natural candidate to become a link you can follow (decision D4)
    • +
    +
    +
    +
    + +

    Marking the word hands the app the exact point of divergence — which is the very signal decision D3 wonders whether it must build. And the verb you pick isn't cosmetic: it decides how the app ranks where you went, and which surface remembers it — a warning for a slip, a path for a jump.

    +
    + + +
    +
    +

    Not every seam is equally alive

    +

    How reliably do you slip here — and has it changed?

    +

    A flat list of everywhere you ever slipped is the wrong thing to read back. What matters is which seams are still catching you — and, just as much, which you've beaten, because that's the progress the feature exists to earn. So every confusion point carries a state, and it moves as you work the seam.

    +
    +
    +
    every pass

    Catches you each time

    The loudest warning, top of the list, hottest on the map. The this is the one you always miss the feature was built to say.

    +
    sometimes

    Catches you on some passes

    A quieter warning, mid-weight on the list and map. Where most seams live — and where a new capture is born.

    +
    used to

    You've beaten it

    Stops nagging — at most a faint still solid? Stays visible in the calm colour, because a seam you conquered is worth seeing. The encouraging half of the whole feature: it remembers your wins, not only your weak spots.

    +
    dismissed

    Not really a problem

    A stray flag you decide isn't a seam. It goes quiet and drops off the list — distinct from used to: nothing was beaten, it simply wasn't one.

    +
    +
    + A seam moves along a natural arc — and the moment that matters most is the good one: you land on it, the warning fires, and this time you don't slip. One tap on «beat it» retires it. +
    new capture → sometimes → (again & again) → every pass → (you land, don't slip) → used to
    +
    +

    You own the state, and it's never added up. Every pass and used to are notes you set about yourself — the way a teacher writes watch this or you've got this now — not a rate the app computes, which it couldn't do honestly anyway, since it sees your captures, never your recitation. At most it nudges: nothing here in six weeks — retire it? And there is no "seams beaten: 12", no percentage, no streak across seams. The instant a state becomes a number about you, it's the score this feature refused — which is exactly decision D5 below.

    +
    + + +
    +
    +

    Holding the line

    +

    What it deliberately is not

    +

    Hifth is a navigation instrument, not a place to write. These three guards keep the feature from drifting into an app this one refuses to be.

    +
    +
    +
    +

    Not a notebook

    +

    A confusion point is two coordinates and a direction — from here, toward there — never a paragraph of reflection. If a screen ever invites you to write, the design has drifted.

    +
    +
    +

    Not a second dataset

    +

    It makes no claim to be the "real" look-alikes. It records exactly what one person hit — and that narrowness is the value, not a shortfall.

    +
    +
    +

    Not a score

    +

    No accuracy percentage, no streak, no "verses mastered." A number about someone's worship the data can't support is a mistake this codebase has already refused.

    +
    +
    +
    + + +
    +
    +
    +

    Your call · comment on each

    +

    Five decisions to make before any code

    +

    Each is a genuine fork the design does not settle. Options are weighed side by side; a leaning is offered where there is one, and the single line that would flip it. Leave your call — or your argument — as a comment on the one you want to weigh in on.

    +
    + +
    + + +
    +
    +
    D1Sharpest · settle first
    +

    How safe must these memories be, when the phone quietly forgets?

    +

    iPhones delete a web app's stored data after seven days without a visit. A glance-log of what you read can survive that as a minor loss. A confusion map is weeks of hard-won self-knowledge — losing it silently is a real harm to the exact reliance this feature exists to earn.

    +
    +
    +
    +
    ASame shelter as the reading log
    +
      +
    • Nothing new to build; proven pattern already in the app.
    • +
    • An "add to Home Screen" install is the real defence, and the app already asks for it.
    • +
    • A reader who never installs can lose months of their map with no warning.
    • +
    +
    +
    +
    BGive it a backup it can be saved to
    +
      +
    • The map survives a wipe, a new phone, a reinstall.
    • +
    • Backup means data leaves the device — which is exactly decision D2.
    • +
    • New surface to design and to keep private.
    • +
    +
    +
    +
    +
    +
    Leaning This is the one feature precious enough to justify a save path — but it can't be chosen without also answering D2.
    +
    Flips if you decide a Home-Screen install is a fair price to ask of anyone who wants their map to last.
    +
    + comment your call on D1 +
    +
    + + +
    +
    +
    D2Wants a full decision record
    +

    Should a confusion map ever be allowed to leave the device?

    +

    The app's settled stance is that anything leaving the device is a deliberate, gated act — no export, no sync. Two real pulls argue the other way here; one hard fact argues against.

    +
    +
    +
    +
    ANever — it stays on the device, always
    +
      +
    • A record of where your memory of the Qur'an fails is intimate; keeping it sealed honours that.
    • +
    • Export is the first step of every privacy erosion; not building it is the safest stance.
    • +
    • No backup (blocks D2's answer to D1), and no way to show a teacher.
    • +
    +
    +
    +
    BYes, as a deliberate, gated act
    +
      +
    • A teacher can see a student's map and drill them on it — a genuinely valuable use.
    • +
    • Unlocks a backup path for D1.
    • +
    • Blurs a private log toward the outside world; every future erosion starts here.
    • +
    +
    +
    +
    +
    +
    Recommendation Because it's a genuine either/or with a privacy cost, this one deserves its own decision page drawn on a real mus'haf — not a snap call in a comment.
    +
    Flips if the teacher-drills-student use is judged worth more than the privacy risk it opens.
    +
    + comment your call on D2 +
    +
    + + +
    +
    +
    D3Needs a hafiz's test
    +

    Is the "already listed" suggestion good enough, or does it need new work?

    +

    The suggested targets come free from the resemblance links the app already carries — but that corpus is hand-curated and covers only about a quarter of the book. For an uncovered verse, the sheet opens with an empty list and falls back to a manual jumper. When a slip is captured by pressing the exact word, though, the reader hands over the divergence signal directly — so option B is only ever needed for verses marked at the whole-verse level.

    +
    +
    +
    +
    AShip with what's already there
    +
      +
    • Free — reuses existing links, ranked identical-wording first.
    • +
    • The manual jumper covers the gaps; a slip is never lost.
    • +
    • On uncovered verses it isn't the one-tap experience — you type the target.
    • +
    +
    +
    +
    BAlso build a shared-opening / rasm signal
    +
      +
    • Even an uncovered verse can offer candidates; empty-list case becomes rare.
    • +
    • Matches how the eye and tongue actually confuse — the bare consonant skeleton.
    • +
    • The one genuinely new piece of data work; deferrable to a later phase.
    • +
    +
    +
    +
    +
    +
    Leaning Ship A first; let a hafiz using it on real slips tell us how often the list comes up empty before spending on B.
    +
    Flips if early use shows real slips keep landing on verses the curated corpus omits.
    +
    + comment your call on D3 +
    +
    + + +
    +
    +
    D4Privacy & provenance
    +

    Should your own slips become links you can navigate, like the app's own?

    +

    The captured pairs could appear in the hop rail as a fourth kind of link — your edges, beside the corpus's public ones — so the app you navigate is shaped by your own history. Appealing, and a provenance question. The slip/jump split sharpens it: a slip is an error and stays a warning; a jump is a path your memory actually takes, and far more natural to make navigable.

    +
    +
    +
    +
    AKeep them private — warning only
    +
      +
    • A private log stays cleanly separate from public, sourced links.
    • +
    • The at-the-seam warning may already deliver the value without merging the two.
    • +
    • Your history never reshapes the map you navigate.
    • +
    +
    +
    +
    BSurface them as a fourth link type
    +
      +
    • The app you move through is genuinely shaped by your own weak seams.
    • +
    • Mixes a private log into the surface that draws public, sourced links.
    • +
    • Cuts against the gate built to keep those two apart.
    • +
    +
    +
    +
    +
    +
    Leaning Keep slips as warnings (A); let jumps — the deliberate paths — be the only thing considered for the rail. The sharper question isn't "do captures become edges," it's "do jumps."
    +
    Flips if the warning proves too easy to miss, and a rail entry is what makes it land.
    +
    + comment your call on D4 +
    +
    + + +
    +
    +
    D5Privacy & the score line
    +

    Who says how reliably you slip — you, or the app's guess from the data?

    +

    Each seam's state — every pass, sometimes, used to, dismissed — has to be set by someone. The design's bet is you own it, because the app sees your captures, not your recitation: silence at a seam could mean "I beat it" or "I stopped logging," and a state the app computed would be the very score this feature refused. The counter-pull is friction — a reader who never marks a seam beaten leaves conquered seams warning forever.

    +
    +
    +
    +
    AYou set it; the app never guesses
    +
      +
    • Honest — it never claims to measure worship it cannot see.
    • +
    • The cleanest distance from a score.
    • +
    • Conquered seams keep warning until you remember to retire them.
    • +
    +
    +
    +
    BYou set it, but the app may quietly nudge
    +
      +
    • A gentle "nothing here in six weeks — retire it?" spares the friction.
    • +
    • The write itself still stays your explicit act.
    • +
    • Any inference from capture data leans toward measuring — the line to watch.
    • +
    +
    +
    +
    +
    +
    Leaning Reader-owned, with a quiet nudge (B) — but the line between a nudge and an assertion is the thing to settle, and it echoes the score guard directly.
    +
    Flips if even a suggestion is judged to lean too far toward putting a number on someone's recitation.
    +
    + comment your call on D5 +
    +
    + +
    +
    +
    + + +
    +
    +

    If it's built

    +

    The order it would come in

    +

    Ordered by dependency, not committed to dates. Each phase is a whole, useful thing on its own.

    +
    +
      +
    1. + 01 +
      Settle the stance a decision, not code +
      Answer D1 and D2 at least provisionally — they decide whether the store needs a save path and how strict its privacy is.
      +
    2. +
    3. + 02 +
      Capture, from the app's own links +
      The private store; the ayah-level control and the hard-press word menu with the slip/jump choice; the sheet drawing candidates straight from the resemblance graph. Slips and jumps get recorded — a whole feature on its own.
      +
    4. +
    5. + 03 +
      The warning, and the review list +
      The at-the-seam warning, and the list sorted by how often you hit each slip. Where capture starts paying back — arguably the real first version.
      +
    6. +
    7. + 04 +
      The personal map +
      A picture of the mus'haf coloured by where your slips cluster. Highest polish, lowest urgency.
      +
    8. +
    9. + 05 +
      The shared-opening signal gated on D3 +
      Only if phase-2 use shows corpus-only candidates come up empty too often.
      +
    10. +
    11. + 06 +
      Export / teacher-share gated on D2 +
      Only if the decision goes that way, and built as the deliberate, gated act the privacy stance demands.
      +
    12. +
    +
    + + +
    +
    +
    +
    +
    Weigh in where it counts.
    +

    Comment on any of the five decisions above — D1 and D2 are the ones that gate everything else, D2 is the one that likely deserves its own drawn options page before it's fixed, and D5 draws the line between a helpful note and a score. Your calls and arguments in the comments come back to shape the plan.

    +
    +
    +
    + Hifth · a navigation instrument for huffaz  ·  Feature: personal confusion points (Tier C1)  ·  Status: design only, nothing built
    + Not settled here: the external scan of how other tools handle personal capture, and the note shape checked against the annotation model — both owed before costing. +
    +
    +
    + + diff --git a/docs/design/word-indexing.md b/docs/design/word-indexing.md index 99a3b8c8..97a21cc2 100644 --- a/docs/design/word-indexing.md +++ b/docs/design/word-indexing.md @@ -464,8 +464,9 @@ frame. Identity: the QAC ↔ print alignment shipped in word-D and is baked into **What is.** `build-tajweed.mjs` emits, per ayah, `family → [start, end, …]`, and those numbers are **codepoint offsets into that ayah's Tanzil Uthmani text**. The alignment joins two *word* indices. Between a codepoint and a word sits a segmentation of a text this repo -does not hold and has a standing rule against holding — *"There is no Quran text in this -repo and there will not be"* (`morphology.mjs`). QAC's Buckwalter segments reconstruct a +does not vendor and has a standing rule against vendoring — *"nothing this project vendors, +and nothing it ships, is Quran text"* (`morphology.mjs`, enforced by `gate:scripture`). +QAC's Buckwalter segments reconstruct a word well enough to ask whether two ayahs share phrasing and nowhere near well enough to count Uthmani codepoints. So the offsets currently resolve against nothing committed here. @@ -583,3 +584,31 @@ KB gz, not the estimate — and to need `gate:assets` reviewed rather than assum probe is not a gate and will not become one: it reads the gitignored 378 MB page cache, so on a clean checkout it has nothing to read. Re-run it before writing the bake — the numbers above are of one pin. + +### ⑥ The look-alike panel draws one rectangle where the geometry gives it several · **open** + +Opened 2026-08-16 by the decision `comparison-crop`, which chose how to fix it. This row +tracks the build, not the choice — the choice is made, recorded, and drawn. + +`bandsFor` returns one rectangle per printed line the ayah touches. `DiffView` takes their +union and draws that, so it also draws whatever else is printed on those lines. Measured +across both sides of all 2,544 drawable pairs: 5,088 crops, mean 69.8% of the drawn +rectangle is the ayah it names, 820 of them (16.1%) more neighbour than ayah, worst 10.3%. +The wash marks the words the two ayahs do not share; nothing distinguishes an unmarked +neighbour from unmarked shared wording, and a reader can take one for the other. + +**What was chosen** (option F, 2026-08-16): the neighbours veiled with a paper-coloured +scrim over the frame minus the bands; the shared range marked green; each divergent run +marked yellow. The two tints stop meaning *which ayah* and start meaning *what the words +are*, identically on both halves of the panel. + +**What that needs**, all inside `DiffView` and its stylesheet: an evenodd path for the +scrim, a band per shared line, a band per divergent line, and two new tokens. No new data +and no new geometry — `divergentRuns` and `bandsFor` already return every rectangle +involved, which is what `scripts/build-crop-options.mjs` draws the option from, so the +geometry is proven before the component moves. Existing behaviour to preserve: no edge, no +shard or no page still renders nothing and the hop row keeps its plain note. + +**What it does not settle.** Which words get marked — read off the edge's spans, verified, +unchanged. The exact green and yellow, chosen to survive over ink on cream and open to +argument without reopening the decision. diff --git a/docs/issues.json b/docs/issues.json index 89c8199a..9745b276 100644 --- a/docs/issues.json +++ b/docs/issues.json @@ -375,6 +375,14 @@ "owner": "agent", "note": "ANSWERED 2026-08-06 by probe-tajweed-words.mjs, and the heading is wrong: a build change with a named exception list, not a third text. The move that did it is not the one this row predicted — the text is not found, it is RECONSTRUCTED from the print's own per-word data-hafs, which build-words.mjs already reads and drops on purpose, so nothing is vendored and the standing no-text rule is untouched. Three corrections make it agree with Tanzil, each earned by a failed run and each stated by the corpus rather than guessed: prepend the basmala to ayah 1 of every surah but 1 and 9 (326 out-of-range annotations → 0), glue the split conjunction waw the print flags itself as data-waw-alatf, and drop the pause-mark words the print numbers and this text does not carry (63.40% → 97.03%, and the evidence was a miss histogram clustered on EVEN values decaying monotonically, the signature of one repeated two-codepoint insertion). Checked against an oracle rather than its own output: hamzat_wasl must start on ٱ and lam_shamsiyyah on ل, 15,985 annotations independent of any word boundary. AUDITED TO THE END 2026-08-07: the 172-ayah residual the first run left as unnamed 'orthographic drift' was bracketed — for each failing ayah, read the reconstruction between its last correct oracle annotation and its first wrong one — and resolved into FOUR more corrections, all of the same kind (the same printed mark, a different number of codepoints in the two texts): a small high mark on a tatweel carrier «ـۧ»/«ـۨ», «أٓ» spelled with one codepoint more there, the small high madda «ۤ», the hamza below «ٕ». 97.03% → 99.81%, 172 → 11 ayahs, and the corrections moved paintability with them unasked (past-the-end 2 → 0, wider-than-two 4 → 1), which is the second reason to believe them. The narrowness is measured rather than chosen: generalising the tatweel rule to every U+0640 scores 94.48%, WORSE than applying nothing, and a candidate rule that gained 1 annotation and 0 ayahs was rejected with 36:52 recorded as a named exception instead. Result: oracle 15,955/15,985 = 99.81%; 50,032/60,057 = 83.31% of annotations inside ONE print word; 16.69% across two ADJACENT words, which is the cross-word phonology (idghaam, ikhfa, iqlab) painting correctly on two boxes rather than a misalignment; exactly 1 wider than two (12:41, idghaam_ghunnah); 0 past the end. The residual is 11 ayahs (0.18%) and it is ORTHOGRAPHIC in ③'s sense — all 30 misses within ±2 codepoints, every ayah drifting by a single constant amount — and it is not a rate: each one is NAMED in residual.named, and 12:39, 12:41 and 15:7 are exceptions lib/segmentation.mjs ALREADY names for the print↔QAC alignment, reached here from a third text neither of those involves. Recorded in packages/etl/data/tajweed/tajweed-words.probe.json, not heuristised; the run prints ⚠ for any residual ayah NAMED does not cover, so a moved pin announces itself. Answering it does not lift the beta label — the palette still waits on a hafiz (plan-tajweed-golden-row) — and ① prices the bake in advance at the roots tree's shape, 450.7 → 532.3 KB gz, not the estimate. ORIGINAL NOTE FOLLOWS. Opened 2026-08-06 by a sweep, and the sweep is the point: this work had been sitting inside plan-loop-assigned-deferrals as 'blocked on word geometry' for two loops after the geometry shipped, and inside where-the-alignment-lives as one sentence of an answered item. Neither place counted it, which is the exact failure the catalog exists to prevent. The two walls it used to name are gone — assets/words/** holds 91,451 boxes on our frame, and the QAC↔print alignment is baked into assets/adj/** and assets/roots/**. What stands is different in kind: build-tajweed.mjs emits codepoint offsets into each ayah's Tanzil Uthmani text, the alignment joins two word indices, and between a codepoint and a word sits a segmentation of a text this repo does not hold and has a standing rule against holding (morphology.mjs: 'There is no Quran text in this repo and there will not be'). QAC's Buckwalter segments reconstruct a word well enough to compare phrasing and not well enough to count Uthmani codepoints. What would answer it is ①'s move run once more, ETL-only: fold Tanzil-Uthmani against the print's per-word text, which build-words.mjs already reads and drops, and measure how many annotations land inside one print word. Answering it does not lift the beta label — the palette still waits on a hafiz (plan-tajweed-golden-row) — and ① prices the bake in advance at the roots tree's shape, 450.7 → 532.3 KB gz, not the estimate." }, + { + "id": "the-crop-draws-neighbours-with-nothing-saying-so", + "source": { "file": "docs/design/word-indexing.md", "item": "⑥" }, + "status": "open", + "severity": "defect", + "owner": "agent", + "note": "Opened 2026-08-16, and it is a build row rather than a question: the decision comparison-crop is DECIDED (option F, omar, 2026-08-16) and the fix is drawn, measured and checked in — what is missing is the component. The panel takes the union of bandsFor's per-line rectangles and draws that, so it draws whatever else is printed on those lines: 5,088 crops over 2,544 pairs, 69.8% mean share, 820 of them (16.1%) more neighbour than ayah, worst 10.3% (80:36, page 585). The wash marks the words the two ayahs do not share, so nothing distinguishes an unmarked neighbour from unmarked shared wording and a reader can read one as the other — on the app's own signature pair the crop labelled 2:123 has its whole first line occupied by 2:122. What F needs, all inside DiffView and its stylesheet: an evenodd scrim over the frame minus the bands, a green band on the shared range, a yellow band on each divergent run, two new tokens. No new data — divergentRuns and bandsFor already return every rectangle, and scripts/build-crop-options.mjs draws the option from exactly those calls, so the geometry is proven before the component moves. Preserve the fallback: no edge, no shard or no page still renders nothing and the hop row keeps its plain note. Not settled by this row: which words get marked (read off the edge's spans, verified, unchanged) and the exact green and yellow, which can be argued without reopening the decision." + }, { "id": "alef-maddah-is-a-fourth-correction", @@ -422,10 +430,40 @@ { "id": "adj-shards-have-two-upstreams", "source": { "file": "docs/design/track-b-native.md", "item": "①" }, - "status": "confirmed", + "status": "answered", "severity": "defect", "owner": "agent", - "note": "LICENSES.md buckets apps/web/public/assets/adj/** under 'Free use with attribution (inherited) — Waqar144's', naming the mutashabihat dataset alone. That was true when the shards carried only edge endpoints and stopped being true when word-D1 added the spans. build-adjacency.mjs:45 imports wordsByAyah and sharedRuns from morphology.mjs, whose header states what it reads — the GPL'd Quranic Arabic Corpus morphology; spansOf (:287) calls sharedRuns and the result is written into every shard at :342 as `span` / `toSpan`. So the shipped adjacency shards carry values COMPUTED FROM GPL'd data, and under the same strict reading of 'derivative' this repo applies to itself elsewhere they are a GPL derivative with one of their two parents unnamed. Independent of Track B and wrong today, which is why it is `confirmed` rather than parked with the rest of that document's rows. It matters more inside a store binary than on the web, and the asymmetry is the point: a static site discharges GPL §4–6 by handing the browser the whole bundle, which is LICENSES.md's own argument for plain GPL over AGPL, while a wrapped binary is the thing that has to be licensed. What would answer it: correct the row to name both parents, and decide whether gate:license-copy's reach should extend to the bucket table — the drift here was introduced by a FEATURE rather than by anyone touching a licence file, so the gate that would have caught it is one that notices an asset tree gaining a new upstream." + "note": "ANSWERED 2026-08-15: the row names both parents. The claim was re-measured over the shipped shards before anything was edited rather than read off this note — 2,544 of 3,002 edges across 114 files carry a span, and 85 of the 114 contain a toSpan, so the strict reading applies to the common case and not to a corner. apps/web/public/assets/adj/** now reads 'GPL-3.0 (inherited) and free use with attribution (inherited) — The Quranic Arabic Corpus's and Waqar144's', and LICENSES.md gained a section saying how the second parent got there: as a feature, with no licence file touched. The story is load-bearing, not decoration — a two-parent row with nothing behind it reads like an over-cautious duplicate and invites the next person to simplify it back to one. `answered` and not `fixed`, by the same reasoning as drift-label-reads-backwards: what changed is prose, no test would fail if somebody reverted it, and this repo's definition of `fixed` refuses the word without a gate that notices. That gap is not a footnote here — it is the whole of the item this row leaves behind, nothing-notices-a-new-upstream, which is the gate question this note originally carried and which stays open on its own terms rather than being closed by the edit it asked for. The second thing the correction did not settle is adj-shards-ship-no-notice: naming a GPL parent brings both of LICENSES.md's stated consequences with it, and the bucket discharges only the first. ORIGINAL NOTE FOLLOWS. LICENSES.md buckets apps/web/public/assets/adj/** under 'Free use with attribution (inherited) — Waqar144's', naming the mutashabihat dataset alone. That was true when the shards carried only edge endpoints and stopped being true when word-D1 added the spans. build-adjacency.mjs:45 imports wordsByAyah and sharedRuns from morphology.mjs, whose header states what it reads — the GPL'd Quranic Arabic Corpus morphology; spansOf (:287) calls sharedRuns and the result is written into every shard at :342 as `span` / `toSpan`. So the shipped adjacency shards carry values COMPUTED FROM GPL'd data, and under the same strict reading of 'derivative' this repo applies to itself elsewhere they are a GPL derivative with one of their two parents unnamed. Independent of Track B and wrong today, which is why it is `confirmed` rather than parked with the rest of that document's rows. It matters more inside a store binary than on the web, and the asymmetry is the point: a static site discharges GPL §4–6 by handing the browser the whole bundle, which is LICENSES.md's own argument for plain GPL over AGPL, while a wrapped binary is the thing that has to be licensed. What would answer it: correct the row to name both parents, and decide whether gate:license-copy's reach should extend to the bucket table — the drift here was introduced by a FEATURE rather than by anyone touching a licence file, so the gate that would have caught it is one that notices an asset tree gaining a new upstream." + }, + + { + "id": "adj-shards-ship-no-notice", + "source": { "file": "docs/design/track-b-native.md", "item": "⑤" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "scripts/gate-notices.mjs", + "note": "FIXED 2026-08-16: build-adjacency.mjs writes the notice. It names BOTH parents, because the failure available here was fixing the one-parent error in the other direction — a notice that discharged the GPL half and dropped Waqar144's attribution would be the identical mistake pointed the other way, and it would have looked like a fix. The corpus's copyright block is no longer copied into two builders: morphology.mjs gained copyrightBlock(), build-roots.mjs was moved onto it, and the two derived trees now reproduce ONE quotation instead of two copies that can drift — which matters more than it sounds, because the corpus's own third term is that its notice be reproduced appropriately, and two readers of one quotation is exactly how a reproduction stops being one. The roots shards rebuilt byte-identical, which is what makes that extraction a refactor rather than a claim about one. Rebuild: 3,002 edges on 1,521 ayahs across 114 shards, 2,544 carrying a span, unchanged. ORIGINAL NOTE FOLLOWS. LICENSES.md names two consequences of a bucket being a GPL derivative — the corresponding source reaches recipients, and the notice travels with the data. Once adj-shards-have-two-upstreams is taken, apps/web/public/assets/adj/** owes both and discharges only the first. Measured: roots/hafs-kfqc/NOTICE.txt and skins/hafs-kfqc/NOTICE.txt exist and are written by their own builders (build-roots.mjs:271, build-tajweed.mjs:211); adj/hafs-kfqc/ holds 114 shard files and nothing else. Two of the three derived trees keep the convention and the third never joined it, which is what makes this a defect rather than a difference of opinion about how much attribution is enough. It stayed invisible for exactly the reason the licence row did: the spans arrived as a feature, and a feature has no reason to look at what a sibling build step writes beside its output. Deliberately NOT folded into the LICENSES.md correction — that was prose about a file, this is a build change that rewrites a shipped asset tree, and mixing the two would put a 114-file rebuild inside a commit whose diff reads as a documentation edit. What would answer it: build-adjacency.mjs emitting NOTICE.txt beside the shards, quoted verbatim from the source header the way build-roots.mjs does it, and naming BOTH upstreams rather than the one the file is mostly about — the failure mode available here is a notice that fixes the GPL half and drops Waqar144's attribution, which is the same one-parent mistake in the other direction." + }, + + { + "id": "nothing-notices-a-new-upstream", + "source": { "file": "docs/design/track-b-native.md", "item": "⑥" }, + "status": "fixed", + "severity": "risk", + "owner": "agent", + "closedBy": "scripts/gate-notices.mjs", + "note": "FIXED 2026-08-16, and BOTH shapes were built rather than one, because building it showed the choice this note framed was false: the cheap half catches a missing notice and is blind to a wrong bucket row, which is the defect that actually happened, so shipping the cheap half alone would have re-opened this item under a different name. gate:notices ties three things that could each drift alone. (1) The table against the shipped bytes: every LICENSES.md bucket row with inherited terms must ship a NOTICE.txt naming what the row names, checked in BOTH directions, so a new inherited bucket cannot be added to the table without arriving in the gate's declaration. (2) The names against SOURCES.md, so a typo or a renamed project fails instead of quietly matching nothing. (3) The trace: each builder is walked through its transitive relative imports and every vendored input the module graph reads must carry a verdict — an input the trace finds and the declaration does not mention FAILS THE BUILD, which puts the decision on whoever wrote the feature that reached the new upstream, at the moment they reach it. The import trace was the half this note called too expensive to be obviously worth it, and that was wrong: the scope that matters is relative imports inside one package and string literals matched against the vendored files that actually exist. What it cannot see is written into the gate's header rather than left for someone to discover — no dynamic paths, no @hifth/core, and *.pin.json / *.probe.json are treated as this project's own artefacts, so a builder reaching a new upstream ONLY through a pin is invisible to check 3. The exemption list is the part that needed defending, since gate-gates.mjs says in as many words that a quiet line in an array is how an allow-list starts every time: this one cannot go quiet, because a deferred input must name an issue id that exists AND is still open, so closing the question breaks the build until the declaration is revisited. It found adj-shards-carry-a-third-upstream on its first run. ORIGINAL NOTE FOLLOWS. The general form of adj-shards-have-two-upstreams and adj-shards-ship-no-notice, kept as its own row because it outlives both: one corrects a row, the other writes a file, and neither makes the next occurrence more visible than this one was. The drift ran for the whole life of the spans feature and every mechanism that could have caught it was pointed elsewhere. gate:license-copy pins the licence text quoted in SOURCES.md against the copy in the colophon — a real check on whether a quotation drifted, and no check at all on whether the SET of things being quoted is still complete. Nothing in the repo greps for NOTICE: two builders write one by convention, verified by nobody. The honest difficulty is that 'an asset tree gained a new upstream' is a fact about an import graph and not about a directory — build-adjacency.mjs never opens the morphology file and reaches it two hops out through morphology.mjs, so a gate that could see this has to trace transitively, which is real work with a real chance of producing a gate that is wrong in a way nobody notices. That is worse than no gate. What would answer it: deciding which of two shapes is worth building. The cheap one — every directory under assets/ that a builder writes carries a NOTICE.txt, and the set of upstreams named across them matches LICENSES.md's bucket table — would have caught the missing notice and would NOT have caught the wrong bucket row, since that row was internally consistent while being incomplete. The real one is the import trace. Not obviously worth either, which is why this is a question about a gate rather than a task to write one." + }, + + { + "id": "adj-shards-carry-a-third-upstream", + "source": { "file": "docs/design/track-b-native.md", "item": "⑦" }, + "status": "open", + "severity": "question", + "owner": "user", + "blockedBy": ["a licensing opinion"], + "note": "Found by gate:notices on the first run it ever did, which is the strongest thing that can be said for having built the trace. build-adjacency.mjs reads data/pages/ayah-pages.json and writes `page` and `dPage` into EVERY edge of all 114 shards — 3,002 of them. That table is derived from the KFGQPC page corpus, which LICENSES.md buckets separately as 'not ours to relicense', under terms neither of the adjacency row's two upstreams grants. So the shards may have three parents rather than the two just established. They may equally have two: which page an ayah falls on is arguably a fact about a printing rather than expression, and this repo already treats it as a measurement — the same table is used in REVERSE to identify which print a corpus follows (the V1/V2 argument in data/pages/PROVENANCE.md), which is not a thing one does with someone else's authorship. The shards carry no page image, no glyph and no text; they carry the integer. Not decidable from here and guessing costs more than waiting: naming KFGQPC on the row without grounds overstates what the app owes AND propagates into the NOTICE.txt shipped to every reader, while leaving it unnamed understates it if the answer runs the other way. Both are worse than an open question with the number attached to it. What would answer it: the same licensing opinion gpl-and-the-app-store is already waiting on, with this added to its scope — a fourth question for a reader already being asked three, which is cheaper than a second engagement. Until then gate:notices carries the deferral explicitly and the deferral is only valid while this row is open, so it cannot be forgotten and cannot outlive its answer. WIDENED 2026-08-16: the dependency audit measured the same table reaching two further shipped outputs — all 44,431 root-shard occurrence tuples and assets/manifest.json verbatim — so the opinion this row waits on has to be asked about three outputs, not one. Carried as the-pagination-question-covers-three-outputs rather than rewritten here, because this row is cited from track-b-native.md and from the notices gate's deferral and widening it would change what those pointers mean." }, { @@ -445,7 +483,7 @@ "severity": "defect", "owner": "agent", "blockedBy": ["gpl-and-the-app-store"], - "note": "PLAN.md §Track B reads 'Capacitor iOS, then Android', with Android as a ~95%-shared-code fast-follow. On the licences this repo vendors that order inverts: the store that blocks is Apple's, so the reachable order is Android first and iOS gated on gpl-and-the-app-store. The same section carries a second, smaller error: it cites the iOS deep-link caveat as 'research §5', and §5 of docs/research/2026-07-20-mobile-svg-pwa.md is 'Offline: quota is a non-issue; eviction is the issue' — the deep-link caveat is §6. Both are one-line edits. Deliberately NOT made in the commit that introduced the scoping doc: a roadmap edit that changes a track's stated order should be its own decision with its own record, not a footnote to a document nobody has reviewed yet. Blocked on the licensing opinion for the same reason — if that opinion comes back saying the §7 route is available after all, the order it dictates changes again, and editing the roadmap twice is worse than editing it once." + "note": "HALF FIXED 2026-08-15: PLAN.md:1297 now cites research §6. The citation was checked rather than trusted — §5 of docs/research/2026-07-20-mobile-svg-pwa.md is the offline finding (quota is not the problem, eviction is) and §6 is the iOS deep-link caveat — so this row was right about which number was wrong. That half was safe to take on its own precisely because it is a pointer and not a plan: it changes nothing about what Track B does or in what order, so it carries none of the reason the order half is being held. The ORDER half stands and stays blocked on gpl-and-the-app-store, which is why this row is still open. ORIGINAL NOTE FOLLOWS. PLAN.md §Track B reads 'Capacitor iOS, then Android', with Android as a ~95%-shared-code fast-follow. On the licences this repo vendors that order inverts: the store that blocks is Apple's, so the reachable order is Android first and iOS gated on gpl-and-the-app-store. The same section carries a second, smaller error: it cites the iOS deep-link caveat as 'research §5', and §5 of docs/research/2026-07-20-mobile-svg-pwa.md is 'Offline: quota is a non-issue; eviction is the issue' — the deep-link caveat is §6. Both are one-line edits. Deliberately NOT made in the commit that introduced the scoping doc: a roadmap edit that changes a track's stated order should be its own decision with its own record, not a footnote to a document nobody has reviewed yet. Blocked on the licensing opinion for the same reason — if that opinion comes back saying the §7 route is available after all, the order it dictates changes again, and editing the roadmap twice is worse than editing it once." }, { @@ -707,6 +745,21 @@ "owner": "user", "note": "The other half hour, and it asks a question the first one cannot. A forced choice returns a preference — our rectangle beat the one it was shown against — and a correction that points the right way and falls a third short wins every trial while still being wrong. Built 2026-08-12: `pnpm nudge:marks` renders one rectangle per trial, starting a fixed distance out in a direction that changes every time, and the reader puts it where it belongs; `pnpm nudge:score` subtracts what the machine proposed from where it was actually put, and what is left is a magnitude in units. It reports that against the reader's OWN precision, measured in the same sitting from marks shown twice, because a residual smaller than the hand that produced it is not a finding. Deliberately a second session rather than a drag handle on the first: adjusting a rectangle inside a two-panel trial and then scoring the landing against the two starting positions IS the answer key, and adjusting after answering teaches the direction of the correction, which points the same way on nearly every page. The two should not be worked back to back by the same person. Risk rather than question because mark-C's thresholds are a claim about magnitude and only this can check one." }, + { + "id": "check-placement-holds-off-its-own-pages", + "source": { "ledger": "placement-holds-off-its-own-pages" }, + "severity": "risk", + "owner": "user", + "note": "The third sitting, and the first one that can be wrong. Its two predecessors both drew every trial from the same forty pages the correction was worked out on, because a trial needs a proposed move to start from and only those forty had one — so between them they could establish that the correction points the right way and nothing else. Built 2026-08-12 once all 604 pages carried a measurement: the builder now takes the pages to draw from as an argument, will hold out the pages an earlier sitting used, and picks by preference from the pages wanting the largest and smallest moves rather than at random. That last part is the whole point — across the original forty the proposed moves spanned 0.31 units of which most was noise, and across the held-out forty it spans 1.75 across and 3.38 down, four to six times the leverage per placement, which is the difference between being able to read the SIZE of the correction and not. Two blocks, deliberately on pages that do not overlap: forty-seven placements over forty held-out pages for whether it travels, and twenty-three over five pages at four or five each for how much of the leftover belongs to the page rather than to the hand — the second thing being what the clustered interval currently has to assume the worst about. Neither block has the correction applied to it: applying it first would make a genuine nought indistinguishable from a coincidence and would bake an unresolved number into the instrument measuring it. FOUR PREDICTIONS ARE BANKED IN THE LEDGER BEFORE ANYBODY PLACES ANYTHING, which is the part that makes this a check rather than an exercise — direction at 90% or better, the leftover returning either near (-0.073, -0.110) or near nought, the size estimate at 1.0 with an interval a fifth of a unit wide, and the between-page spread exceeding the within-page one. Each says in the ledger what its own failure would mean. What this still cannot answer is whether the leftover is a fact about the print or about one reader; only a second person placing the same rectangles separates those, and that block is designed in the plan and not built. Scoring is guarded twice over: the answers carry a fingerprint of the displacements AND of the chosen pages, and the scorer refuses a displacements file that has no row for a page the sitting used, because a file can carry the right fingerprint and still be the wrong file for these pages. Risk rather than question for the same reason as its neighbours: mark-C cannot ship on a correction judged only where it was fitted." + }, + + { + "id": "check-placement-what-kind-of-wrong", + "source": { "ledger": "placement-what-kind-of-wrong" }, + "severity": "risk", + "owner": "user", + "note": "The fourth sitting, and the first one that does not answer in a distance. Its three predecessors all ask how far — is our rectangle preferred to the one beside it, how far does a reader move it, does either answer survive on a page the correction never measured — and a distance is the right currency right up to the point where the best correction places most rectangles from their own ink and the remainder stops being rectangles that are merely far out. Measured over the whole book: where a rectangle was placed from its own ink the printed strokes fill about a fifth of it and roughly one in eighty is effectively bare; where the match was weak it is a ninth and more than a QUARTER are bare; where the best match sat on the edge of how far the search was allowed to look it is a twentieth and more than a THIRD are bare. About a third of what is left is a rectangle over blank paper, and no distance can say `there is nothing in this place`. Nor can one separate right-place-wrong-size from right-size-wrong-place — two faults with two repairs in two files, and a size complaint written as a displacement is a wrong number that looks like a right one. Built 2026-08-13: `pnpm report:marks` draws sixty of them one at a time, each croppable between the whole word and a close look because the first attempt at this was stopped after one screen for not being able to tell WHICH letter a mark belonged to, and the reader answers in words from a vocabulary of five — move it, wrong shape, box the right ink instead, the print is odd, bank it. As many as are true, not one: forced to pick one a reader picks the most obvious and the second fault is never recorded by anybody, which is the deliberate break from all three earlier sittings. Two of the five carry their own rules. Wrong shape carries no coordinates and never will, for the reason above. The print is odd is the only answer on the page that is not about us — we did not draw those pages — and it routes into THIS catalog as rows of its own rather than into any ruling that grades a correction, because counted as a placement failure a printing oddity makes our correction look worse than it is and counted the other way, better. The page reports how many are left and nothing else: no score, no running share, no verdict, because a reader who can see the score answers the score. Risk rather than question because both readings of the tail in mark-registration.md §⑩ ⑨ are live — if most of the remainder is the print then every error figure in that document overstates our own by that share, and if most of it is rectangles that could have been placed and were not then the search is too timid and that is a change to make. Widened the same day into TWO sittings of sixty, and the second one is the one that matters most: everything above is about the six in a thousand the correction refuses to place, and the other 99.4% — every rectangle put where its own printed ink is — has never been looked at by anybody. Its only evidence is a held-out split, which defends against a correction that fits noise and not at all against one that is confidently wrong the same way everywhere, since the ink is both what it was fitted to and the only thing that has ever been asked. Three faults in sixty there is a few per cent of 324,638 marks, which is thousands, and a fact about the whole book; the identical share in the tail is a rounding error. The two are never pooled and there is no combined figure, here or in the scorer: the populations differ in size by a factor of 173, so a rate over both would be a fact about how many of each the sampler happened to draw. `pnpm report:score` reads each ruling, re-derives which population every answer came from rather than believing the file, prints an interval beside every share because sixty marks cannot tell one in fifty from one in seven, and takes the marks called odd in the print out of the denominator rather than into the numerator — printing the rate both ways so the size of that choice is visible." + }, { "id": "plan-perf-verdict", @@ -805,6 +858,24 @@ "closedBy": "apps/web/e2e/word.spec.ts", "note": "Fixed 2026-08-05 by word-D, in the order the note below predicted and with one deliberate departure from it. The blocker went first: the alignment landed, a word run now refines the hop list, and only then did the keyboard path get built — Enter on an already-selected ayah descends, ←/→ carry the run, Shift extends it, Escape climbs one rung. The departure is the announcement. This note said it should name the run's first and last word; what shipped counts the outcome instead — «٧ مواضع مشابهة», how many places the run is about — because naming the words means putting scripture in a UI string, which is the one thing the word grain exists not to do (word-indexing.md §10, and §11 ② for the argument). The overlay stays decorative and e2e/word.spec.ts still asserts the accessibility tree is untouched, so the double-edged test kept both its edges: it protects the ayah buttons AND is no longer the proof of a gap. What is still owed is a human's: validation ledger `screen-reader-walkthrough` is the only thing that can say a count is enough for a memoriser. PLAN 15 carries the full account. Original note follows. Opened 2026-08-04 by word-C, in the same commit that shipped the feature — this is the first thing Hifth can do that a screen-reader user cannot, and it is written down rather than left to be found. The descent to word granularity is a 350 ms press inside an already-lit ayah, a gesture with no key that stands for it, and what it produces is ink in #hifth-overlay, which is decorative and adds no node, role or name to the tree. e2e/word.spec.ts asserts that emptiness before and after, deliberately double-edged: it protects the ayah buttons from being buried under overlay nodes, and it is the proof the feature is unreachable without sight. Not fixed now because nothing above the stage consumes a word run yet — onSelectWords has no listener until 13's print-vs-QAC alignment lands, so a keyboard path would reach a state that does nothing and a live region would announce a phrase and then fall silent. Order to fix: 13 first, then a key that descends from a selected ayah and an announcement naming the first and last word of the run rather than counting it. PLAN 15 carries the reasoning." }, + { + "id": "plan-golden-baselines-predate-the-spread-triage", + "source": { "file": "docs/PLAN.md", "item": "16" }, + "status": "fixed", + "severity": "defect", + "owner": "user", + "closedBy": "apps/web/e2e/golden.spec.ts", + "note": "CLOSED 2026-09-01: the owner accepted the post-hop framing (the page one pixel lower, highlights unchanged) and both baseline sets were rewritten in one commit — darwin by make golden-update, linux by make golden-linux UPDATE=1 in the pinned image — the same twelve shots on each platform and nothing else. ORIGINAL NOTE FOLLOWS. Reproduced 2026-09-01 on darwin, Chromium, the built app on 4173: 12 of the phone goldens fail (selection, breadcrumb and phrase on pages 1, 7, 9, 19, 604, and the page-7 marquee), from 2,624 px (0.01) to 59,475 px (0.12) different, the marquee 574 -> 575 px tall. Bisected by building one commit at a time: passes at 7d4402e, fails at b90b450 with the identical count, unchanged through ff81f8e, and the working tree adds nothing (same counts with the tree stashed). Owner-owned because the only open question is whether the post-hop framing b90b450 introduced is the wanted one; if yes, `make golden` and `make golden-linux` in one commit that says why. PLAN 16 carries the numbers and the bisection." + }, + { + "id": "plan-two-ayah-boxes-are-cut-short-of-the-line", + "source": { "file": "docs/PLAN.md", "item": "17" }, + "status": "blocked", + "severity": "defect", + "owner": "agent", + "blockedBy": ["the upstream page cache, a 348 MB fetch nobody starts without asking"], + "note": "Reproduced 2026-09-01 by the whole-book sweep: 2 of 12,350 rectangles are off the line grid, 68:3 on page 564 (27.9 on a 36 line) and 107:2 on page 602 (28.3), both a left-margin tail cut ~8 units short with the next ayah's box starting that much early. Held at 2 by gate:boxes as ceiling and floor. Agent-owned because the repair is mechanical (a vendoring repair entry, re-vendor, re-pin, lower the gate to 0); the one owner-visible part is that two pinned pages change. PLAN 17 carries the numbers and both answers." + }, { "id": "plan-hifth-licence", "source": { "file": "docs/PLAN.md", "item": "5" }, @@ -866,8 +937,243 @@ "status": "open", "severity": "question", "owner": "user", - "blockedBy": ["placement-residual-by-hand"], - "note": "Opened 2026-08-12 by building the instrument that answers it, and it is the gap the row above cannot close by any number of trials. A forced choice yields at most one bit per trial — is the proposed move preferred to the alternative it was shown against — so it can confirm a direction and is structurally incapable of confirming a distance. Every threshold in that document's §⑦ is a claim about distance. What answers it is a placing session: one rectangle, started deliberately out of position, put where a reader says it belongs, with the proposed correction subtracted from the landing afterwards. Three outcomes, and all three are useful: the residual is smaller than the reader's own measured precision (adopt as proposed, and say the precision it was adopted at); it is larger and consistent (adopt with the residual applied); or it points the OTHER way from the forced choice's verdict, which is the most valuable result of the three and the reason the two sessions are scored separately and never summed. The runbook is `placement-residual-by-hand`; this row stays open until a residual is banked." + "note": "Opened 2026-08-12 by building the instrument that answers it: a forced choice yields at most one bit per trial — is the proposed move preferred to the alternative it was shown against — so it can confirm a direction and is structurally incapable of confirming a distance, and every threshold in that document's §⑦ is a claim about distance. What answers it is a placing session: one rectangle, started deliberately out of position, put where a reader says it belongs, with the proposed correction subtracted from the landing afterwards. The runbook is `placement-residual-by-hand`. It ran the same day, over 60 marks placed by one reader, and this is what came back. SETTLED by it: the correction points the right way — 59 of 60 landed nearer the corrected rectangle than the shipped one (98.3%, interval 91.1–99.7), typical miss 1.15 units from the shipped box against 0.39 from the corrected one; the reader's own precision is 0.03 units, from 9 marks placed twice from independent starting points; the pull of where each rectangle started came out at effectively nil, which is what the evenly-spread starts were for; and 0/60 placements said the box was the wrong size. NOT settled, which is why this is open rather than answered: the residual of (-0.073 across, -0.110 down) is four times the precision floor, but the 60 placements sit on 40 pages and two marks on one page share that page's frame — clustered by page, neither component clears nought (down [-0.230, +0.009], across [-0.223, +0.077]). It was marginal before that correction and is not established after it; most of the interval is still on one side, so our correction MAY be short. Recorded first as 'only the down component is separable from nought' and corrected 2026-08-12 when the scorer was taught to cluster; the lesson already existed in probe-mark-ink.mjs about its own sampling and had not travelled. Nor could the sitting measure the correction's SIZE at all: the 40 pages propose nearly the same move, so 'exactly right' and 'a fifth short' produce the same landings and the gain spans from the wrong sign to 2.5x. More trials do not fix that — see the-correction-is-only-judged-where-it-was-fitted, which is also why every placement so far came from a page the correction was fitted to, and which since closed its coverage half: all 604 pages are now measured, they vary far more widely than those 40 did, and the leverage this section lacked therefore exists. Verdict, therefore: adopt the correction as measured, do NOT apply the residual. Applying it would edit the recorded per-page displacements rather than the arithmetic that derives them, is the-displacement-correction-could-go-stale's neighbour, and is tracked as mark-C — but it is not to be done on this evidence. Ruled out along the way and worth not re-buying: not the mark's name (a mean per name leaves MORE spread, 0.425 vs 0.415 down and 0.545 vs 0.526 across, over 11 names), not a scale error across the page (t = -0.5 down, -1.6 across), not the starting point, not fatigue (t = 0.2 down, -0.5 across against trial order). What would close this: a sitting on held-out pages, several marks per page so the page and the hand can be told apart, and a second reader — disagreement over 0.03 units would make the residual a fact about a person rather than about the print. This is not nobody-has-looked-at-the-placement-verdicts' answer either: a distance and a preference are scored separately and never summed. The sitting that would close it is now built rather than only described — check-placement-holds-off-its-own-pages, on forty pages held out of the measurement plus five pages placed four deep, with what it expects written into the ledger before anybody places a rectangle so that it can fail. Two of the three things named above are in it; the third, a second reader, is designed and not built, so whatever leftover it banks is still one hand's." + }, + { + "id": "the-correction-is-only-judged-where-it-was-fitted", + "source": { "file": "docs/design/mark-registration.md", "item": "⑧" }, + "status": "open", + "severity": "risk", + "owner": "agent", + "note": "Opened 2026-08-12 as 'only forty pages have a measured correction'; renamed the same day when the coverage half was closed and the name stopped being true. HALF CLOSED: the full pass ran 2026-08-12 over 30,000 marks and 604 pages, and 600 of 604 now carry a measured correction. The four without — pages 1, 2, 603 and 604, the short ornamental frames — never draw enough marks to clear the 20-mark floor, so mark-C needs a STATED fallback for them (the average of everything else) rather than a silent one; a page with no row was never looked at, not found to be right. WHAT IT ANSWERED: the corrections do vary, and the first 40 pages hid it. Those 40 were representative in the centre — their mean correction is indistinguishable from the other 560 (t = -0.1 across, -1.2 down), which was not guaranteed — but understated the spread badly (page-to-page sd 0.107/0.097 against 0.172/0.165 over the rest), and most of what spread they did show was measurement noise: scoring the same 40 pages twice from different mark samples disagrees by about as much as those pages differ from each other. That is precisely why the sitting built on them could confirm direction and never size. The variation is also not the same on both axes: 95% of pages need a downward move inside the original range, so down behaves nearly like one global number, while only 51% fall inside the original across range, about nine pages need essentially NO across move while needing the usual down one, and page 113 wants to move the opposite way down from every other page. WHAT THAT UNBLOCKS: the original 40 offered a 0.31-unit span of proposed moves, nearly all noise, which is why a-preference-does-not-say-how-far came back undecidable on size; across all 604 that span is over 1.75 units, so a sitting drawing marks from both extremes gets several times the leverage per placement — enough to tell 'exactly right' from 'a fifth short', which no number of placements on the original 40 could have done. The zero-across pages and page 113 are worth the most, being where one-number-for-the-print and one-number-per-page disagree loudest. STILL OPEN, and the reason this row is: no reader has judged a single mark on a page outside the original 40. Every by-eye placement to date came from a page the correction was fitted to, because a trial needs a proposed move to start from. Only a held-out sitting separates a fit from a finding, and it can be built now. ALSO MEASURED, and new: taking each page's own displacement out is most of the repair and measurably not all of it — separation -0.234 to +0.281, marks beating their own null 13.5% to 75.7%, but 19.49% still further than 0.75 units off afterwards and a corrected median overlap of 0.492 against the 0.909 a per-mark best fit reaches. Whatever that remainder is, it is not a page-level shift, and it is NOT the same quantity as the leftover distance in a-preference-does-not-say-how-far — that one is a further move of the correction, this one is scatter the correction cannot reach. Neither is applied. Both shift files are now committed under docs/validation/rulings/ so a banked verdict re-derives from committed bytes. WHAT CHANGED 2026-08-12, later the same day: the sitting this row asks for is built — check-placement-holds-off-its-own-pages draws every trial from pages held out of the original forty, and from the extremes of the spread measured above rather than at random, so it is the first by-eye evidence about these boxes that is not a check on its own fit. This row stays open regardless, and the distinction matters: an instrument existing is not a reader having looked. It closes when that sitting is placed and read, not when it was built. AND THE OTHER INSTRUMENT, later still the same day: the coverage argument was originally applied only to the placing session, on the reasoning that the forced choice was a separate instrument whose headline did not depend on the correction being right — but the limit is about which pages can be asked about, not about what is estimated from them, and it applies identically. check-placement-correction-by-eye is the check that actually releases mark-C, and until now every one of its hundred trials came from the forty pages the correction was fitted to. Its build now holds those forty out and takes forty of the remaining 560; selectPages grew a second strategy for it, spread evenly through the print rather than drawn from the extremes, because a forced choice reports a proportion and a proportion has no leverage to gain from the extremes and its representativeness to lose. That discards the five answers banked against the old build, deliberately and with the owner's agreement: five in-sample answers are not worth the coverage claim they would cost. score-mark-adjudication.mjs now replays the recorded page list, the same guard the placing scorer already had. SUPERSEDED IN FRAMING 2026-08-13, and this row stays open under it: the-frame-is-registered-on-ornaments-and-used-to-place-text names the cause of the remainder above. Everything this row measures — the residual scatter no per-page shift reaches, the 19.49% still badly out — is that cause seen from the outside. It does not close this row: the coverage question here is about which pages a READER has judged, which no amount of arithmetic answers, and the held-out sitting is still unsat. But it does change what the sitting should be sat against, because a per-page correction is no longer the model anybody expects to ship." + }, + { + "id": "the-frame-is-registered-on-ornaments-and-used-to-place-text", + "source": { "file": "docs/design/mark-registration.md", "item": "⑨" }, + "status": "open", + "severity": "defect", + "owner": "agent", + "note": "Opened 2026-08-13 as the ROOT CAUSE under a run of rows that each measured a symptom of it. The alignment between our pages and the second printing whose geometry we vendor is fitted, per page, on the ayah-end ornaments — 5 to 20 point pairs, the only objects both corpora label — and the four numbers it produces are then used to place mark rectangles and word boxes, neither of which the fit has ever seen. Page 7 closes on its ornaments to 0.0912 units while its ink is out by about 1.0; corpus-wide the fit's self-reported residual correlates 0.117 with the real error, 0.019 with the irreducible per-page residual, and -0.022 with |dty|, the LARGER component. It grades hardest on the part that matters least. This is Fitzpatrick's FRE/TRE result (SPIE Medical Imaging 2009, cited in §⑤) reproduced in a mus'haf, and it is a documented trap rather than an exotic case — three of this project's sessions were spent arguing about that residual, which is exactly the failure the literature describes. IT EXPLAINS ALL FOUR SYMPTOMS AT ONCE, which is why it is one row and not four: off the mark, wrong size, on the wrong mark, biting the neighbour. Nothing sits between the ink and the screen but those four numbers; a wrong sx/sy reads as wrong size AND as a shift growing with distance from the fit's centre, and a shift exceeding the gap between adjacent marks reads as the wrong mark. The control that rules out the alternative is the labelling census: 326,515 marks, genuine mislabels ZERO. It is all displacement. WHAT IS SETTLED. (a) The per-page family is EXHAUSTED — the best transform fittable to a page from its own ink still leaves ~0.44 units of across-scatter and ~0.34 down, ~1 mark in 6 badly out; adding a scale term removes 1.8%/1.0% of that and makes across WORSE on 189 of 600 pages, and a mark set 0.14% narrow is invisible, which retires the wrong-size hypothesis. THE FIGURES BEHIND THAT SENTENCE, banked here because they exist in no other file (run of 2026-08-13, 604 pages, 30,000 marks; the field names alone survive in probe-mark-ink.mjs). The stretch: dsx median -0.00138 with |t| 22, dsy +0.00047 with |t| 15 — statistically certain and practically nothing, which is the whole point of writing both halves down. At the page edge that is worth 0.29 units across and 0.17 down, against a mark 5.6 by 3.6 and a translation of 0.50 across and 1.17 down; a real effect two orders below the one that matters is a footnote, and it is only recognisable as a footnote if the significance is stated beside the size. The ceiling, per page, p10 / median / p90 of the scatter left once each model has done its best: translation only, 0.324 / 0.454 / 0.673 across and 0.248 / 0.348 / 0.511 down; adding a scale term, 0.297 / 0.438 / 0.665 across and 0.242 / 0.338 / 0.487 down. Read as a gaussian, marks landing further than 0.75 units from their ink go 17.9% -> 15.8% — and 17.9% against the 19.49% actually measured is a useful check that reading the ceiling this way is sound at all. (b) The error is organised BY PRINTED LINE, which was predicted from typography (differing glyph advances and line metrics accumulate along a line and reset at the next) before our own numbers were looked at: every rung has since been re-scored through the shipping scorer on held-out marks (120 pages, ~63,600 marks, half of each page's marks fitted and the other half graded), and they agree with the arithmetic to within half a point: marks beyond 0.75 units go 96.63% as shipped → 18.38% [17.96, 18.81] per page → 11.77% [11.42, 12.13] per line → 5.24% [5.00, 5.50] per tilted line, and median ink overlap goes 0.015 → 0.494 → 0.584 → 0.681 against a ceiling of 0.909. Two things make that a signal rather than a richer model flattering itself: every line made to wear ANOTHER line's correction gives 31.2%, worse than applying no per-line correction at all and six times worse than the real one; and at the page grain, grading on the fitted marks gives 18.12% against 18.38% held out, a quarter of a point, which is what no overfitting looks like. A floor sweep 2–10 is flat. ONE FINDING KEPT SEPARATE, per the standing rule against summing distinct failure modes: the remainder has changed CHARACTER, not just size. A 2-D gaussian of the measured spread predicts 25.06% / 11.46% / 3.10% for the three rungs against 18.38% / 11.77% / 5.24% measured — so the page grain's tail is lighter than gaussian, the line grain matches, and the tilted line is HEAVIER. After the best correction a minority of rectangles are badly out for a reason of their own rather than every rectangle being slightly out. THAT REASON WAS LOOKED FOR ON 2026-08-15 AND HALF OF IT WAS FOUND. The badly-out marks are not scattered: 54.04% of them sit on the 590 printed lines in 8,817 that are at least a quarter bad, so whole lines go wrong rather than individual marks going wrong here and there; and a mark at either end of a line is about twice as likely to be badly out as one in the middle (6.58% and 6.95% against 3.31–4.34%), which is what accumulation looks like when it does not accumulate at a constant rate — and is not a shape a straight line has the freedom to follow. So a fourth rung was added to the ladder, line-curve, which lets each printed line's correction bend rather than only tilt. Held out, over all 326,515 rows and 8,702 printed lines: badly out 4.95% → 4.12%, median leftover 0.224 → 0.198 units. Those are displacement arithmetic over the whole corpus, NOT the rasterising scorer over 120 pages that produced the 5.24% quoted above, so 4.95% is the tilt's own baseline on the same population and the two pairs must not be crossed. The control holds at the new rung: a line made to wear another line's bend gives 34.40%, against 18.20% for applying no per-line correction at all. A cubic was fitted the same way and rejected — better on its own training marks and worse held out — so the ladder stops at the bend rather than wherever the arithmetic stops improving. THE OTHER HALF IS STILL UNEXPLAINED, and it is the half the finding started from: bending fixes the middle of a line (4.34% → 2.94%) and barely touches the ends (6.58% → 5.97%, 6.95% → 6.02%), so a mark at the end of a printed line is STILL about twice as likely to be badly out as one in the middle, and whatever is doing that is not a shape in where along the line a mark sits. That is left standing rather than chased with a fifth rung. (c) WHATEVER SHIPS MUST MOVE THE TEXT AND LEAVE THE ORNAMENTS ALONE — probe-ornament-witness.mjs asks the one object the ink fit could not have been fitted to and gets 0.053 units under the shipped fit against 1.206 under the ink correction, worse on 120 of 120 pages. That is also the only non-circular evidence in this whole line of work. WHAT KEEPS IT OPEN: nothing has shipped. R3/R4 are not expressible in the four-number pin at all and need a per-line table that both build-words.mjs and applierFromPin read; the pin places word boxes as well as marks, so adopting one rebuilds assets/words/**, re-pins the per-page sha256 and may move goldens; pages 1, 2, 603 and 604 never clear the floor and need a STATED fallback. Those are a decision, not a measurement, and they have now been written up as one: mark-placement in docs/decisions.json, whose four options are drawn on a real page of the mus'haf in docs/design/mark-placement.html and published for anybody who has no repository. This row closes when one of them is chosen and shipped, not when it was drawn. The word-box half is option G specifically — move the marks and leave the words — and it is the half most at risk of being settled by nobody, because it costs nothing by any number above: every number above is measured on marks. Instruments: probe-mark-ink.mjs (the scorer), lib/registration-grain.mjs (the grain models, split-half and shuffled controls), probe-ornament-witness.mjs (the witness)." + }, + { + "id": "whether-what-is-left-over-is-even-ours", + "source": { "file": "docs/design/mark-registration.md", "item": "⑩" }, + "status": "open", + "severity": "question", + "owner": "user", + "blockedBy": ["placement-what-kind-of-wrong"], + "note": "Opened 2026-08-13 as the named half of the tail the-frame-is-registered-on-ornaments-and-used-to-place-text refused to average into its figures. That row establishes that the remainder has changed CHARACTER rather than only size — a heavier-than-gaussian tail after the best correction, so a minority of rectangles are wrong for a reason of their own — and then stops, because every instrument in that line of work answers in a distance and the question here is not a distance. It is which of four things a bare rectangle is: somewhere else entirely, the right place at the wrong size, the right size in the wrong place, or a page that was printed unusually and is not our fault at all. THE FOURTH IS WHY THIS IS A ROW AND NOT A PARAGRAPH. We did not draw those pages, and there is currently no route by which a reader who notices something odd in the printing can say so — the placement rulings have nowhere to put it, and put anywhere else it silently becomes a grade on our own correction, in whichever direction happens to flatter or damn us. This catalog is that route, and answers of that kind become rows here, one per finding, sourced to the page they are about. Deliberately owner:user and deliberately not narrowed by any probe: a probe can settle where a mark is and is structurally incapable of settling whether a printed page looks wrong to somebody who reads mus'hafs. What closes it is the sitting in check-placement-what-kind-of-wrong being placed and read, plus whatever print rows it opens being triaged like any other defect — not the instrument existing, which it already does. Read against the standing rule that distinct failure modes are never summed: the share that comes back as the print is a correction to be applied to this project's error figures, not a number to add to them." + }, + { + "id": "the-reader-could-not-see-the-box-they-were-vouching-for", + "source": { "file": "docs/design/mark-registration.md", "item": "⑪" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/build-mark-report.test.mjs", + "note": "Found 2026-08-14 by auditing the page that asks whether-what-is-left-over-is-even-ours, BEFORE the sixteen sittings that will answer it were sat. It is a row here rather than an ergonomics note for one reason: it changes what an answer MEANS. The crop draws the print's own artwork and that paper is never re-themed, so a mus'haf page stays white in a dark theme — but the two rectangles drawn on it were themed, and under dark our line fell 5.05:1 -> 2.49:1 against that paper and the reader's own 4.89:1 -> 1.70:1 at a constant ~1.64px stroke. The commonest honest answer on that page is nothing-is-wrong; a reader who cannot make out the rectangle gives exactly that answer and it banks as an affirmation indistinguishable from a considered one. Same shape as nobody-has-looked-at-the-placement-verdicts and the same reason that one was rewritten: an instrument whose failure mode is agreement flatters whatever it is measuring, and nobody files a bug about being agreed with. Closed by a rule, not a colour — nothing drawn on the paper is themed; the paper, the ink and the four rectangle colours are stated once in the base palette and never restated for dark, while the controls around them stay themed because they sit on the app's surface and not on the print. The two rectangles are also separated by a dash pattern rather than by hue alone, so the distinction survives colour blindness and survives a later re-theme. The test asserts the dark block redefines none of the six and that both rectangle lines clear 3:1 against the paper. WHAT THIS DOES NOT CLEAR: no transcript records the theme it was sat in, then or now, so the sixty answers already banked from the placed set cannot be shown free of it by anything but the reader's memory — the argument that those cards were safe is an argument about the cards (placed from their own ink, gross error structurally impossible), not evidence about the screen." + }, + { + "id": "the-scorer-averaged-increments-and-printed-zero", + "source": { "file": "docs/design/mark-registration.md", "item": "⑫" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/score-mark-report.test.mjs", + "note": "Found 2026-08-14 against the banked transcript rather than argued from the code. The scorer printed 0.000 across and 0.000 down for twenty-six marks the reader had visibly dragged, which is why this is a row: a measurement was taken, reported, and was not the quantity it was labelled. Two faults arriving as one number. (1) It took the middle of every recorded STEP across every move, and steps cancel — a nudge one way and a nudge back average to nothing, and one mark corrected forty-four times outvotes twenty-five marks corrected once. Nothing is wrong with the field; medianing it was the error. (2) The running total beside each step is measured from the rectangle as SHIPPED, so it already contains the correction the pipeline applied. That is a real quantity worth printing — where the reader put the box against what the app draws today, -3.569 / -3.134 — but it is not the reader's own hand, which is that same total less the correction, -2.468 / -2.010. The middle figure is the one that corroborates the ink measurement in mark-registration §④ (the print sets its text lower and further across than the ornament fit predicts, 599 of 600 pages): two instruments built from different evidence, same direction, same rough size. A scorer printing zero said the hand agreed with nothing. Closed by one row per mark reading only where the rectangle finally rests, the two distances printed under two SEPARATELY WORDED sentences with the prose saying outright that the gap between them is only the correction already applied — otherwise somebody differences them and believes they have found a discrepancy — and the marks-to-goes ratio stated as its own finding (26 marks over 205 goes is a complaint about the nudge controls, not about the print). Distances and counts are never combined. The fixture that would have caught it is now in the tests: one mark nudged out, back and out again against a second moved once, asserting the printed middle is the distance and the count is two marks rather than four steps." + }, + { + "id": "a-sitting-banked-what-was-left-not-what-was-seen", + "source": { "file": "docs/design/mark-registration.md", "item": "⑬" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/settle-mark-report.test.mjs", + "note": "Found 2026-08-15 in the first sitting over the fallback population. The page that asks the questions banked how many marks were STILL TO GO rather than how many the reader had been through: 115 seen, 19 written down. That is a row rather than an ergonomic note because it is the denominator of every rate this project reports about placement — a count of complaints over a count of marks looked at — so the sitting's whole output was a set of correct answers divided by a wrong number. It was caught only because it was caught at the end: the rates came out 600.0%, 2280.0% and NaN%, and a reader who had stopped four fifths of the way through would have produced a believable error rate over a denominator nothing in the file admitted was wrong. Closed three ways, because a page can always be got wrong again. The count is written from what was answered rather than from what was left; both readers of a transcript take the number of marks actually spoken about as a floor; and when either raises a claimed count to that floor it prints the transcript's own file name while doing it, so the line reads as a defect in the page rather than a quirk of the file. Deliberately a warning and not a refusal — the answers in a miscounted sitting are still good, and discarding an hour of somebody's eyes over a wrong header is the more expensive mistake. The direction is the durable part: a count of marks somebody has looked at can only ever go up, so a count that falls is never a fact about the reader." + }, + { + "id": "print-oddities-called-out-by-eye", + "source": { "file": "docs/design/mark-registration.md", "item": "⑭" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "Raised 2026-08-15 by the sittings so far over the fallback population. Fifteen marks on fifteen separate pages were set aside as odd — I cannot say how: not our rectangle in the wrong place, but something about the printed mark itself the reader could not read as ordinary. Pages 18, 19, 142, 230, 233, 242, 294, 390, 437, 440, 441, 461, 581, 582, 604; marks 18:25, 19:377, 142:336, 230:249, 233:332, 242:429, 294:138, 390:39, 437:117, 440:75, 441:470, 461:417, 581:18, 582:249, 604:50. Six fathas, four successive kasratans, two successive dammatans, one damma and two hamzas — spread across the vocabulary rather than one name behaving badly, which is the first thing that would have made it ours. They are held out of the denominator of that sitting's error rate, and being held out is what leaves them unsettled: neither counted against the correction nor evidence for it, and a category that costs nothing either way is one that quietly grows. owner: user because nothing in this repo can answer it — every instrument reads the same bytes the reader was shown, so a reading that is wrong about what the print contains is wrong identically in all of them. Settled only by holding those fifteen pages against another copy of this print: either the oddity is upstream, in which case the marks stay out of every rate on purpose, or it is in our rendering, in which case these fifteen are the visible end of something also silently wrong on marks nobody flagged. Measured in docs/validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json. EXTENDED 2026-08-17 by two more, and they matter out of proportion to their number: 86:23 superscript alef and 463:130 wasla, called odd in the same words. All fifteen above came from marks the correction could NOT place, which left the cheap explanation standing — that a mark nothing can find ink for and a mark that looks strange to a reader are the same marks for the same reason, and the oddity is downstream of whatever defeats the search. These two are from marks placed from their own printed ink, matched well enough to clear the bar, so whatever the reader is seeing survives a successful match and cannot be explained by the search having failed. Seventeen marks on seventeen pages across two populations sharing nothing but the print. FIFTEEN MORE ON 2026-08-17 from a full part of 106, which is the first number here worth calling a rate: 14 in every 100 looked at, against 9 in the 160 sat before. Three fathas, two kasras, two hamzas, two fathatans, two successive fathatans, a shadda, a successive kasratan, a successive dammatan, a kasratan, on 14 pages between 60 and 593 — marks 60:82, 92:91, 100:74, 100:393, 107:260, 285:453, 298:91, 304:143, 343:420, 355:48, 375:18, 440:72, 490:219, 507:545, 593:139. 32 marks in all, still no name behaving badly and no page carrying more than two. THAT THE RATE DID NOT FALL AS THE INSTRUMENT IMPROVED is the part worth noticing: every other figure from that sitting moved as the fixes predicted and this one did not move at all, so what a reader sees when they say odd is not something the page was doing to them. At 14 in a hundred, a full pass over what is left would set aside something like 240 marks on the grounds that nobody could say what was wrong with them. Measured in docs/validation/rulings/2026-08-17-placement-what-kind-of-wrong-placed.seed23.settled.json and docs/validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json. ELEVEN MORE ON 2026-08-17 from the first wrong-size part dealt off the recomputed corpus, and this batch is NOT spread across the vocabulary: five fathas, three hamzas, two kasras, one successive dammatan, on 11 pages between 333 and 596 with none carrying two — marks 333:319, 388:335, 414:247, 474:414, 526:318, 556:5, 572:91, 578:260, 584:304, 588:56, 596:223. 43 marks in all. Ten of the eleven are single-piece marks and the sitting was half doubled (33 doubled against 28 single among the marks answered), so odd was said about roughly one single mark in three and one doubled mark in thirty. TWO READINGS AND NOTHING HERE SEPARATES THEM: either single marks carry more of the oddity, or odd is the button left when a reader has no word for what is wrong and a doubled mark always gives them one — its rectangle is visibly the wrong size, a complaint with a name and its own button. The second reading makes this a fact about which complaints were available rather than about the print, which is counting-presses-stopped-meaning-anything seen from the other end. The check that decides it is unchanged. FOUR MORE ON 2026-08-19, finishing the same wrong-size sitting rather than starting a new one: a successive dammatan, a hamza, a small yeh and a successive fathatan, on four pages between 115 and 260 — marks 115:470, 138:226, 154:252, 260:248. 47 marks in all. The small yeh is a name not seen in this note before, which keeps the spread-across-the-vocabulary reading alive rather than settling either of the two above. ONE MORE ON 2026-08-19, from a band sitting on the placed population rather than the fallback or wrong-size ones — the first odd-in-the-print call from any of the five overlap bands sat at the bottom of the placed set: page 128, mark 128:85, a shadda. 48 marks in all. SEVENTEEN MORE ON 2026-08-19 AND 2026-08-20, closing the wrong-size deck's third part: on 17 pages between 76 and 598 — marks 76:148, 162:188, 272:455, 346:243, 347:329, 350:311, 357:232, 360:140, 374:9, 400:295, 438:236, 470:58, 568:379, 570:284, 574:399, 588:133, 598:221. 65 marks in all. Measured in docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.75_0.85.seed23.settled.json and docs/validation/rulings/2026-08-20-placement-weak-size-part3.seed23.settled.json." + }, + { + "id": "pointed-answers-are-not-hand-placed-ones", + "source": { "file": "docs/design/mark-registration.md", "item": "⑮" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "Raised 2026-08-15, from what the first sitting over the fallback population cost: 127 of the 128 marks the reader worked through were moved, over 226 separate goes, median 2.009 units of travel and worst 7.052 on a mark 5.6 by 3.6. Every go is a person doing two jobs — which mark is meant, which only a person can do, and how far across and down, which the print already knows — so the second was handed back to the print: the ink inside the window is cut into its separate pieces and a tap on a piece puts the rectangle round it, two taps when the mark is printed in two parts. That makes TWO KINDS OF ANSWER that must never be pooled, and this row exists to keep that fact alive after the code that produced it has been forgotten. The words banked are identical — the rectangle belongs somewhere else, it is the wrong size — because the reader is saying the same thing; what differs is who measured it. A hand-placed answer is an eye's estimate and its spread is partly the eye's. A pointed one is the reader choosing which ink, with the extent taken off the printed outlines, and its spread is the grain the ink was cut at. Averaged together the second tightens the first, and the tighter figure reads as the reader agreeing with us more closely than we thought while meaning nothing of the kind. Guarded rather than argued: every answer records which way it was given, and a press on top of a pointed answer gives up the claim, so a hand-finished correction can never be banked as one the print placed. What stays open is a question about readers rather than about the page. Marks already answered were answered on the older instrument, so any rate across both is a fact about the mix (the same trap as ⑬ in a different column); and making a correction cheaper is a change to the one ratio these sittings measure — complaining and affirming still cost a single tap each, which is the constraint the page is built to, but FINISHING a complaint no longer costs a dozen, so a reader who would have shrugged and affirmed may now say what they think. That is the point of the change and also why the two halves are not one population. Settled only by the same marks answered both ways, by the same reader, far enough apart that they are not remembering." + }, + { + "id": "a-finger-resting-on-the-box-banked-a-placement", + "source": { "file": "docs/design/mark-registration.md", "item": "⑯" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/build-mark-report.test.mjs", + "note": "Found 2026-08-15 while auditing the gesture pointed-answers-are-not-hand-placed-ones had just made the commonest one on the page, and it is a row here rather than an ergonomics note for the reason that rule exists: it did not make the page annoying, it made a number wrong. Two boundaries in two different units decided what a gesture was. A tap had to end within ten screen pixels of where it started AND inside 600 ms; a drag banked a placement once it had moved more than 0.05 page units. At the framing that shows the whole word ten screen pixels is about 1.5 page units — thirty times that floor — so the two left a gap, and a finger resting three-quarters of a second with a few pixels of tremor fell into it: too slow to be a tap, far too far to be nothing, banking a placement of nearly half a unit on a mark 5.6 by 3.6. Fixed to one boundary, in one unit, with no clock — ten screen pixels decides and both the 600 ms and the page-unit floor are gone. The clock never asked anything worth knowing, since a slow tap is still a tap and a reader steadying their hand is not making a different statement, and only distance can answer whether a finger stayed still; screen pixels rather than page units because a finger is the same size on every card and a page unit is not. The same audit corrected two smaller things about the same gesture: which piece of ink a tap reaches was decided by area alone across everything within a fingertip of slack, so aiming at the dead centre of a piece returned a DIFFERENT piece on 3.7% of aims at the wide framing and 2.3% of taps on blank paper reached past nearer ink to something smaller further away — ranking landed-on above came-near takes the first to 1.2% and ends the second; and a tap that reaches no ink now says so, because silence is indistinguishable from a control the page never received. WHAT THIS DOES NOT CLEAR: every transcript banked before the fix carries whatever it produced and cannot be cleaned of it afterwards, because a phantom placement is a well-formed placement and nothing distinguishes it from a real one. The affected marks are the ones reached for by tapping the ink, which is the same population ⑮ says must be read apart from hand-placed answers, so the caveat travels with that one rather than needing a second denominator." + }, + { + "id": "nothing-checked-the-sittings-were-still-current", + "source": { "file": "docs/design/mark-registration.md", "item": "⑰" }, + "status": "fixed", + "severity": "risk", + "owner": "agent", + "closedBy": "packages/etl/scripts/audit-mark-sittings.test.mjs", + "note": "The rebuild step of the sitting review ended in the only instruction in that routine with no command behind it — confirm the deal did not move, same parts, same total, nothing already answered coming round again — which meant sixteen files by eye at the end of an hour of somebody else's work, which meant nobody. Both failures are invisible from inside a part: a rebuild skipped or run with one hand-over left out re-asks answered questions on pages that look exactly like unseen ones, and a rebuild against a different measurement draws every rectangle from displacements that are not on disk, so every answer is about a picture nobody can reconstruct. Neither needed a person — a built part already carries what it was built from, because the reader's place is keyed on the measurements' fingerprint plus the set, slice and seed, with the answered set folded into the slice so that shrinking the pool cannot strand somebody at card ninety of a hundred and seventeen. It announces itself to anything that asks; nothing asked. `pnpm audit:sittings` asks, and eleven tests build each failure on purpose because an auditor that only passes the clean case is indistinguishable from one that always says yes. The reading of the word answered is shared with the rebuild rather than written twice, since two readings drift into a mark one drops and the other counts; the extraction left every built part byte-identical. First run found nothing wrong: 1,710 marks across 16 parts, 167 already answered, 1,877 in all, one measurement throughout — which was not knowable before and is a different claim from having assumed it. NOT A GATE and will not become one: the parts are build products and the answers accumulate on whichever machine served them, so in a clean checkout it would pass by being unable to look, which is the same reason the published-page sweep is not called one either." + }, + { + "id": "the-same-nudge-twice-magnified-the-page", + "source": { "file": "docs/design/mark-registration.md", "item": "⑱" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/build-mark-report.test.mjs", + "note": "Reported from a phone mid-sitting. Two taps close together in time and space are the browser's double-tap-to-zoom, and between the same arrow twice and two arrows a centimetre apart that is most of what placing a rectangle consists of. The magnification is not the cost: the card walks off the screen mid-answer, so the next press lands somewhere the reader did not aim and the transcript records a placement nobody made — and a reader who cannot keep the rectangle still stops nudging and affirms, which biases the exact ratio the sitting exists to measure. The page had carried touch-action since the drag was wired but only on the stage, so every control in the chrome kept the default. Fixed with `manipulation` on the body — the ordinary behaviour minus that one gesture — on the body rather than per button, because two taps on adjacent buttons trigger it as readily as two on one and a per-button rule would leave the commonest case unfixed. It costs nothing: touch-action is intersected down the ancestor chain rather than inherited, so the stage's pan-y and none still win, and pan-y already excluded double-tap zoom on the print. Pinch still zooms everywhere." + }, + { + "id": "a-sitting-was-reachable-only-from-the-machine-serving-it", + "source": { "file": "docs/design/mark-registration.md", "item": "⑲" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/lib/tailnet.test.mjs", + "note": "The serving side bound loopback by default, which is reachable from exactly the machine serving it, so every sitting ever sat on a phone depended on somebody passing a flag that was written down nowhere — not in the routine, not in a script, not in the Makefile. Underneath sits the sharper hazard: a reader's place and every rectangle they moved are kept per origin, host compared as text, so this machine's name and this machine's number are two separate memories of one sitting with nothing on screen to say so. That has already cost an hour. Fixed by one module that decides the single address, read off this machine's own interfaces rather than asked of a program (the private network draws from a range no home or office network uses, so an interface holding one IS that network); the server binds it by default — that network only, never every interface, because a sitting carries a write endpoint and a café's wifi is an interface too — and prints one address with the others named as things not to use. Two named commands and a sixth step in the sitting routine mean the incantation is no longer somebody's memory. Sees the same decision from the other side: sitting-hosting in the decision register." + }, + { + "id": "the-front-door-could-not-say-which-sitting-you-were-in", + "source": { "file": "docs/design/mark-registration.md", "item": "⑳" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/build-sittings-index.test.mjs", + "note": "Sixteen parts drawn as sixteen bare numbered tiles, so every question a reader arrives with — where was I, which have I finished, is there any point opening number nine — could only be answered by opening parts until one looked familiar, at a megabyte and a third each on a phone. The obvious fix does not work: a rebuild drops every answered mark, so at build time every part is untouched and every bar reads zero, and the live figure exists only in the log the serving side has been appending to. So the page carries the marks each part holds, fetches what this machine has heard and counts the overlap, and names the part to carry on with — the one begun and unfinished, because a part left half-done is the only place the re-deal cannot help. Answers for marks in no part on the page are ignored rather than added: one log covers every deal there has been, and counting them again would take the remaining figure below zero. The rule for which answers still stand is not restated in the generated page — that function is written closed over nothing and its own source is inlined, and a test pulls the copy back out, evaluates it with nothing in scope and makes it agree with the module it came from." + }, + { + "id": "pointing-at-ink-files-both-words-every-time", + "source": { "file": "docs/design/mark-registration.md", "item": "㉑" }, + "status": "confirmed", + "severity": "defect", + "owner": "agent", + "note": "Found 2026-08-17 reading two banked sittings, not argued from the code: 60 marks of 60 came back carrying BOTH placement and wrong-shape, with no mark anywhere saying one without the other. Nearly all were answered by pointing at ink. A point sets position and size in one gesture, and the emitter files a complaint for each whenever the change is anything other than exactly zero — the guard is floating-point epsilon, so two rectangles fitted independently to the same ink always trip both. The mark-level rate is NOT affected and stands: the reader confirmed every tap was a correction they meant, so 60/60 faulted is a real measurement of those bands. What is wrong is the split between the words — in the weaker band alone, 4 of 30 filed as wrong-size changed size by under 0.05 units and 4 filed as misplaced moved under 0.05 — so reading '37 wrong-size statements' as 37 marks judged the wrong size reads an artefact of the gesture. Deliberately NOT fixed with a threshold: any floor would be a number chosen rather than derived (a pointed extent comes off the printed outlines and is rounded to no grain), and it would answer the wrong question, because a reader who points has made ONE statement — the rectangle belongs around this ink — and the vocabulary has no word for it, so the page spends the two it has. Same observation as pointed-answers-are-not-hand-placed-ones one level in: that row says the two kinds cannot share a rate, this says the words stop meaning different things once an answer was pointed. Closes when a point has its own word, the existing two are reserved for what a hand says, and the settler reports the two kinds under separate headings. Until then word-level counts from any pointed sitting are not quoted and the mark-level rate is. HOW BIG THE DISTORTION IS, MEASURED 2026-08-17 on a further 106 marks: it is not fixed, it is a property of how close the rectangle already was. On the fallback marks, where the box starts far from the ink rather than nearly on it, the artefact all but vanished — 1 of 106 filed misplaced while moving under 0.05 units and 7 of 104 filed the wrong size while their size changed by under 0.05, against 4 of 30 for each in the bands — and two marks came back saying moved without saying wrong size, the first anywhere to carry one word without the other, which retires the word FOREVER in the source. So the sittings whose word counts are least trustworthy are the ones where the correction was already working, which is the opposite of the guess. Two consequences: fallback word counts can be quoted with the caveat attached and band word counts still cannot, and a sitting drawn from marks the correction places WELL cannot be scored on words at all — which is most of the book, and is the population any future sitting checking a good rule would have to come from." + }, + { + "id": "the-fallback-rule-may-be-wrong-in-two-different-ways", + "source": { "file": "docs/design/mark-registration.md", "item": "㉓" }, + "status": "answered", + "severity": "question", + "owner": "agent", + "closedBy": "packages/etl/scripts/lib/mark-ink.test.mjs", + "note": "ANSWERED 2026-08-17 AND THE ANSWER WAS NO: one part of sixteen was telling us about itself. Both position findings are refuted at book scale; the size finding survives and has moved to the-doubled-marks-are-drawn-too-small. Fitting the line correction over all 326,515 marks and measuring the residue class by class leaves every class within 0.09 units of zero on a box about 6.3 by 3.4 — fatha 0.011/0.013 on n=121,277, sukun -0.022/-0.016 on n=36,616, superscript alef -0.032/-0.054, all classes together 0.004/0.002 on n=322,011. In particular the marks that sit above the letter do NOT have a different vertical relation to the line: finding (2) below is contradicted directly, 36,616 sukun against twelve. THE PATTERN WAS A FACT ABOUT THE SELECTION, which is the general trap and is why the wrong reasoning is left standing in the record: this sitting is drawn from the marks the search refused, a mark is refused for one of two unrelated reasons (the ink was further than the search could look, or the search reached it and disliked the match), those two are not evenly spread across classes, and sorting the population by the name of the mark therefore sorts it mostly by which refusal it suffered. The relocation is the useful half — the fault is not in what a rectangle knows about each kind of mark, it is in the rule deciding a rectangle cannot be placed at all, which is the-search-could-not-look-far-enough and the-search-reported-answers-it-never-checked, both now fixed. Original reasoning, kept because it was reachable, internally consistent and wrong: From the first full part sat on the rebuilt deal, 2026-08-17: 106 marks, every one of them a mark the correction could not place from its own ink, so every rectangle was inherited from the printed line. All 106 faulted, which is what that population is selected to produce and says nothing. The finding is the split nobody had looked for: THE TWO THINGS A RECTANGLE CAN GET WRONG ARE NOT GOING WRONG ON THE SAME MARKS. Across the sitting the reader moved a median of 1.479 units and changed size by 0.130 — an order of magnitude apart on a box about 6.3 by 3.4 — so the line-inherited rule gets size nearly right and position badly wrong. Sorted by name that becomes three findings. (1) The single marks are misplaced and correctly sized: fatha, kasra, damma, shadda, hamza — 78 of the 106 — moved about a unit sideways and barely downward, size untouched to within a tenth. Hamza moved LEAST of the five, the opposite of the-weakest-matches-may-go-wrong-by-class, and the first evidence that its hamza finding belongs to the weak tail of the placed population rather than to hamza. (2) The marks sitting above the letter are misplaced downward as well, and by much more: sukun (n=12) moved 2.280 across and 2.151 down, two rounded zeros 2.079 and 3.043, two maddahs 0.214 and 1.254, against under half a unit downward for every other class — so the printed line carries no information about how high above the letter a non-vowel sits. (3) The doubled marks are placed nearly right and drawn far too small: the ten tanwin and successive marks moved a few tenths at most and were grown 0.5 to 1.4 units in both width and height, the only marks whose size changed by more than a fifth. A doubled mark is two of something and the box is being drawn for one. The third is the most actionable and the least dependent on the reader's aim, because growing a box is a statement about extent and the print can be asked about extent directly. NOT YET A FIX: ten doubled and sixteen above-the-letter marks say look here and are nowhere near enough to edit any placement, and one part of sixteen is one draw from a shuffle. What would answer it, neither half needing a reader: re-measure size against the ink for every doubled mark in the book, which is the whole of finding (3) and is free; then test whether above-the-letter marks have a different vertical relation to the line than the vowels do, over all 326,515 rather than over sixteen. Measured in docs/validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json." + }, + { + "id": "the-search-could-not-look-far-enough", + "source": { "file": "docs/design/mark-registration.md", "item": "㉔" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/lib/mark-ink.test.mjs", + "note": "Relocated out of the-fallback-rule-may-be-wrong-in-two-different-ways once the corpus refuted its per-class theory, then measured against the only thing that can settle it: 272 marks a person placed by hand. The search looks three units in each direction for the ink a rectangle should sit on and gives up when the best it finds is out at that boundary. THE READER'S ANSWERS SAY THOSE MARKS ARE A MEDIAN OF 4.292 UNITS AWAY AND 230 OF THE 272 ARE FURTHER OUT THAN THREE — the search was being asked to find something it was forbidden to reach, and where it ran out of room it was already pointing correctly: on a hundred such marks the direction it strained in agreed with the reader 92 times across and 99 down. Scored as distance from where the reader put the box: raw rectangle 4.292; the printed line as shipped 1.974 (31% within a unit, 19% within half); its own ink at three units 1.816 (40%, 28%); its own ink at eight units 0.099 (84%, 83%). NOT FIXED BY WIDENING THE SEARCH, and this is the whole shape of the fix: widening it for every mark keeps 99.82% accepted and moves 4.11% of the already-accepted ones — 2,722 marks — by more than two units, worst 13.528, because a wider window finds a better-scoring match that is the NEIGHBOURING mark's ink and does it confidently. There is no ground truth on those and there never will be, so they cannot be adjudicated, only avoided. Closed by escalating instead: search three units as before, and only where that refuses, search again at eight. Marks the ordinary search placed keep their MEASURED DISPLACEMENT byte for byte, so the risk to the 99.43% is zero by construction rather than small — checked on 14,398 accepted marks over 28 pages, 14,046 identical and all 352 others being marks the search had reported outside its own boundary, which is the-search-reported-answers-it-never-checked. Cost is near nothing because the wide search runs on the half-percent that failed. ONE THING THAT GUARANTEE DOES NOT COVER, stated rather than found later: the correction is FITTED from these measured displacements, so a mark previously pinned at the boundary was contributing a truncated number to the fit and now contributes a true one — which moves the fit, and therefore moves where every mark on that printed line is drawn. By very little, since the correction is a median over roughly thirty-six marks a line and this changes well under one in a hundred of them, but not by nothing. The bit-for-bit claim is about what the search measured; what a reader finally sees is that measurement plus a fit, and the fit got better inputs. It still refuses honestly, which is the point of keeping a refusal at all: on those pages 1,227 marks are refused by the first look, 1,124 placed by the second, and the 103 left match at a median 0.894 against the 0.909 a good match scores — marks whose ink is genuinely eight or more units from where the rectangle claims to be, not marks the rule has quietly started accepting. WHAT THE RECOMPUTE COSTS: the two rulings a person has already given are stamped with a fingerprint of the displacements they were given against, and the scorer refuses a ruling stamped with a different one — correctly, since a verdict about where a box goes means nothing if the box moved underneath it. The old displacements are kept on this laptop under their own fingerprint so those rulings stay scorable here, but the file is 73 MB and has never been committed, so anywhere else the only route back is checking out the code as it stood before the fix and re-running the sweep. The answers themselves are safe: what a person said is stored against the mark, not against the measurement, so a fresh deal inherits every answer already banked." + }, + { + "id": "the-search-reported-answers-it-never-checked", + "source": { "file": "docs/design/mark-registration.md", "item": "㉕" }, + "status": "fixed", + "severity": "defect", + "owner": "agent", + "closedBy": "packages/etl/scripts/lib/mark-ink.test.mjs", + "note": "Found while fixing the-search-could-not-look-far-enough, and older and quieter than it. The ink search runs two passes, a coarse sweep of the whole allowed region then a fine sweep around whatever the coarse one liked, and the fine sweep was not bounded by the region — so a coarse winner sitting on the boundary let the refinement step a quarter unit past it, returning an answer whose surroundings the search had never scored and which it therefore could not know was best. The damage is not the quarter unit. THE ONLY WAY ANYTHING DOWNSTREAM CAN TELL A MARK RAN OUT OF ROOM IS THAT ITS OFFSET CAME BACK SITTING EXACTLY ON THE BOUNDARY, so every mark that slipped past became invisible as a refusal: 2,252 in the shipped corpus, of which 1,923 WERE BEING ACCEPTED AND SHIPPED AS PLACED FROM THEIR OWN INK at a median match of 0.859 against the 0.909 a good match scores. Nearly three times the size of the edge population being counted, all of it on the wrong side of the count. Closed by clamping the fine sweep to the region so that an offset on the boundary means what everything downstream already reads it as meaning, and by moving the test itself next to the search that produces the rows, so the rule naming a mark a refusal and the rule rescuing one cannot drift apart." + }, + { + "id": "the-doubled-marks-are-drawn-too-small", + "source": { "file": "docs/design/mark-registration.md", "item": "㉖" }, + "status": "confirmed", + "severity": "defect", + "owner": "agent", + "note": "The one finding of the-fallback-rule-may-be-wrong-in-two-different-ways that survived the corpus, carried out of it so the refuted half and the confirmed half stop travelling together. It survived because it is not about position at all. A doubled mark is two of something and the rectangle is drawn for one, which shows up as a class refused far more often than any other AND matching worse even when accepted: refused at 2.42% against a base rate of 0.45 to 0.54%, matching at 0.889 where accepted against 0.909, and at 0.457 where refused — half of a good match, a number no rectangle in the right place can produce. THE EVIDENCE THAT IT IS A SIZE FAULT IS THAT DISTANCE WAS NEVER THE PROBLEM: these marks need 2.14 units of movement against 4.292 for the refused population as a whole, and only 17% ever reach the search boundary. The escalation in the-search-could-not-look-far-enough recovers most of the book and only 45% of these, exactly as it should — a rectangle too small to contain its mark caps the overlap below the floor wherever it is put, so a fix that only moves things cannot reach it. What would answer it, and it needs no reader: measure the extent of the ink under every doubled mark in the book against the width and height we draw, which the print can be asked about directly. A consistent shortfall means the box is being computed from one component of a two-component mark and the repair is arithmetic; a varying one means extent has to be measured per mark. Either way it is a change to what the rectangle IS rather than to where it goes, and belongs in a different part of the pipeline. ANSWERED BY A READER 2026-08-17 AND IT IS THE VARYING BRANCH, so the arithmetic repair does not exist. Ninety badly-matched marks from the recomputed corpus were put in front of somebody; 61 came back corrected, 33 doubled and 28 single — the first ground truth this item has had rather than an inference from match scores. THE POPULATION CLAIM GETS STRONGER: doubled marks are 8,554 of 326,515 in the book (2.6%) and 160 of the 329 refused for matching badly (48.6%), 19x over-represented, at rates of successive kasratan 3.15%, fathatan 2.86%, kasratan 1.84%, successive fathatan 1.55%, successive dammatan 1.11% against fatha 0.05%, kasra 0.05%, sukun 0.00%, shadda 0.00%. THE HEADLINE OF THIS ITEM IS WRONG AND IS LEFT STANDING: 'drawn too small' was a prediction from the theory's shape, and of the 33 doubled marks 14 were made wider and 19 narrower — median width change -0.13 against a median magnitude of 0.57, and height +0.23 against 0.51. The error is four times its own average, which is another way of saying there is no average. WHERE IN THE RECTANGLE THE FAULT SITS, which nobody had asked: measured edge by edge, a doubled mark's near edge moves 0.209 and its far edge 0.549, while a single mark's two side edges move 0.672 and 0.586 — a single box slides whole, a doubled box is anchored right and runs to the wrong place. Whether the near edge is the one the reading starts from is a guess, not a claim; the geometry does not know which way the line is read. WHAT EACH KIND OF FIX COULD BUY, scored as overlap with the rectangles the reader settled on, doubled then single: as shipped 0.788/0.623; given the reader's size about our centre 0.796/0.722; moved perfectly at our size 0.852/0.744; given the reader's size from our near edge 0.868/0.711. So for a doubled mark fix the size and leave the position, and for a single mark the exact reverse — a rule treating the refused population as one thing spends its effort on the wrong half either way. AND THE REFUSAL IS VINDICATED: the answer the search found and rejected is worse than what we ship in its place, 0.596 against 0.715 overlap with its centre 1.065 units out against 0.664. TWO THEORIES THIS SITTING SUGGESTED AND THE CORPUS KILLED, both named because both were reachable: the 13 hamza in the sitting all shipped at 4.00 by 3.90 while hamza ships 21 sizes across the book, which looked like a class drawn from a constant — but that size is 16,214 of the 16,385 hamza and is refused at 0.43%, the class's own rate; and one level down, that some particular size fails, which breaks out to nothing anywhere (successive kasratan ships 323 distinct sizes across 1,935 marks, and the worst-failing sizes are the ones with fewest marks under them). Both were the selected population describing its own selection. WHAT THIS CANNOT SAY: every measurement here is over refused marks, so noisy size among refused doubled marks does not establish it among accepted ones — that still needs the ink extent measured under all 8,554, which is the free half above and is still owed." + }, + { + "id": "counting-presses-stopped-meaning-anything", + "source": { "file": "docs/design/mark-registration.md", "item": "㉗" }, + "status": "confirmed", + "severity": "defect", + "owner": "agent", + "note": "Found while reading the sitting that answered the-doubled-marks-are-drawn-too-small, and it nearly changed that answer. The sitting reports 85 presses of 'the rectangle is the wrong shape' across 60 of the 61 marks answered, which read as opinions is overwhelming and is the first thing the summary shows. FIFTY-SEVEN OF THOSE SIXTY MARKS ALSO CARRY A TAP ON THE INK, and a tap puts the rectangle around what was tapped, which changes its size, which is recorded as the shape having been called wrong — so the press was the tap rather than a second judgement. Three of the sixty are a shape complaint made on its own. The 92 placements split the same way: 65 from a tap, 27 from the arrows. THIS IS NOT A REASON TO DISTRUST THE SITTING and the distinction matters: the rectangles the reader settled on are exactly as good as they were, because a tap is a statement about where the box goes made faster. What is broken is the TALLY — every count of how often a fault was named is now a count of how often a gesture was used, and the two stopped being the same thing the day the tap landed. The settling summary says '60 were reshaped, over 85 separate goes' and means far less than it sounds. What would answer it: record what the reader did and how they did it and count them apart — a shape called wrong by hand is a different row from a size that moved because a tap put the box somewhere. The transcript already carries the distinction on every placement; it is the shape events that do not, and nothing downstream separates them. Worth an item rather than a footnote because a count that overstates a fault twenty to one is the kind of number that gets quoted into a decision, and this one nearly was: the doubled-mark finding was reached from the geometry of the corrections, but the RATE at which shape was called wrong is what suggested looking there, and on its own it points at every mark in the sitting equally." + }, + { + "id": "reach-for-the-ink-rather-than-resize-toward-it-sample", + "source": { "file": "docs/design/mark-registration.md", "item": "㉘" }, + "status": "confirmed", + "severity": "risk", + "owner": "agent", + "note": "The sample-scored stage of reach-for-the-ink-rather-than-resize-toward-it, kept as its own row because it is its own document item: scored against the reader's own tapped ink and regressed over a tenth of the corpus, before the full-corpus escalation in item ㉙ turned the accepted-population safety claim from a sample into a guarantee. Read that row for the current state." + }, + { + "id": "reach-for-the-ink-rather-than-resize-toward-it-escalation", + "source": { "file": "docs/design/mark-registration.md", "item": "㉙" }, + "status": "confirmed", + "severity": "risk", + "owner": "agent", + "note": "The full-corpus escalation stage of reach-for-the-ink-rather-than-resize-toward-it, kept as its own row because it is its own document item: fired only where the shipped rule already refuses (329 of 326,515 marks), turning the accepted-population safety claim from a sample into a guarantee, before item ㉚ finished the sitting it was checked against and found the disagreement rate worse than this stage's own count showed. Read that row for the current state." + }, + { + "id": "reach-for-the-ink-rather-than-resize-toward-it", + "source": { "file": "docs/design/mark-registration.md", "item": "㉚" }, + "status": "confirmed", + "severity": "risk", + "owner": "agent", + "note": "Follows from counting-presses-stopped-meaning-anything: 57 of the 61 marks in the wrong-size sitting were placed by the reader TAPPING THE INK, so their rectangles were already close to a rule that reads the ink under the shipped window rather than resizing a guess toward it. Checked two ways. Against what the reader chose: taking every ink piece whose middle falls inside the shipped rectangle reproduces the reader's own tapped set on 96% of the 57 (94% doubled, 100% single), and scores 0.847/0.769 overlap against the reader's rectangle on doubled/single marks, both above what ships (0.788/0.623) and above every resized-guess candidate the-doubled-marks-are-drawn-too-small scored. Against marks nobody is arguing about: run over 31,805 marks on 60 pages spread through the book (about a tenth of the corpus), the rule's answer sits a median 0.077 units from what ships for the 31,773 already-accepted marks, 99.2% within half a unit. The 791 already-accepted doubled marks among them barely change size (width -0.075, height -0.088) against the several-unit corrections the refused doubled marks needed — the direct answer to what the-doubled-marks-are-drawn-too-small left owed, that the accepted doubled marks are not secretly wrong too. The 32 refused marks in the same sample moved the way predicted, median 1.47 units. Then run as a true escalation (fires only where iouBest < 0.55) over all 329 marks the current corpus refuses, not a sample: the accepted 326,186 are unchanged by construction, not by sampling. Of the 329, 304 get an answer; 25 have no ink piece under them at all (11 of 71 hamza) and must keep today's rectangle. Matched mark-for-mark against the 33 doubled marks with reader ground truth then on record, typical agreement was close (median combined size error 0.064 units) but one mark showed the rule sweeping in an unintended piece of ink and proposing growth of (4.79, 12.35) against a reader who barely touched it (0.04, -0.20); four more disagreed by 2-4 units. THE SAME SITTING WAS FINISHED ON 2026-08-19 (ninety of ninety marks, up from sixty-one) and the doubled-mark ground truth grew from 33 to 45: the worst case is still the same mark, nothing sat since disagreed worse, but the disagreement rate is not what the smaller count said — 17 of 45 doubled marks (38%) now disagree by two units or more, not 5 of 33 (15%); singles stayed solid, 2 of 44. severity stays risk, but the guard this needs is no longer optional caution against a tail case — at more than a third of doubled marks, the rule cannot ship without it. mark-H/E on the task board carries the implementation; the guard's own design is item ㉜, its own row below." + }, + { + "id": "reach-for-the-ink-rather-than-resize-toward-it-guard", + "source": { "file": "docs/design/mark-registration.md", "item": "㉜" }, + "status": "open", + "severity": "risk", + "owner": "agent", + "note": "The guard ㉙ said the rule could not ship without, designed rather than built: checked the repo first and found nothing to attach a guard to — the commits behind ㉘ and ㉙ (67f1905, 4a138e3) touch only this design doc and the task board, never a script, so the piece-union rule itself has never been committed, only scored from a run that was thrown away. The design proposes the same shape of refusal the rule already has for finding no piece at all (keep the shipped rectangle) applied to the opposite failure — a piece found, but the candidate grown far past the rectangle already shipping — measured as a ratio of areas rather than a fixed unit count, since a doubled mark's box and a single mark's box are not drawn at the same size. Names the gap in the numbers already on record (0.064 median combined units of agreement against a worst case grown several times its own shipped area) but does not fix a cutoff inside it, and says plainly what would: run a candidate ratio against all 45 doubled marks with a reader's answer and check it both catches ㉚'s seventeen disagreements and stays quiet on the marks ㉙ already called safe. mark-H/E on the task board carries whatever is built from this design. The cutoff and the re-deal are now both settled: item ㉝ found the guard has to be symmetric (a shrink, not only a growth, is most of the failure); ㉞ moved the cutoff to 1.75/0.571 on double the ground truth and found it held; and ㉟ made the re-deal call — run the checked guard now — and recorded what it decides: on the 195 never-sat candidate marks it accepts 142 (73%) and refuses 53 (27%), the refusals catching candidates grown or shrunk implausibly (worst ratios 29.6, 20.5, 18.2). What keeps this row open is the one step ㉟ deliberately did not take: wiring those 142 corrections into what the app draws is the same act as ruling option H of the open mark-placement decision, so it waits on that ruling. Read item ㉟ for the current state." + }, + { + "id": "reach-for-the-ink-rather-than-resize-toward-it-guard-shape", + "source": { "file": "docs/design/mark-registration.md", "item": "㉝" }, + "status": "confirmed", + "severity": "risk", + "owner": "agent", + "note": "The check ㉜ itself called for, run now that the piece-union script exists to run it with: scored against all 89 marks carrying a reader's settled answer, not only the 45 doubled ones ㉜ named, because ㉜'s own test needs the marks that already agree too, and those are not all doubled — the corpus has moved since ㉜ was written, the same drift ㉛ already found once, so the 89 split 40 doubled and 49 single now, not 45 doubled. Of the 89, 17 (16 of them single) have no piece to point to and are out of scope for an area-ratio cutoff by construction; the other 72 (39 doubled, 33 single) each carry a candidate and a ratio. The guard exactly as ㉜ describes it — refuse only once a candidate has grown past the shipped rectangle — catches at most 5 of the 20 marks disagreeing by two combined units or more, because 15 of those 20 disagreements are candidates that came out smaller than shipped, not larger; a guard that only watches for growth cannot see a shrink. The same measure read both ways — refuse when the candidate's area sits far from the shipped area in either direction, grown past roughly double or fallen under about half — catches 15 of the 20 disagreeing marks while wrongly refusing only 1 of the 52 that already agreed, and holds up close to evenly across doubled (10 of 13 caught, no false refusals) and single (5 of 7 caught, one false refusal) marks. Settles that area alone can separate the two populations, contrary to what ㉜'s one-sided shape assumed; does not settle the exact cutoff, which is only where this run's 89 marks happen to sit a line, or the re-deal question for the 229 unsat marks, which ㉜ also left open. mark-H/E on the task board carries whatever ships from this. A second sitting doubled the ground truth and moved the cutoff; read ㉞'s row below." + }, + { + "id": "reach-for-the-ink-rather-than-resize-toward-it-guard-cutoff", + "source": { "file": "docs/design/mark-registration.md", "item": "㉞" }, + "status": "confirmed", + "severity": "risk", + "owner": "agent", + "note": "㉝ scored 89 marks and said its cutoff was a line, not a number checked against any wider population. A second wrong-size sitting has since finished (91 more marks, weak-size-part3, from the same 329-mark population weak-size-part1 was itself drawn from), rerun through a script committed this time rather than lost — score-piece-union-guard.mjs — against both sittings together, 181 marks with no id repeated. 145 carry a candidate; 39 disagree with the reader by two combined units or more (close in proportion to ㉝'s 20 of 72), 106 agree closer than that. At ㉝'s own cutoff (ratio ≥ 2 or ≤ 0.5) the guard still catches 30 of 39 (77%, against ㉝'s 75%) while wrongly refusing 2 of 106 (1.9%, against ㉝'s 1 in 52) — the shape holds at twice the size. But sweeping the ratio from 1.5 to 4 found a number that strictly beats 2.0: at 1.75 the guard catches 34 of 39 (87%) for the identical 2 false refusals out of 106 that 2.0 costs — doubled 21 of 24 caught (1 false refusal in 46), single 13 of 15 caught (1 in 60). Looser at 1.5, four more catches (38 of 39) cost nearly quadruple the false refusals (7 of 106). The cutoff moves from 2/0.5 to 1.75/0.571 on double the ground truth ㉝ asked for. Does not touch the re-deal question either item left open — run the guard on the 216 of 229 never-sat marks, or hold for weak-size-part2's roughly 90 still-unsat marks first. mark-H/E on the task board carries whatever ships from this." + }, + { + "id": "reach-for-the-ink-rather-than-resize-toward-it-guard-redeal", + "source": { "file": "docs/design/mark-registration.md", "item": "㉟" }, + "status": "confirmed", + "severity": "risk", + "owner": "agent", + "note": "The re-deal question ㉜, ㉝ and ㉞ each left open — trust the guard's verdicts on the never-sat marks now, or hold for another wrong-size sitting first — was made: run it now, on the reasoning ㉞ had already surfaced (the 1.75/0.571 cutoff held under a doubling of ground truth, so a third sitting moves it less than the second did, while it blocks nothing else). Applied at 1.75/0.571 to every never-sat refused mark carrying a piece-union candidate: 142 of 195 accepted (73%), 53 refused (27%) — 38 of the refusals candidates that shrank under 0.571x, 15 that grew past 1.75x, the worst area ratios 29.6, 20.5 and 18.2 plainly a neighbour's ink swept in. The accept rate holds at ~73-75% whether 'never sat' counts only the wrong-size sittings drawn from this population or any sitting at all, and the corpus has moved well past ㉞'s counts — 209 refused marks now carry a reader's answer, not 145, so waiting buys even less. What ㉟ deliberately did NOT do, and what keeps the umbrella guard row open, is wire the 142 endorsed corrections into what the app draws: that act is adopting option H of the open mark-placement decision, so it waits on that ruling rather than pre-empting it. ㉟ is the evidence that ruling needs." + }, + { + "id": "the-boundary-marks-that-ship-as-placed-were-never-read", + "source": { "file": "docs/design/mark-registration.md", "item": "㉛" }, + "status": "confirmed", + "severity": "risk", + "owner": "agent", + "note": "the-search-reported-answers-it-never-checked fixed the code bug: a mark whose search ran out of room could slip past the check meant to catch that and ship as placed from its own ink. Fixing the bug never asked whether the rectangles it now correctly counts as boundary-hits are actually right — they still ship as placed whenever they clear the match floor. That population, on the corpus as it stands today, is 339 marks (search ran out of room, iouBest 0.55 to 1.01). Two sittings of forty, drawn the same way as the-doubled-marks-are-drawn-too-small and reach-for-the-ink-rather-than-resize-toward-it, 80 of 339 seen so far: every one of the eighty carries a complaint, and unlike counting-presses-stopped-meaning-anything's tally the movement is mostly real (79 of 80 moved by 0.05 units or more, 73 of 80 genuinely resized). But it is a small kind of wrong — median hand distance 0.676 against the fallback population's multi-unit corrections, median size change 0.07 to 0.09 units — not the fallback population's kind. 259 of 339 remain unsat; whether the rate and size hold across the rest is open." + }, + { + "id": "the-weakest-matches-may-go-wrong-by-class", + "source": { "file": "docs/design/mark-registration.md", "item": "㉒" }, + "status": "open", + "severity": "question", + "owner": "agent", + "note": "From two sittings of thirty on 2026-08-17, drawn from the bottom of the marks the search accepts — overlap 0.55 to 0.65, a band starting exactly at the floor below which it gives up, and 0.65 to 0.75 immediately above. Every hamza in both was moved sideways in the same direction, ELEVEN OF ELEVEN, with width left exactly as drawn: median 1.077 page units in the weaker band and 0.573 in the better one, against 0.002 for every other mark. Fatha and kasra came back too short in both and by less in the better one (0.600 and 0.500 against 0.065 and 0.215, on boxes about 3.9 tall, width untouched). The whole sitting moves the same way — median hand distance 0.363 falling to 0.276, worst 5.877 falling to 2.053 — so THE OVERLAP SCORE IS DOING ITS JOB: it does not predict whether a reader complains, because pointing at ink files a complaint either way (see pointing-at-ink-files-both-words-every-time), but it predicts how far out we were. The halving across the two bands is the strongest thing here, because it ties the error to how sure the search was, which is what an argument about the search predicts and a fixed per-class offset does not. This does not contradict §④'s refutation of a per-class bias and the two are about different marks: that one is the residue after correction across the whole book measured by ink, this one is sixty marks the ink search itself flagged as doubtful measured by a person, and a class error living only in the doubtful tail is invisible to an average over 326,515. Actionable because of its shape — an even residue across every class is the price of correcting by a median and there is nothing to do about it, whereas a whole class in one direction is either a bug in what the search matches against or a case for a per-class term. What would answer it, in order and neither needing a reader: re-measure sideways displacement for hamza alone across the whole book, to separate a property of the tail from a property of hamza; then, if it is the tail, test whether the search is settling on the letter a hamza sits on rather than on the hamza, which §④ names as the mechanism and does not test. MUST NOT become a per-class correction fitted to sixty marks." }, { "id": "the-displacement-correction-could-go-stale", @@ -910,6 +1216,231 @@ "severity": "question", "owner": "agent", "note": "Answered 2026-08-08 over all 326,515 marks, with no page, no fitted transform and no rectangle in the arithmetic: no mark in the corpus is drawn with the strokes of a different name. The category is empty. Five drawings still match another name overall and all five are shown to be the right strokes at an unusual spacing — that residue is carried as spacing-is-scored-as-shape. A further 21.30% match a name this print draws as the same shape, which is a limit of the method and never counted as a disagreement — carried as position-might-separate-the-look-alike-pairs. Named in the placement document so its silence on the question is not read as a claim." + }, + { + "id": "the-pagination-question-covers-three-outputs", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "①" }, + "status": "open", + "severity": "question", + "owner": "user", + "blockedBy": ["a licensing opinion"], + "note": "adj-shards-carry-a-third-upstream asks whether a table of which page an ayah falls on carries its source's terms forward, and scopes the question to one asset tree. The dependency audit of 2026-08-16 measured the same table reaching two more shipped outputs: every one of 44,431 root-shard occurrence tuples carries a page number matching the vendored table across all 604 pages, and assets/manifest.json ships the table verbatim at 6,236 entries, byte-identical to data/pages/ayah-pages.json. So the opinion being awaited is the same opinion and only its scope is wrong — which matters because scope is what a reader of the request will answer against, and an opinion returned about one tree would have to be re-obtained for the other two. Carried separately from the original row rather than folded into it because that row is cited from track-b-native.md and from the notices gate's deferral, and widening a row other things point at changes what those pointers mean. What would answer it: the licensing opinion already requested, asked about all three outputs." + }, + { + "id": "notices-gate-states-a-false-fact-about-roots", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "②" }, + "status": "answered", + "severity": "defect", + "owner": "agent", + "note": "scripts/gate-notices.mjs declares data/pages/ayah-pages.json as 'ours' for the roots bucket, on the stated ground that every root-ayah pair is filed by ayah and 'No page number reaches a root shard'. Measured on the shipped tree: build-roots.mjs writes [abs, pageOf(abs), n, ...ids] and 44,431 of 44,431 occurrence tuples carry a page number, every one matching the vendored table, spanning all 604 pages. build-roots.mjs's own header says the structure 'doubles as the page table'. Written by me on 2026-08-16, one day before the audit that measured it, which is the useful part of the finding: the gate was built to catch an upstream nobody had noticed and its first hand-written declaration asserted the absence of one. A gate that states a falsehood is worse than no gate, because a reader takes it as having checked. Fix is small and needs no opinion: change the verdict to defer to the-pagination-question-covers-three-outputs the way the adjacency bucket already defers, so the exemption expires when the question closes rather than outliving it." + }, + { + "id": "structural-metadata-reaches-the-hop-unnamed", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "③" }, + "status": "open", + "severity": "question", + "owner": "agent", + "note": "HALF CLOSED 2026-09-01: named. gate-notices.mjs declares tanzil-quran-metadata and the name Tanzil for the adj bucket by hand, so the LICENSES.md row and the shipped NOTICE.txt must both carry it; build-adjacency.mjs writes a Tanzil paragraph into the notice. Only NOTICE.txt changed on rebuild. What stays open is the design half, whether the trace should follow imports into @hifth/core, so this is now a question rather than a defect. ORIGINAL: build-adjacency.mjs computes sameJuz from juzOf in @hifth/core, whose tables derive from the CC BY structural metadata, and writes it into dir.sameJuz on 510 of 3,002 shipped edges (measured across all 114 shards). Neither the LICENSES.md row for that tree nor the NOTICE.txt shipped beside it names that upstream. The exposure itself is small — CC BY is attribution rather than copyleft, and the app already credits the source in its colophon — but the mechanism is not: gate:notices cannot see this, because it does not follow imports into @hifth/core and says so in its own header. That blind spot was declared as a known limit when the gate was written; this is the first demonstration that something real is inside it, which turns a stated limit into a measured one. Two separable pieces of work: name it in the row and the notice, and decide whether the trace should follow the core package at all — that graph is large and mostly uninteresting, so following it is a design question rather than an obvious yes." + }, + { + "id": "the-asset-manifest-is-in-no-licence-bucket", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "④" }, + "status": "fixed", + "closedBy": "scripts/gate-notices.mjs", + "severity": "defect", + "owner": "agent", + "note": "FIXED 2026-08-16. Both uncovered trees have rows in LICENSES.md — the word geometry under our own licence with the Sadaqa-Jaria grant behind it named, the manifest under ours with the page table's one open question stated — and the structural half is closed the way this row asked for: gate-notices.mjs now readdirs apps/web/public/assets and fails on any entry that is neither an inherited bucket nor a named declaration, and requires LICENSES.md to mention each named one. Proven to fail on both halves before it was left green (an empty directory under assets/, and the words row removed from LICENSES.md). The manifest's remaining licensing question is not this row's: it is the-pagination-question-covers-three-outputs, which the gate now defers to by name and which stays open until a licensing opinion answers it. ORIGINAL FINDING: assets/manifest.json ships ayahPages: the complete 6,236-entry page table, verified byte-identical to packages/etl/data/pages/ayah-pages.json. LICENSES.md's bucket table names four asset directories — pages, roots, skins, adj — and the manifest sits at the root of assets/ beside them, matching no row. So the file is shipped under no stated terms at all, and the gap is structural rather than an oversight about this one file: the table is organised by directory and nothing checks that every shipped path falls in some row. Whichever way the-pagination-question-covers-three-outputs resolves, this file needs a row; if the answer is that the table carries terms forward, it needs one urgently, because a manifest is the first thing a reader fetches. WIDENED 2026-08-16: six things ship under apps/web/public/assets and LICENSES.md has rows for four. The second uncovered one is assets/words/hafs-kfqc/** — 604 files, 2.8 MB, shipping since loop 7 — which appears in neither LICENSES.md nor SOURCES.md nor gate-notices.mjs, verified by grep for the string 'words' in all three. Its terms are the least troublesome in the repo, since build-words.mjs emits 91,451 bounding rectangles measured on our own viewBox and nothing textual, which is exactly why nobody wrote it down. The structural half is confirmed too: gate-notices.mjs line 187 cross-checks BUCKETS against the rows of LICENSES.md in both directions and never calls readdirSync on the assets directory, so a tree absent from both hand-written lists is invisible to it by construction. Fix is a row for each plus a readdir-driven coverage check, on the same shape as the one that catches an undeclared bucket. Priority note on the manifest specifically: apps/web/vite.config.ts puts it in globPatterns while globIgnores excludes assets/pages/**, so the derived table is install-time precached onto every visitor's device while the artwork it was derived from is only runtime-cached — the derived thing travels further than its parent." + }, + { + "id": "nothing-checks-the-package-tree-licences", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑤" }, + "status": "open", + "severity": "risk", + "owner": "agent", + "note": "gate:license, gate:license-copy and gate:notices were each read in full during the 2026-08-16 audit; none of them looks at node_modules. Every licence check in this repo is about vendored data. The tree was audited by hand and is clean — 565 package entries across 12 buckets, zero copyleft, zero source-available, zero unstated, zero missing — which is the argument for adding the check now rather than after something lands. One design constraint the audit surfaced and a naive gate would miss: it must run over what actually SHIPS, not over production dependencies, because workbox-window is declared as a development dependency and ships anyway through the offline-support registration in apps/web/src/pwa.ts, as does idb, which nothing in this repo declares at all. Per the convention gate:notices set, it owes a demonstration that it can fail." + }, + { + "id": "structural-metadata-could-be-public-domain", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑥" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "The structural tables this app uses — ayah counts, part and half-part starts — come from a source licensed CC BY 3.0, whose §4(a) forbids applying technological measures that restrict a recipient's exercise of the granted rights. That is the same clause family as CC BY 4.0 §2(a)(5)(B), which track-b-native.md already names as the tajweed tree's store blocker, so this source carries an exposure the repo had not counted. A metadata file under a public-domain dedication exists and is a strict superset of what is used here: 604 pages, 556 ruku, 240 quarters — which divide into the 60 half-parts the current source supplies — 30 parts, 7 manzil, 15 prostrations with their flags. No attribution, no share-alike, no technological-measures clause. This is a decision rather than a task: the numbers agree, so the choice is about trusting a compiler's dedication over a named project's stated licence, and it belongs in docs/decisions.json with its options drawn rather than being settled in a note. ADDED 2026-08-16: the research found the current source's own grant stated three ways on three surfaces that do not match — tanzil.net/docs/text_license names CC BY 3.0, tanzil.net/download/ shows the same permission text with no CC reference at all, and the notice embedded in the 2011 corpus file vendored here records CC BY-ND 3.0. The vendored metadata XML's root element says only license=\"cc-by\" with no version, which SOURCES.md:254 quotes honestly without inventing one. Not our defect, but it strengthens the case for this move: an unversioned grant restated inconsistently by its own steward is exactly the kind of thing a public-domain dedication removes, and it also means that pinning the version — which the derivation audit asked for — may not be possible from the steward's own pages." + }, + { + "id": "the-hop-could-be-computed-from-the-print", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑦" }, + "status": "open", + "severity": "question", + "owner": "agent", + "note": "The morphology corpus does two unrelated jobs here: root and lemma annotation, and separately a word-segmented text. Everything assets/adj/** owes it comes from the second job only — sharedRuns is computed over reconstructed word skeletons and never reads a root tag. A permissively-licensed substitute for that second job is already vendored and already read at build time: the ligature corpus carries per-word text under a grant permitting use, modification and distribution for any lawful purpose including commercial, attribution not required, and lib/tajweed-fold.mjs already reconstructs ayah text from it at 99.86% against an eighteen-rule oracle. Recomputing the runs over the print's own words would take assets/adj/** out of copyleft entirely and delete the corpus-to-print index conversion, because the runs would already be in the only index a word box carries. What blocks it is a measurement nobody has run: the print splits 9,533 proclitics the corpus joins, so run lengths change and the uniqueness rule keeps a different number of spans than the current 2,544. Until that number exists this option has no cost attached and cannot honestly be drawn on an options page — which makes running it the cheapest unblocking move in the audit, and it is doable from here." + }, + { + "id": "the-tajweed-spans-could-be-generated-not-vendored", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑧" }, + "status": "open", + "severity": "question", + "owner": "user", + "blockedBy": ["a hafiz's judgement on the divergences"], + "note": "An MIT-licensed engine ships a corpus-free core of about 34 KB that would mean vendoring no annotation data and no Quran text at all, emits the same codepoint-range shape the current source does, and models pause — which a precomputed span dataset structurally cannot, and which matters for an app whose reader stops mid-page. It agrees with the incumbent at 99.80% across all 6,236 ayahs with all 108 divergences catalogued. Two things weigh against: it was created five weeks before this audit, is at version 0.1.1 with two stars and one author, and depending on that is a real risk — largely dissolved by the fact that MIT permits vendoring the core outright. And the incumbent is worse than SOURCES.md records: its grant is one README sentence covering 'this data file', singular, while the classifier that generated it carries no licence at all, so regenerating the annotations ourselves from that project is not an available escape. Blocked on the same hafiz judgement the colouring already waits on, since the 108 divergences are exactly what a reader qualified to judge tajweed would have to rule on." + }, + { + "id": "the-page-table-has-only-one-witness", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑨" }, + "status": "open", + "severity": "risk", + "owner": "agent", + "note": "data/pages/ayah-pages.json reaches three shipped outputs — 3,002 adjacency edges, 44,431 root-shard occurrence tuples, and assets/manifest.json verbatim — and nothing checks it against anything. It is derived from one upstream and corroborated by none, which is precisely the shape gate:quran-meta exists to refuse for the hand-typed core constants: every number stays in range, the list stays plausible, and the only symptom of an error is an ayah quietly filed on the wrong page. The audit of 2026-08-16 found a metadata file under a public-domain dedication (the Unlicense — no attribution, no share-alike, no technological-measures clause) that carries a page number per ayah, compiled independently. Comparing them across all 6,236 costs a build step and buys two different things: a correctness check on an unchecked table, and evidence bearing on adj-shards-carry-a-third-upstream and the-pagination-question-covers-three-outputs, because two independent compilations agreeing across 6,236 entries is what a fact about a printing looks like rather than an authored arrangement. Disagreement would be worth more and nothing today would ever surface it. Nothing constrains this source, so this is the least encumbered item in the audit." + }, + { + "id": "the-segmentation-split-has-no-third-opinion", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑩" }, + "status": "open", + "severity": "question", + "owner": "agent", + "note": "the-hop-could-be-computed-from-the-print turns on 9,533 positions where the print splits a word the morphology joins. Today exactly two corpora have an opinion on those positions and they are the two being compared, so a disagreement establishes only that they differ and never which one is unusual — which is why that item is currently an argument rather than a measurement. The audit surfaced two independently annotated corpora carrying word segmentation and not derived from the morphology; both were set aside as replacement candidates for having no roots, which is irrelevant to this use. Segment agreement between each of them and the two incumbents, over the disputed positions only, converts the blocked item into a decidable one. Two constraints: both are share-alike, so they may be read at build time but must not be vendored here or shipped — reading a corpus to check a number is a different act from distributing it, and this repo ships numbers, not text. And provenance has to be established before agreement means anything: the audit found a project claiming independence whose annotations were partly scraped from the very corpus it would be checked against, so a second witness derived from the first is not a second witness." + }, + { + "id": "nothing-measures-what-the-hop-misses", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑪" }, + "status": "open", + "severity": "question", + "owner": "agent", + "note": "The pairings corpus is deliberately not exhaustive — data/mutashabihat/PROVENANCE.md records the curation stance, and PLAN names exhaustive phrase matching as a non-goal — so its selectivity is a feature. But no number exists anywhere for what it leaves out, which means 3,002 edges on 1,521 ayahs cannot be read as sufficient or as thin, and the-hop is the reason this app exists. A larger curated collection surfaced by the audit renders around 5,641 phrases (its own index says 5,277 and the two disagree, which is itself a reason to treat it as a ruler rather than a source) under an unstated data licence: unusable as a dependency, usable as a measuring stick. A recall figure computed at build time, shipping nothing, would say more about whether the hop is finished than any other measurement available here — including how much of the gap is genuine omission rather than the deliberate exclusions the curation made on purpose, which is the distinction that decides whether anything needs doing about it." + }, + { + "id": "the-licence-map-describes-one-door", + "source": { "file": "docs/design/what-we-distribute.md", "item": "①" }, + "status": "answered", + "severity": "defect", + "owner": "agent", + "note": "ANSWERED 2026-09-01: LICENSES.md is three tables under three doors, with what-we-distribute.md numbers in the rows. Not fixed: gate-notices still parses the rows but nothing checks the by-door organisation. ORIGINAL: LICENSES.md's bucket table is organised by path, which silently means the deployed-site channel — the derived asset trees under apps/web/public/assets. It describes neither the repository channel, which carries all 17 vendored input files verbatim at 12.3 MB and is where the copyleft obligation is actually discharged, nor a store channel. So a reader asking whether something may be distributed gets an answer without being told which distribution it answers for, and the two live channels carry almost disjoint content: the 6.3 MB morphology corpus and the 5.6 MB tajweed annotations are in the repository and reach no browser, while ~100 MB of derived assets reach every browser and are in no sense the inputs. The confusion is not hypothetical — the-asset-manifest-is-in-no-licence-bucket is a file shipped through the site channel and covered by no row, and 'the corpus never ships' is a true statement about one channel that says nothing about what the shards are made of. Fix needs no opinion: organise the map by channel and then by path, so each row states what goes out, through which door, under whose terms. Everything needed is already measured." + }, + { + "id": "inputs-are-not-sorted-into-ingredients-and-instruments", + "source": { "file": "docs/design/what-we-distribute.md", "item": "②" }, + "status": "open", + "severity": "question", + "owner": "agent", + "note": "Separating what is built from what is distributed helps when the encumbered thing is an INSTRUMENT — read only to check an output, never to produce it, so the output is not derived from it and it goes through no channel at all — and does not help when it is an INGREDIENT, because a shard computed from a corpus is computed from it whether the computation happened on a build machine or in another repository. That single distinction is the whole of the technique and this project has never sorted its inputs by it. Some are unambiguous: the pairings and the page artwork are ingredients, and the comparison corpora tracked as the-page-table-has-only-one-witness and the-segmentation-split-has-no-third-opinion are instruments. One is genuinely undecided — the morphology corpus is an ingredient for the root lens, and for the adjacency tree may become replaceable by an instrument if the-hop-could-be-computed-from-the-print holds. The classification is cheap and would say immediately which separation patterns are even available, which is why it should come before any of them are attempted. SHARPENED 2026-08-16 by the separation-techniques research, which supplies two tests better than 'did the input contribute', a question that answers yes for everything. FIRST, recoverability: can a recipient recover the input's protected expression from what we shipped? ODbL 1.0 splits a Derivative Database from a Produced Work on exactly this, the OSMF guideline's operative phrase being 'not intended for the extraction of the original data', and CDLA-Sharing-1.0 §3.5 carves out Results on a de minimis floor. Six bodies that never coordinated converge on containment rather than causation — USCO, Getty v Stability [2025] EWHC 2863 (Ch) at [600], LG München I in GEMA v OpenAI, Creative Commons, OSMF, CDLA — and they disagree on outcomes, not on the test. We already have the measurements: the skins tree is fully recoverable at 60,057 of 60,057, adj is fully recoverable at 2,516 of 2,516, roots carries the whole incidence relation at 44,431, and words carries nothing of its source. SECOND, the Micro Star escape hatch: Micro Star v FormGen, 154 F.3d 1107 (9th Cir. 1998) held level files containing no art and no code infringing because they described the copyrighted display in exact detail ('what does sheet music do but describe in precise detail the way a copyrighted melody sounds'), and footnote 5 makes the test whether the description is meaningful against a work other than the one it came from. Sorts our trees: root ids keyed to a standard morphological scheme mean something against any Arabic text and are footnote-5 safe; page numbers and bounding boxes derived from one specific printed mus'haf are useful only against that mus'haf and are footnote-5 exposed. Second independent reason the-pagination-question-covers-three-outputs is the blocker. Also note the numbers trap: CCC v Maclean Hunter, 44 F.3d 61 (2d Cir. 1994) and CDN v Kapes, 197 F.3d 1256 (9th Cir. 1999) held numbers protectable because they were estimates rather than observations, so the question is never whether something is a fact but whether a person chose among more than a few defensible alternatives — a morphological parse and a tajweed span are judgements, where ink physically sits on a page is an observation." + }, + { + "id": "whether-a-store-build-carries-every-tree", + "source": { "file": "docs/design/what-we-distribute.md", "item": "③" }, + "status": "open", + "severity": "question", + "owner": "user", + "blockedBy": ["inputs-are-not-sorted-into-ingredients-and-instruments"], + "note": "The store channel is the only one carrying a blocker, and it does not have to carry what the site channel carries. Both blocking clauses — the copyleft prohibition on imposing further restrictions, and the share-alike prohibition on applying technological measures — are triggered by what the channel does to a recipient rather than by anything the data is, which is why the identical bytes pass through the site channel today without difficulty. So the question is not how to make the data acceptable but which trees a wrapped build needs to contain. The audit has the feature-loss numbers and they are very uneven: omitting the root shards costs a whole lens and leaves the hop intact, omitting the tajweed skin costs colouring and nothing else, omitting the pairings ends the app, and omitting the page artwork means there is no app at all. Deliberately not opened as a decision yet: inputs-are-not-sorted-into-ingredients-and-instruments may remove the blocker from the adjacency tree entirely, which would change what the options are, and drawing options against a set that is about to move is how an options page goes stale before it is read." + }, + { + "id": "twelve-verses-of-scripture-ship-in-the-bundle", + "source": { "file": "docs/design/what-we-distribute.md", "item": "④" }, + "status": "fixed", + "closedBy": "scripts/gate-scripture.mjs", + "severity": "defect", + "owner": "agent", + "note": "FIXED 2026-08-16. verse-text.ts and its test are deleted, and the panel they served is rebuilt from data that was already shipping: every look-alike edge carries the matching word run on both sides in the print's own numbering, so the panel now crops both ayahs out of the page artwork and washes the words outside the shared run. That covers 2,544 pairs where the typed table covered 12, and it fixes a correctness defect the table had — the transcription was in a plainer spelling than the mus'haf on screen, so a reader comparing the two saw different letters in each. The rule now has a check: gate:scripture fails on any run of three or more consecutive fully-vowelled Arabic words anywhere in the tree, a threshold placed in a gap the tree provides (11 and 5 words on the two offenders, 1 or 2 everywhere else, nothing at 3 or 4), with a named allowlist for single-specimen files that the phrase rule can override. Run before the fix it named exactly the two offending files and nothing else. Prose across 9 files that stated the rule unscoped is narrowed to what is true and provable — nothing vendored and nothing shipped is Quran text — and each site now names the gate. loop-5.md was re-read as this row asked: its sentence is 'our shards emit no Quran text', which is scoped and was true throughout, so the CC BY-ND argument standing on it is undamaged. The second scripture site this row did not know about is tracked as a-pipeline-test-held-a-four-word-phrase. ORIGINAL FINDING: packages/core/src/verse-text.ts holds VERSE_TEXT: 12 fully vocalized ayahs (2:40, 2:45, 2:47, 2:48, 2:58, 2:60, 2:122, 2:123, 2:153, 7:160, 7:161, 82:19) as hand-typed TypeScript literals, 48 token strings and 1,414 Arabic characters, every one carrying harakat. Measured against apps/web/dist/assets/index-CYQteDdt.js built 2026-08-16 15:20: 48 of 48 token strings are byte-present, so it is not dead code and does not tree-shake away — diffPair reads VERSE_TEXT, is exported from packages/core/src/index.ts:107, and DiffView is mounted at HighlightMenu.tsx:213 and HopPopover.tsx:166. The standing rule it contradicts — 'no Quran text in this repo' — appears 22 times across 20 files including apps/web/public/assets/roots/hafs-kfqc/NOTICE.txt, the adj NOTICE.txt, SOURCES.md, build-roots.mjs, build-tajweed.mjs, build-adjacency.mjs and morphology.mjs, and it is the load-bearing sentence of the CC BY-ND argument in docs/decisions/loop-5.md. Nothing enforces it: gate:notext is about elements in page SVGs under content-visibility, a Safari rendering requirement, not about source. Provenance is undetermined and the orthography rules out every vendored source — the strings are Imlaei/simple where all six corpora here are Uthmani — and the file header says only 'Loop-3 demo data, the mock's hand-verified tokens' awaiting an ETL replacement that never landed. Defect under every reading of every licence involved, and the cheapest thing in either audit to fix: the feature is a leftover demonstration serving two popovers, so removal costs almost nothing. Whichever way it is fixed, loop-5.md must be re-read afterwards and the rule needs an actual check, since a claim asserted 22 times and verified nowhere is how this survived four loops. PRIORITY RAISED 2026-08-16 by the separation-techniques research: the Crossway/ESV DMCA campaign of July-August 2025 issued 11 notices and roughly half were path-scoped into code repositories — /tree/main/data/esv, /tree/master/data, and in one case a test-fixtures directory. Blast radius tracks the directory holding the text, not the project's purpose or the text's licence status, and scripture in apps/web/src is exactly the shape those notices reached. This is the only row in either design doc that needs no licensing opinion to act on, so it should be fixed before anything that does." + }, + { + "id": "a-pipeline-test-held-a-four-word-phrase", + "source": { "file": "docs/design/what-we-distribute.md", "item": "④" }, + "status": "fixed", + "closedBy": "scripts/gate-scripture.mjs", + "severity": "defect", + "owner": "agent", + "note": "FIXED 2026-08-16, and found only because the gate written for twelve-verses-of-scripture-ship-in-the-bundle was run over the whole tree before the fix rather than at the one file that motivated it. packages/etl/scripts/lib/tajweed-fold.test.mjs held a four-word opening phrase as a fixture, plus a three-word one inline: five consecutive fully-vowelled words at its longest, 29 vowelled words in the file, second only to verse-text.ts and ahead of everything else by a factor of two. It does not ship — it is a pipeline test — and that is exactly why it is worth a row of its own rather than a line in the other one: the takedown research this work was prioritised by measured that roughly half of the Crossway/ESV notices of July-August 2025 were path-scoped into code repositories, one of them at a test-fixtures directory, so blast radius tracks the directory holding the text and not whether a build step ever reads it. Fixed by replacing the phrase fixtures with bare letters, which is all the fold arithmetic ever reads — the function never looks at a word's content when building the prefix — and keeping real orthography only where a codepoint is itself the subject of the assertion. All 54 tests still pass and nothing the file asserts was weakened." + }, + { + "id": "the-transliteration-table-is-copied-into-our-code", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑫" }, + "status": "fixed", + "closedBy": "packages/etl/scripts/build-roots.mjs", + "severity": "defect", + "owner": "agent", + "note": "FIXED 2026-08-16. The comment no longer cites the corpus's page as the thing copied, and no longer says verbatim. It now says what is actually true: the scheme is Tim Buckwalter's, published with the LDC's Arabic morphological analyser, and the rows past the letters are its XML-safe variants plus the extra codepoints the corpus needs for a mus'haf's pause, madd and small-letter marks. What is in the file is a character correspondence — one alphabet against another — which has to match the corpus's exactly or the roots come out wrong, and that is a constraint rather than a copy. This closes the route around the derived-not-copied argument that the row was about; whether a permissive relicence is available at all is still its own open question and is not answered here. ORIGINAL FINDING: packages/etl/scripts/build-roots.mjs:86-103 declares BUCKWALTER, 51 entries, under the comment 'Buckwalter → Arabic (corpus.quran.com/java/buckwalter.jsp, verbatim)'. The word verbatim is the repo's own. Every other claim LICENSES.md makes about the GPL corpus runs through a build step and rests on the shards being derived rather than copied; this route goes around that argument entirely, because nothing derived it. Weighs both ways and the row records both: Buckwalter transliteration was published by Tim Buckwalter years before the QAC adopted it, the QAC published a table for a scheme it did not author, and a bijection between two alphabets constrained to be ASCII-typeable and reversible has very few expressive degrees of freedom. Against that, the comment as written concedes copying from a GPL source, and this becomes load-bearing the moment whether-a-permissive-relicence-is-available-at-all is asked, since a permissively-licensed file containing a verbatim table from a copyleft one is the case that needs an answer. Resolved by comparing against Buckwalter's own published table (Xerox/LDC) and, if that is the real source, saying so in the comment. Related but separately arguable and not opened as its own row: apps/web/src/format.ts:59-80 SURAH_NAMES_EN, 114 romanised names argued at lines 45-58 to be proper nouns rather than vendored data, where the argument is strong for the Arabic names and weaker for a consistently-applied romanisation convention, and the repo does not record whether the list was typed from a source or produced by applying a rule. Contrast packages/core/src/quran-meta.ts, whose 318 Tanzil-derived numbers are re-derived from the vendored XML by gate:quran-meta on every CI run — that is what the other three routes should look like, and it is applied to exactly one of the four." + }, + { + "id": "the-artwork-pipeline-declares-two-transforms-and-applies-three", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑬" }, + "status": "fixed", + "closedBy": "SOURCES.md", + "severity": "defect", + "owner": "agent", + "note": "FIXED 2026-08-16. Both sentences now say three and name the third. The source entry lists the transforms one by one — the minifier at its pinned config, the two identifier repairs, and the twenty-three polygon repairs across nineteen pages — and says outright that it claimed two for as long as there were three, with the pipeline's own header as the authority. The licence file no longer says the artwork is reproduced unmodified; it says what is applied and keeps the promise that was actually being made, which is that nothing is hand-edited. That is the promise the corpus's changing-is-not-allowed clause rests on, and it survives the correction intact. ORIGINAL FINDING: SOURCES.md:86 says the page pipeline 'applies exactly two declared transforms'; vendor-pages.mjs:25-26 says 'Three transforms are applied, all declared, all reproducible, all asserted', and all three are real and counted in the code: svgo at a pinned version and config with floatPrecision 1, ID_REPAIRS with 2 entries, and POLYGON_REPAIRS with 23 entries across 19 distinct pages. Separately LICENSES.md:131 says the artwork 'is reproduced here unmodified', which at floatPrecision 1 it is not, since svgo rewrites every path's coordinate data. Prose-only defect — the repairs are already declared, counted and asserted, and vendor-pages.mjs dies if the applied count does not match the declared one — and it matters more than a typo for one reason: the whole answer to the QAC's 'changing it is not allowed' clause, at LICENSES.md:50-53, is that this repo vendors upstream files verbatim and never edits them. That answer depends on 'vendored verbatim' being a practice a reader can trust across the repo, and a second tree that says verbatim and is not undermines the practice rather than the tree. KFGQPC's terms are not the QAC's terms and nothing here breaches them. Fix is to correct both sentences to say three and name the third." + }, + { + "id": "whether-a-permissive-relicence-is-available-at-all", + "source": { "file": "docs/design/what-we-depend-on.md", "item": "⑭" }, + "status": "open", + "severity": "question", + "owner": "user", + "blockedBy": ["twelve-verses-of-scripture-ship-in-the-bundle", "the-transliteration-table-is-copied-into-our-code", "a licensing opinion"], + "note": "Worth separating from the store question because someone may want it answered for unrelated reasons — a reader wanting to build a different mus'haf on @hifth/core, where GPL is the thing stopping them. LICENSES.md:55-57 answers it already: 'The app code is not a derivative of the corpus data — it reads the shards at runtime — so nothing here reaches our source by way of the data.' The premise was verified and holds: every asset access in apps/web/src/assets.ts is a fetch by URL, and neither apps/web/src/** nor packages/core/src/** contains a readFileSync, import or fetch targeting packages/etl/data/. The conclusion does not follow, because the content did not arrive over the boundary being guarded — somebody typed it in, by four routes ranked worst first in the design doc: the twelve verses, the Buckwalter table, the 114 romanised names, and the 318 Tanzil numbers, the last being the only one with an automated re-derivation behind it. Also not an escape from the data question: permissive code beside GPL data in one deploy is exactly the unresolved aggregation reading, where GPL-3.0 §5's aggregate paragraph protects the other parts from the covered work rather than the reverse, and the honest answer is that a static deploy of separately-fetched JSON is the paradigm aggregate under one reading and not separate-and-independent under another. Two things worth knowing before this is decided: relicensing the ETL scripts more permissively does not break the §6 corresponding-source obligation, it over-satisfies it; and file-level copyleft is the usually-skipped middle option, letting a third party embed the engine in something closed while changes to our own files still come back, which fits if the motive is 'let people use this' rather than 'let people take this'." + }, + { + "id": "how-durable-must-a-confusion-map-be", + "source": { "file": "docs/design/confusion-points.md", "item": "①" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "The confusion-points design (Tier C1) proposes a personal log of where a hafiz's own memory slips between ayahs. Following the revision-record pattern gives IndexedDB plus a `since` stamp — enough to stop an emptied log lying about itself, not enough to stop it emptying, and iOS silently wipes script storage after 7 idle days. For a glance log that is an accepted cost; a confusion map is the distilled product of months of revision, so losing it silently harms the exact reliance the feature exists to earn. What would answer it: a human's call on whether `since` + a Home-Screen install is sufficient defence, or whether this is the feature that finally justifies an export/backup path (see should-a-confusion-map-leave-the-device). The design's own sharpest open question, worth settling before code. OWNER DIRECTION 2026-08-29: reviewing the Where-You-Slip walkthrough the owner asked for durable web storage that survives a cache flush, WITH a downloaded-file backup you can re-upload, and iCloud once there is a mobile app — a clear lean toward 'yes, this justifies a backup path'. The file-backup half is now being opened as a full decision (should-a-confusion-map-leave-the-device); the iCloud/sync half rides on the mobile track, gated on gpl-and-the-app-store." + }, + { + "id": "should-a-confusion-map-leave-the-device", + "source": { "file": "docs/design/confusion-points.md", "item": "②" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "Revision-record's settled stance is that anything leaving the device is a deliberate, gated act — no export, no sync. Two real pulls argue the other way for a confusion map: a hafiz wants to back up something this precious (see how-durable-must-a-confusion-map-be), and a teacher wants to see a student's map to drill them on it, a genuinely valuable use the app's navigation nature does not otherwise serve. Against both: a record of where a person's memory of the Qur'an fails is intimate, and export is the first step of every privacy erosion. What would answer it: a decision, and because it is a genuine either/or with a privacy cost, the design recommends it become a full decisions.json record with a published options page drawn on a real mus'haf — which the decisions gate requires before an open decision row is allowed, so opening it is real work, not a line. OWNER DIRECTION 2026-08-29: the owner leans toward letting the map leave the device and named two distinct mechanisms this question must keep apart. (1) A user-controlled file backup — export to a downloaded file, re-upload to restore; nothing leaves unless the reader saves the file, and only where they put it. The lighter-cost mechanism and the leading option. (2) Automatic cloud/iCloud sync — the map copies itself to a third-party cloud continuously; materially heavier privacy cost, not reachable from a web app at all, needs the native mobile wrap (roadmap Track B, gated on gpl-and-the-app-store), so not a near-term option. Teacher-sharing is a third mechanism and stays in scope. NOW BEING OPENED as a full decision (confusion-map-export) covering the file-backup option first, with sync and sharing named as the heavier options it deliberately does not take yet." + }, + { + "id": "how-are-slip-candidates-ranked", + "source": { "file": "docs/design/confusion-points.md", "item": "③" }, + "status": "open", + "severity": "risk", + "owner": "user", + "note": "The smart-suggestion half of the feature reuses the app's existing resemblance edges (twin / shared-run) to offer the verses a hafiz likely slipped toward. Phase 1 can ship those ranked twin-first with the surah/ayah jumper as fallback for un-covered ayahs. Whether that is good enough, or whether a new opening-word / rasm similarity signal is needed to make the common case truly one-tap, is a judgment about how often real slips land on ayahs the hand-curated corpus omits (it covers ~a quarter of the book). What would answer it: a hafiz trying capture on their own real slips and reporting how often the right target was absent — a validation-ledger candidate, not something to settle from the renderer. The ranking order itself (does opening-word overlap beat longest-shared-run for slip prediction?) wants the same human test." + }, + { + "id": "does-a-captured-slip-become-navigable", + "source": { "file": "docs/design/confusion-points.md", "item": "④" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "Should a hafiz's own captured confusion points appear in the hop rail as a fourth kind of link — their edges, alongside the corpus's sourced ones — so the app they navigate is shaped by their own history? Appealing, and a privacy/provenance question: it mixes a private log into the surface that draws public, sourced links, and the store's whole privacy gate is built to keep those apart. What would answer it: a decision on whether the warning-at-selection surface (which keeps the log private) already delivers this value without merging the two, which is the current design's bet." + }, + { + "id": "who-sets-a-confusion-seams-state", + "source": { "file": "docs/design/confusion-points.md", "item": "⑤" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "Each confusion point carries a state — every-pass, sometimes, used-to, dismissed — that drives warning weight, list grouping and map colour. The question is who writes it. The design's bet is the reader owns it, because the store sees captures, not recitation: silence at a seam could mean 'I beat it' or 'I stopped logging', and a state the app computed would be the very score this feature refused. The counter-pull is friction — a reader who never marks a seam beaten leaves conquered seams warning forever. What would answer it: a human's call on how strong the nudge may be, whether the app may suggest a state change from count + recency (and how loudly) while the write stays an explicit act, or whether even a suggestion leans too far toward measuring worship. Leaning reader-owned-with-a-quiet-nudge; the line between nudge and assertion is the thing to settle." + }, + { + "id": "should-a-student-share-a-map-with-a-teacher", + "source": { "file": "docs/design/confusion-points.md", "item": "⑥" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "A hafiz revises with a teacher whose job is to drill them exactly where they are weak — which is what the confusion map records — so there is a real pull to let a student share their map with a teacher. OWNER DIRECTION 2026-08-29: the owner said yes, students should be able to share with teachers, and asked that it be its OWN feature (a dedicated capability), not folded into anything else. Kept apart from the backup question (should-a-confusion-map-leave-the-device / decision confusion-map-export) on purpose: a backup is a reader keeping their own copy safe, while sharing sends an intimate record to another person — a larger privacy step and a different consent. That export decision deliberately does NOT settle this and names it as its own future page. What would answer it: its own drawn decision — who initiates and who can undo the share, live map versus a snapshot the student chose, and whether it rides on the file-backup mechanism (a student sends the teacher the file they can already save) or wants something built for it. Not to borrow the export decision's answer." + }, + { + "id": "should-the-app-learn-where-most-readers-slip", + "source": { "file": "docs/design/confusion-points.md", "item": "⑦" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "Across many readers the same look-alike verses would cluster — there are places the whole tradition slips on. OWNER DIRECTION 2026-08-29: the owner asked whether the app could do data analysis on where MOST people slip, pooling many readers' maps to find common slip points. The value is real: it could seed a new reader's warnings before they slip once, and show which verses the resemblance model most needs to cover. But it is a sharp turn away from everything else this feature is — every other part keeps one reader's map on one reader's phone and lets nothing leave without a deliberate act; this asks to gather many readers' maps in one place, close to the opposite of the private design, and the heaviest privacy question the project has yet raised (an intimate record, pooled, off-device, by default). What would answer it: a decision that weighs the pooling itself — whether it can use only data a reader knowingly contributes, whether what is pooled can be stripped of anything identifying and still be useful, and whether a server is needed at all or the common slip points are already knowable from the corpus without touching anyone's map. Named so it is not lost; built by nobody until that decision is made. Distinct from should-a-student-share-a-map-with-a-teacher (one-to-one sharing) and confusion-map-export (a reader's own backup)." + }, + { + "id": "is-the-privacy-rule-nothing-leaves-or-nothing-that-doesnt-serve", + "source": { "file": "docs/design/revision-record.md", "item": "④" }, + "status": "open", + "severity": "question", + "owner": "user", + "note": "The app's privacy stance is written in two places as an absolute — the revision-record 'Export or sync' out-of-scope line and the gate:revision-privacy invariant both read as 'nothing leaves the device'. OWNER DIRECTION 2026-08-31 (on the confusion-map-export decision page): 'nothing leaves the phone' is the wrong stance; 'nothing leaves unless it is in the user's interest, and under their control' is the right one. A flat 'nothing leaves' breaks the first time the reader's own interest points outward (wanting a hard-won record to survive a lost phone) and then reads as a promise abandoned; naming the test keeps the promise. This does NOT loosen gate:revision-privacy — the gate makes leaving a deliberate act, and 'in the reader's interest, under their control' is the shape of a deliberate act; the gate stays, the sentence it defends gets the truer wording. What would answer it: adopting the sharper wording in this doc and the invariant's prose so a future export/sync question is weighed by whether it serves the reader rather than refused by reflex. Already applied in decision confusion-map-export; this row exists so the app-wide framing (revision-record especially) is revisited against it. Related: should-a-confusion-map-leave-the-device." } ] } diff --git a/docs/issues.md b/docs/issues.md index a2dfae2e..1a2622c7 100644 --- a/docs/issues.md +++ b/docs/issues.md @@ -1,5 +1,5 @@ - + # Open items @@ -26,29 +26,67 @@ code, never reproduced. `open` — undecided, nothing blocking. `blocked` — th and unavailable. `answered` — decided, nothing owed in code. `fixed` — closed in code *and* in a test that would fail if it came back; the gate refuses the word without one. -## Open — 40 +## Open — 80 | item | status | severity | owner | waiting on | | --- | --- | --- | --- | --- | -| [`LICENSES.md` buckets `assets/adj/**` under one of its two upstreams](design/track-b-native.md#-licensesmd-buckets-assetsadj-under-one-of-its-two-upstreams--confirmed) | confirmed | defect | agent | — | +| [Counting what a reader pressed stopped meaning anything when tapping the ink was added](design/mark-registration.md#-counting-what-a-reader-pressed-stopped-meaning-anything-when-tapping-the-ink-was-added--confirmed) | confirmed | defect | agent | — | +| [Whether a mark reached by pointing can ever be called only one kind of wrong](design/mark-registration.md#-whether-a-mark-reached-by-pointing-can-ever-be-called-only-one-kind-of-wrong--confirmed) | confirmed | defect | agent | — | +| [The same sitting, sat the rest of the way, says the outlier in ㉙ was not one mark](design/mark-registration.md#-the-same-sitting-sat-the-rest-of-the-way-says-the-outlier-in--was-not-one-mark--confirmed) | confirmed | risk | agent | — | +| [Run as an escalation over every mark the book currently refuses, the rule mostly holds up and one way it does not](design/mark-registration.md#-run-as-an-escalation-over-every-mark-the-book-currently-refuses-the-rule-mostly-holds-up-and-one-way-it-does-not--confirmed) | confirmed | risk | agent | — | +| [Doubling the ground truth moves the cutoff, not just confirms it](design/mark-registration.md#-doubling-the-ground-truth-moves-the-cutoff-not-just-confirms-it--confirmed) | confirmed | risk | agent | — | +| [The re-deal, made: run the checked guard now, and record what it decides](design/mark-registration.md#-the-re-deal-made-run-the-checked-guard-now-and-record-what-it-decides--confirmed) | confirmed | risk | agent | — | +| [The guard ㉜ describes catches one disagreement in four; read both ways, it catches three](design/mark-registration.md#-the-guard--describes-catches-one-disagreement-in-four-read-both-ways-it-catches-three--confirmed) | confirmed | risk | agent | — | +| [A rule that reaches for the ink, rather than resizing toward it, was checked against the whole book](design/mark-registration.md#-a-rule-that-reaches-for-the-ink-rather-than-resizing-toward-it-was-checked-against-the-whole-book--confirmed) | confirmed | risk | agent | — | | [`skins.ts` still says the print's glyphs are anonymous](design/sub-word-marks.md#-skinsts-still-says-the-prints-glyphs-are-anonymous--confirmed) | confirmed | defect | agent | diacritic-shards-need-a-caller | -| [How far is our correction still out](design/mark-registration.md#-how-far-is-our-correction-still-out--open) | open | question | user | placement-residual-by-hand | +| [The marks that ran out of room and still shipped as placed were never looked at, until now](design/mark-registration.md#-the-marks-that-ran-out-of-room-and-still-shipped-as-placed-were-never-looked-at-until-now--confirmed) | confirmed | risk | agent | — | +| [The doubled marks are drawn too small, and no amount of moving them will help](design/mark-registration.md#-the-doubled-marks-are-drawn-too-small-and-no-amount-of-moving-them-will-help--confirmed) | confirmed | defect | agent | — | +| [How far is our correction still out](design/mark-registration.md#-how-far-is-our-correction-still-out--open) | open | question | user | — | +| [A third upstream reaches the adjacency shards](design/track-b-native.md#-a-third-upstream-reaches-the-adjacency-shards--open) | open | question | user | a licensing opinion | | [Arabic number agreement outside `distance`](design/i18n.md#-arabic-number-agreement-outside-distance--open) | open | defect | user | a hafiz | | [A mark id is only meaningful against an array's order](design/sub-word-marks.md#-a-mark-id-is-only-meaningful-against-an-arrays-order--open) | open | risk | agent | diacritic-shards-need-a-caller | | [Are the shards worth 2.28 MB, and against what ceiling](design/sub-word-marks.md#-are-the-shards-worth-228-mb-and-against-what-ceiling--open) | open | question | agent | tajweed-span-to-named-mark | +| [Does a captured slip become navigable like a corpus edge?](design/confusion-points.md#-does-a-captured-slip-become-navigable-like-a-corpus-edge--open) | open | question | user | — | | [Does a real fore-edge stack vary?](design/page-transition.md#-does-a-real-fore-edge-stack-vary--open) | open | question | user | — | | [Whether this document should be generated rather than written](design/etl-pipeline.md#-whether-this-document-should-be-generated-rather-than-written--open) | open | question | agent | — | | [Whether the GPL/App-Store reading in ①–③ is right](design/track-b-native.md#-whether-the-gplapp-store-reading-in--is-right--open) | open | risk | user | a licensing opinion | +| [How are slip-candidates ranked, and is the opening-word signal worth building?](design/confusion-points.md#-how-are-slip-candidates-ranked-and-is-the-opening-word-signal-worth-building--open) | open | risk | user | — | +| [How durable must a confusion map be, given iOS wipes it?](design/confusion-points.md#-how-durable-must-a-confusion-map-be-given-ios-wipes-it--open) | open | question | user | — | +| [Which encumbered inputs are ingredients and which are only instruments](design/what-we-distribute.md#-which-encumbered-inputs-are-ingredients-and-which-are-only-instruments--open) | open | question | agent | — | +| [Is the rule "nothing leaves the device," or "nothing leaves unless it serves the reader"?](design/revision-record.md#-is-the-rule-nothing-leaves-the-device-or-nothing-leaves-unless-it-serves-the-reader--open) | open | question | user | — | | [Whether the names agree with the text they belong to](design/mark-labels.md#-whether-the-names-agree-with-the-text-they-belong-to--open) | open | question | agent | — | | [Would a person pick our rectangle over the one we ship](design/mark-registration.md#-would-a-person-pick-our-rectangle-over-the-one-we-ship--open) | open | question | user | placement-correction-by-eye | +| [Nothing checks the licences of the installed package tree](design/what-we-depend-on.md#-nothing-checks-the-licences-of-the-installed-package-tree--open) | open | risk | agent | — | +| [Nothing measures what the hop does not contain](design/what-we-depend-on.md#-nothing-measures-what-the-hop-does-not-contain--open) | open | question | agent | — | | [Do the pages count the marks they should](design/mark-registration.md#-do-the-pages-count-the-marks-they-should--open) | open | question | agent | — | +| [Whether an answer given by pointing is the same evidence as one given by hand](design/mark-registration.md#-whether-an-answer-given-by-pointing-is-the-same-evidence-as-one-given-by-hand--open) | open | question | user | — | | [What separates the look-alike pairs](design/mark-registration.md#-what-separates-the-look-alike-pairs--open) | open | risk | agent | the placement decision | +| [Whether the marks a reader called odd are odd in the print or odd in our reading of it](design/mark-registration.md#-whether-the-marks-a-reader-called-odd-are-odd-in-the-print-or-odd-in-our-reading-of-it--open) | open | question | user | — | +| [What should refuse a piece union too large to trust](design/mark-registration.md#-what-should-refuse-a-piece-union-too-large-to-trust--open) | open | risk | agent | — | +| [Should a confusion map ever leave the device?](design/confusion-points.md#-should-a-confusion-map-ever-leave-the-device--open) | open | question | user | — | +| [Should a student be able to hand their confusion map to a teacher?](design/confusion-points.md#-should-a-student-be-able-to-hand-their-confusion-map-to-a-teacher--open) | open | question | user | — | +| [Should the app learn where *most* readers slip, not just where you do?](design/confusion-points.md#-should-the-app-learn-where-most-readers-slip-not-just-where-you-do--open) | open | question | user | — | | [Whether a spacing-aware comparison is worth building](design/mark-labels.md#-whether-a-spacing-aware-comparison-is-worth-building--open) | open | question | agent | — | +| [Whether to move the structural metadata to a public-domain source](design/what-we-depend-on.md#-whether-to-move-the-structural-metadata-to-a-public-domain-source--open) | open | question | user | — | +| [Structural metadata reaches the adjacency shards and nothing names it](design/what-we-depend-on.md#-structural-metadata-reaches-the-adjacency-shards-and-nothing-names-it--open) | open | question | agent | — | | [Does a tajweed span land on a mark a reader can be shown](design/sub-word-marks.md#-does-a-tajweed-span-land-on-a-mark-a-reader-can-be-shown--open) | open | question | agent | a hafiz looking at the drawn marks | +| [Does the correction hold on a page nobody measured](design/mark-registration.md#-does-the-correction-hold-on-a-page-nobody-measured--open) | open | risk | agent | — | +| [The look-alike panel draws one rectangle where the geometry gives it several](design/word-indexing.md#-the-look-alike-panel-draws-one-rectangle-where-the-geometry-gives-it-several--open) | open | defect | agent | — | | [Is there anything stopping the correction going stale](design/mark-registration.md#-is-there-anything-stopping-the-correction-going-stale--open) | open | risk | agent | the placement decision | +| [Why the marks were ever in the wrong place, and what has to change](design/mark-registration.md#-why-the-marks-were-ever-in-the-wrong-place-and-what-has-to-change--open) | open | defect | agent | — | +| [Whether to compute the adjacency word ranges from the print instead](design/what-we-depend-on.md#-whether-to-compute-the-adjacency-word-ranges-from-the-print-instead--open) | open | question | agent | — | +| [The page table is derived from one source and corroborated by none](design/what-we-depend-on.md#-the-page-table-is-derived-from-one-source-and-corroborated-by-none--open) | open | risk | agent | — | +| [Whether the pagination question covers three outputs rather than one](design/what-we-depend-on.md#-whether-the-pagination-question-covers-three-outputs-rather-than-one--open) | open | question | user | a licensing opinion | +| [The segmentation disagreement has no disinterested witness](design/what-we-depend-on.md#-the-segmentation-disagreement-has-no-disinterested-witness--open) | open | question | agent | — | +| [Whether the tajweed spans should move to a pause-aware engine](design/what-we-depend-on.md#-whether-the-tajweed-spans-should-move-to-a-pause-aware-engine--open) | open | question | user | a hafiz's judgement on the divergences | +| [Whether the weakest matches go wrong by class rather than at random](design/mark-registration.md#-whether-the-weakest-matches-go-wrong-by-class-rather-than-at-random--open) | open | question | agent | — | | [`PLAN.md` states an unachievable order and one wrong citation](design/track-b-native.md#-planmd-states-an-unachievable-order-and-one-wrong-citation--open) | open | defect | agent | gpl-and-the-app-store | | [Whether Track B should exist at all after ④ and ⑤](design/track-b-native.md#-whether-track-b-should-exist-at-all-after--and---open) | open | question | user | web v1.0 and somebody using it | +| [Whether our own code could be put under more permissive terms at all](design/what-we-depend-on.md#-whether-our-own-code-could-be-put-under-more-permissive-terms-at-all--open) | open | question | user | twelve-verses-of-scripture-ship-in-the-bundle, the-transliteration-table-is-copied-into-our-code, a licensing opinion | +| [Whether a store build should carry every tree](design/what-we-distribute.md#-whether-a-store-build-should-carry-every-tree--open) | open | question | user | inputs-are-not-sorted-into-ingredients-and-instruments | +| [Whether what is left over is even ours to fix](design/mark-registration.md#-whether-what-is-left-over-is-even-ours-to-fix--open) | open | question | user | placement-what-kind-of-wrong | | [Which of the two printings' fonts produced the pages](design/mark-registration.md#-which-of-the-two-printings-fonts-produced-the-pages--open) | open | question | agent | — | +| [Who sets a seam's state — the reader, or the app's guess from the data?](design/confusion-points.md#-who-sets-a-seams-state--the-reader-or-the-apps-guess-from-the-data--open) | open | question | user | — | | [The CI frame budget is a number from an emulator](backlog.md#-the-ci-frame-budget-is-a-number-from-an-emulator--blocked) | blocked | risk | agent | perf-verdict-on-device | | [Does the fold read at all?](design/page-transition.md#-does-the-fold-read-at-all--blocked) | blocked | question | user | a hafiz on the acceptance phone | | [The same measurement on real mid-tier Android](backlog.md#-the-same-measurement-on-real-mid-tier-android--blocked) | blocked | risk | user | an Android phone | @@ -58,6 +96,7 @@ test that would fail if it came back; the gate refuses the word without one. | [On-device perf verdict](PLAN.md#open-follow-ups) | blocked | risk | user | perf-verdict-on-device | | [On-device VoiceOver/TalkBack pass](PLAN.md#open-follow-ups) | blocked | risk | user | screen-reader-walkthrough | | [The tajweed golden row](PLAN.md#open-follow-ups) | blocked | question | user | a hafiz, loop-7 | +| [Two ayah boxes are cut short of the line, and the pen draws them thin](PLAN.md#open-follow-ups) | blocked | defect | agent | the upstream page cache, a 348 MB fetch nobody starts without asking | | [Does the second mount actually hold the frame budget?](design/desktop.md#-does-the-second-mount-actually-hold-the-frame-budget--blocked) | blocked | risk | user | perf-verdict-on-device | | [The site answers on plain `http`, so the offline app silently isn't one](backlog.md#-the-site-answers-on-plain-http-so-the-offline-app-silently-isnt-one--blocked) | blocked | defect | user | the Cloudflare zone dashboard | | [Everything gated on `perf-verdict-on-device`](design/page-transition.md#-everything-gated-on-perf-verdict-on-device--blocked) | blocked | risk | user | perf-verdict-on-device | @@ -67,16 +106,18 @@ test that would fail if it came back; the gate refuses the word without one. | [8+ day offline survival, installed vs browser tab](validation/ledger.json) | pending | risk | user | — | | [On-device pan/zoom + highlight-toggle verdict](validation/ledger.json) | pending | risk | user | — | | [Placement correction by eye — a hundred forced choices between the corrected box and the one we ship](validation/ledger.json) | pending | risk | user | — | -| [Placement by hand — put each rectangle where it belongs, and measure how far our correction still misses](validation/ledger.json) | pending | risk | user | — | +| [Placement on pages the correction has never seen — does it still land, is it the right size, and is the leftover the page or the hand?](validation/ledger.json) | pending | risk | user | — | +| [What kind of wrong — the marks nothing can place, named one at a time instead of counted](validation/ledger.json) | pending | risk | user | — | | [Two days of real revision produce two days in the record](validation/ledger.json) | pending | risk | user | — | | [VoiceOver / TalkBack gesture walkthrough](validation/ledger.json) | pending | risk | user | — | -## Closed — 58 +## Closed — 81 `answered` owes nothing further. `fixed` owes a test, and names it here. | item | status | stays closed because | | --- | --- | --- | +| [`LICENSES.md` buckets `assets/adj/**` under one of its two upstreams](design/track-b-native.md#-licensesmd-buckets-assetsadj-under-one-of-its-two-upstreams--answered) | answered | — | | [Announcing a word run needs the word's text, and nothing ships it](design/word-indexing.md#-announcing-a-word-run-needs-the-words-text-and-nothing-ships-it--answered) | answered | — | | [`ar` as the reference locale](design/i18n.md#-ar-as-the-reference-locale--answered) | answered | — | | [Should an arrow key turn the leaf (±2) rather than the page (±1) at desktop?](design/desktop.md#-should-an-arrow-key-turn-the-leaf-2-rather-than-the-page-1-at-desktop--answered) | answered | apps/web/e2e/desktop.spec.ts | @@ -91,14 +132,22 @@ test that would fail if it came back; the gate refuses the word without one. | [`langSwitchTo(other)` interpolates a locale's name into another locale's sentence](design/i18n.md#-langswitchtoother-interpolates-a-locales-name-into-another-locales-sentence--answered) | answered | — | | [Whether the names on the marks are right](design/mark-registration.md#-whether-the-names-on-the-marks-are-right--answered) | answered | — | | [The map assumes no ayah spans a page](design/word-indexing.md#-the-map-assumes-no-ayah-spans-a-page--answered) | answered | — | +| [The gate states a fact about the root shards that is false](design/what-we-depend-on.md#-the-gate-states-a-fact-about-the-root-shards-that-is-false--answered) | answered | — | | [Hifth's own license](PLAN.md#open-follow-ups) | answered | — | | [The merge pass is a step, not a formality](PLAN.md#open-follow-ups) | answered | — | | [Word geometry is a second corpus, not a layer, and the print is the question](PLAN.md#open-follow-ups) | answered | scripts/gate-words.mjs | | [`refJoin` («، » / ", ») is punctuation, not copy](design/i18n.md#-refjoin------is-punctuation-not-copy--answered) | answered | — | | [Word-granular tajweed painting needs a third text, not a second baking pass](design/word-indexing.md#-word-granular-tajweed-painting-needs-a-third-text-not-a-second-baking-pass--answered) | answered | — | +| [Whether the marks we cannot place from ink are wrong in one way or in two](design/mark-registration.md#-whether-the-marks-we-cannot-place-from-ink-are-wrong-in-one-way-or-in-two--answered) | answered | packages/etl/scripts/lib/mark-ink.test.mjs | | [The field colour](design/page-transition.md#-the-field-colour--answered) | answered | apps/web/e2e/contrast.spec.ts | +| [The licensing map describes one door and is read as describing all of them](design/what-we-distribute.md#-the-licensing-map-describes-one-door-and-is-read-as-describing-all-of-them--answered) | answered | — | | [The spread's phase](design/page-transition.md#-the-spreads-phase--answered) | answered | packages/core/src/pages.test.ts | | [Where the app reads the alignment](design/word-indexing.md#-where-the-app-reads-the-alignment--answered) | answered | — | +| [Whether a finger resting on the rectangle was banked as a placement](design/mark-registration.md#-whether-a-finger-resting-on-the-rectangle-was-banked-as-a-placement--fixed) | fixed | packages/etl/scripts/build-mark-report.test.mjs | +| [Twelve verses of scripture go out through the site door](design/what-we-distribute.md#-twelve-verses-of-scripture-go-out-through-the-site-door--fixed) | fixed | scripts/gate-scripture.mjs | +| [Whether a sitting counts the marks it looked at or the marks it has left](design/mark-registration.md#-whether-a-sitting-counts-the-marks-it-looked-at-or-the-marks-it-has-left--fixed) | fixed | packages/etl/scripts/settle-mark-report.test.mjs | +| [Whether a sitting could be reached from anything but the machine serving it](design/mark-registration.md#-whether-a-sitting-could-be-reached-from-anything-but-the-machine-serving-it--fixed) | fixed | packages/etl/scripts/lib/tailnet.test.mjs | +| [The adjacency shards ship without the notice their upstream requires](design/track-b-native.md#-the-adjacency-shards-ship-without-the-notice-their-upstream-requires--fixed) | fixed | scripts/gate-notices.mjs | | [Whether the أٓ correction is written, and by whom](design/encoding-inspector.md#-whether-the-أ-correction-is-written-and-by-whom--fixed) | fixed | packages/etl/scripts/lib/tajweed-fold.test.mjs | | [Selecting an ayah removes the page-turn keys, with no way back](design/page-turning.md#-selecting-an-ayah-removes-the-page-turn-keys-with-no-way-back--fixed) | fixed | apps/web/e2e/wayfinding.spec.ts | | [Bundle headroom, and who is going to spend it](backlog.md#-bundle-headroom-and-who-is-going-to-spend-it--fixed) | fixed | scripts/gate-budget.mjs | @@ -111,6 +160,8 @@ test that would fail if it came back; the gate refuses the word without one. | [The mounted set has no ceiling](backlog.md#-the-mounted-set-has-no-ceiling--fixed) | fixed | apps/web/src/components/PageStage.budget.test.tsx | | [The mounted set still has no ceiling](design/page-turning.md#-the-mounted-set-still-has-no-ceiling--fixed) | fixed | apps/web/src/components/PageStage.budget.test.tsx | | [Multi-edition records](design/revision-record.md#-multi-edition-records--fixed) | fixed | packages/core/src/revision.test.ts | +| [Whether the sittings on disk are the ones anybody should still be sitting](design/mark-registration.md#-whether-the-sittings-on-disk-are-the-ones-anybody-should-still-be-sitting--fixed) | fixed | packages/etl/scripts/audit-mark-sittings.test.mjs | +| [Nothing would notice the next asset tree that gains an upstream](design/track-b-native.md#-nothing-would-notice-the-next-asset-tree-that-gains-an-upstream--fixed) | fixed | scripts/gate-notices.mjs | | [The vendored corpus is the largest thing we ship, and nothing watches it](backlog.md#-the-vendored-corpus-is-the-largest-thing-we-ship-and-nothing-watches-it--fixed) | fixed | scripts/gate-assets.mjs | | [Whether the oracle's 26.6% coverage should be widened](design/encoding-inspector.md#-whether-the-oracles-266-coverage-should-be-widened--fixed) | fixed | packages/etl/scripts/lib/tajweed-fold.mjs | | [The page bar drew one node per vendored page](backlog.md#-the-page-bar-drew-one-node-per-vendored-page--fixed) | fixed | packages/core/src/pages.test.ts | @@ -119,6 +170,7 @@ test that would fail if it came back; the gate refuses the word without one. | [An ayah polygon does not always cover the ayah](PLAN.md#open-follow-ups) | fixed | scripts/gate-pages.mjs | | [A page turn should say what is between the two pages, and today it says nothing](PLAN.md#open-follow-ups) | fixed | packages/core/src/gestures.test.ts | | [A gate that enumerates from git has a smaller tree on the machine that writes the code](PLAN.md#open-follow-ups) | fixed | scripts/gate-text-sources.mjs | +| [The golden baselines predate the spread triage, and twelve shots fail at a clean](PLAN.md#open-follow-ups) | fixed | apps/web/e2e/golden.spec.ts | | [`--ink-faint` contrast sweep](PLAN.md#open-follow-ups) | fixed | apps/web/e2e/contrast.spec.ts | | [The revision record](PLAN.md#open-follow-ups) | fixed | apps/web/e2e/revision.spec.ts | | [The stage's coordinate model is doubled, and the page sits in a corner](PLAN.md#open-follow-ups) | fixed | apps/web/e2e/stage-fit.spec.ts | @@ -128,12 +180,23 @@ test that would fail if it came back; the gate refuses the word without one. | [A turn across an absent page is silent](design/page-turning.md#-a-turn-across-an-absent-page-is-silent--fixed) | fixed | apps/web/e2e/wayfinding.spec.ts | | [The desktop spread mounts two leaves, not one](backlog.md#-the-desktop-spread-mounts-two-leaves-not-one--fixed) | fixed | packages/core/src/mounted-set.test.ts | | [The tests that should catch ① structurally cannot](design/page-turning.md#-the-tests-that-should-catch--structurally-cannot--fixed) | fixed | apps/web/e2e/stage-fit.spec.ts | +| [The artwork pipeline is documented as applying two changes and applies three](design/what-we-depend-on.md#-the-artwork-pipeline-is-documented-as-applying-two-changes-and-applies-three--fixed) | fixed | SOURCES.md | +| [Two shipped things are covered by no row of the licensing map](design/what-we-depend-on.md#-two-shipped-things-are-covered-by-no-row-of-the-licensing-map--fixed) | fixed | scripts/gate-notices.mjs | +| [Whether the front door could say which sitting you were in the middle of](design/mark-registration.md#-whether-the-front-door-could-say-which-sitting-you-were-in-the-middle-of--fixed) | fixed | packages/etl/scripts/build-sittings-index.test.mjs | +| [Whether the reader could see the rectangle they were vouching for](design/mark-registration.md#-whether-the-reader-could-see-the-rectangle-they-were-vouching-for--fixed) | fixed | packages/etl/scripts/build-mark-report.test.mjs | | [The rounded corner is not visible, and never was](design/page-turning.md#-the-rounded-corner-is-not-visible-and-never-was--fixed) | fixed | apps/web/e2e/stage-fit.spec.ts | +| [Whether pressing the same nudge button twice was answering or zooming](design/mark-registration.md#-whether-pressing-the-same-nudge-button-twice-was-answering-or-zooming--fixed) | fixed | packages/etl/scripts/build-mark-report.test.mjs | +| [Whether the placing distances were ever the reader's own hand](design/mark-registration.md#-whether-the-placing-distances-were-ever-the-readers-own-hand--fixed) | fixed | packages/etl/scripts/score-mark-report.test.mjs | +| [The search was not allowed to look as far as the marks actually are](design/mark-registration.md#-the-search-was-not-allowed-to-look-as-far-as-the-marks-actually-are--fixed) | fixed | packages/etl/scripts/lib/mark-ink.test.mjs | +| [The search could report an answer it had never checked](design/mark-registration.md#-the-search-could-report-an-answer-it-had-never-checked--fixed) | fixed | packages/etl/scripts/lib/mark-ink.test.mjs | +| [A table in our own code is described in its own comment as copied verbatim](design/what-we-depend-on.md#-a-table-in-our-own-code-is-described-in-its-own-comment-as-copied-verbatim--fixed) | fixed | packages/etl/scripts/build-roots.mjs | | [TTI on mid-Android is an exit criterion with no instrument](backlog.md#-tti-on-mid-android-is-an-exit-criterion-with-no-instrument--fixed) | fixed | .lighthouserc.json | | [The exit criterion has 71 ms of margin, on three pages](backlog.md#-the-exit-criterion-has-71-ms-of-margin-on-three-pages--fixed) | fixed | .lighthouserc.json | +| [Twelve verses of scripture go out through the site door](design/what-we-distribute.md#-twelve-verses-of-scripture-go-out-through-the-site-door--fixed) | fixed | scripts/gate-scripture.mjs | | [What the picture should do when tapped](design/revision-record.md#-what-the-picture-should-do-when-tapped--fixed) | fixed | apps/web/e2e/revision.spec.ts | | [Where does the revision map sit at desktop?](design/desktop.md#-where-does-the-revision-map-sit-at-desktop--fixed) | fixed | apps/web/e2e/desktop.spec.ts | | [`onPinch` anchors in the wrong coordinate space](design/page-turning.md#-onpinch-anchors-in-the-wrong-coordinate-space--fixed) | fixed | apps/web/e2e/stage-fit.spec.ts | +| [Placement by hand — put each rectangle where it belongs, and measure how far our correction still misses](validation/ledger.json) | done | — | | [The GPL §6 offer resolves for a stranger](validation/ledger.json) | done | — | --- diff --git a/docs/map.json b/docs/map.json index 03196716..23799a6b 100644 --- a/docs/map.json +++ b/docs/map.json @@ -58,6 +58,16 @@ "symbol": "swipesFromPath", "note": "Why the highlight looks like a pen and not a box. Every ayah's hit area is a run of axis-aligned rectangles, one per line it occupies; this turns each into a band along the line's middle. It is a parser with a fallback, not an assumption — it returns null on any geometry it does not fully recognise and the Highlighter clones the shape instead, so an unfamiliar page degrades to the old box rather than to nothing. Loop 4b was the test of that: 601 pages arrived at once from the same upstream, and the fallback is still there for the edition that does not come from it." }, + { + "file": "scripts/gate-boxes.mjs", + "symbol": "FALLBACK_COUNT", + "note": "The pen, run over the whole book. Three highlight defects were each found by a reader on one page (2:249 fused into a slab, 10:44 dropped to a box by the print's rounding, and the 110 boxes on that same fallback the fix uncovered); each was a class visible in the corpus all along. This sweeps every ayah box with the app's own pen — imported on purpose, since the question is what the code does with the corpus, not what the corpus is — and holds two counts as both ceiling and floor: 8 boxes it cannot read as lines (all on the decorated pages 1–2) and 2 rectangles off the page's line grid (p564 68:3, p602 107:2 — PLAN 17). Fused and dot rectangles are counted and printed, not held. `--list` names every flagged box; `make box-sweep` draws them." + }, + { + "file": "scripts/build-box-sweep.mjs", + "symbol": "glyphsWithin", + "note": "Draws each box the sweep flags on a strip of its real page, at desktop size, with the app's own swipes (or its fallback wash) over the print and the page's line grid behind — so an off-grid rectangle is seen as a thin band, not read as 0.77. Crops the page's glyph paths by a rough bounding box rather than embedding whole pages, which keeps ten strips under a megabyte; reads the app's colour tokens so the picture cannot drift from the app. Refuses to write a page carrying any Arabic codepoint or an SVG text element." + }, { "file": "packages/core/src/highlighter.ts", "symbol": "private paint", @@ -110,6 +120,16 @@ "symbol": "HopPopover", "note": "The chip's expansion: the destinations, and the diff against the current ayah." }, + { + "file": "packages/core/src/verse-diff.ts", + "symbol": "wordDiff", + "note": "Which words two look-alike ayahs share and which they do not, as arithmetic on word indices — no text anywhere in it. Read the header before changing anything here: this replaced a hand-typed table of twelve ayahs' text that shipped in the bundle, and every one of the three reasons it went matters. It was running scripture in a code repository, which is the shape takedown notices get scoped to; it was typed in a plainer spelling than the mus'haf on screen, so the panel showed the reader different letters from the page underneath it; and twelve ayahs out of six thousand is not a feature. None of it was needed, because an edge already carries the matching run on BOTH sides in the print's own word numbering — the same numbering the word boxes use — so the shared stretch is a lookup and what diverges is the leftover at either end. 2,544 of 2,996 look-alike edges carry a span; `null` for the other 452 is not a failure but the data declining to guess, since build-adjacency only emits a span where the shared run occurs in exactly ONE place on both sides. `divergentRuns` beside it returns ranges rather than indices because the caller paints them with `WordIndex.bandsFor`, which wants a run whole so it can collapse it to one rectangle per line of print." + }, + { + "file": "apps/web/src/components/DiffView.tsx", + "symbol": "DiffView", + "note": "The panel that answers 'why do these two get confused' by drawing the mus'haf's own ink, cropped word by word out of both pages, with the divergent tails washed. The reader compares the printing rather than a transcription of it — which is the point, since a transcription is exactly what was wrong before. Costs a fetch of the TARGET's page, so it is lazy on expand: the panel lives inside a popover opened on demand, and both call sites (`HighlightMenu`, `HopPopover`) pass the page in only once it is open. Falls back to the plain note when there is no span, no shard, or no page — the same fallback the old table left for every pair it did not cover." + }, { "file": "apps/web/src/components/PageStage.tsx", "symbol": "async navigateTo", @@ -259,6 +279,11 @@ "symbol": "PageSlider", "note": "The bottom bar: a 604-long track with a page turn on each edge, pinned RTL in both languages so page 1 sits on the right. Two traps live here — it commits on the native `change` event, not React's `onChange` (which is `input`, and would mount a 170 KB SVG per value crossed), and it takes the arrow keys itself, because a range's own step of 1-of-604 snaps straight back inside a three-page inventory." }, + { + "file": "scripts/build-page-bar-options.mjs", + "symbol": "render", + "note": "The bar's two open decisions, drawn: does a juz marker pull the release onto it, and which juz is a page that carries a juz seam in. Reads the manifest's page-per-verse table, core's juz table and the bar's own stylesheet, so every number on the page (1.34 px a page on a laptop, 0.4 on a phone, four seam pages) is re-derived on each run. Refuses to write a page carrying Arabic. Register rows juz-detents and boundary-juz; record docs/decisions/page-bar.md." + }, { "file": "packages/core/src/keymap.ts", "symbol": "appKeyAction", @@ -611,7 +636,7 @@ { "file": "packages/etl/scripts/lib/mushaf-frame.mjs", "symbol": "fitFrames", - "note": "The one copy of the arithmetic that puts two prints of the same page into one coordinate system. Both corpora mark the end of every ayah with an ornament, so the ornaments are a correspondence neither was built to provide: `readTheirs`/`readOurs` pull them out, `readingOrder` puts both lists in the same order (line bands right-to-left, which is the only ordering an RTL page can be paired in), and `fit` solves `ours = s·theirs + t` per axis by least squares. Everything else here is the plumbing that has to be exact for that to mean anything — `pathBBox` with real cubic extrema rather than control-point hulls, `ringsOf`/`pointInRings` for testing a point against a multi-subpath polygon. It is a library and not a script because three callers need identical answers; two copies of a least-squares fit that disagree in the sixth decimal are two different mus'hafs." + "note": "The one copy of the arithmetic that puts two prints of the same page into one coordinate system. Both corpora mark the end of every ayah with an ornament, so the ornaments are a correspondence neither was built to provide: `readTheirs`/`readOurs` pull them out, `readingOrder` puts both lists in the same order (line bands right-to-left, which is the only ordering an RTL page can be paired in), and `fit` solves `ours = s·theirs + t` per axis by least squares. Everything else here is the plumbing that has to be exact for that to mean anything — `pathBBox` with real cubic extrema rather than control-point hulls, `ringsOf`/`pointInRings` for testing a point against a multi-subpath polygon. It is a library and not a script because three callers need identical answers; two copies of a least-squares fit that disagree in the sixth decimal are two different mus'hafs. **What it fits on is not what it is used for, and that gap is the largest measured error in the pipeline.** The ornaments are the only correspondence the two corpora both label, so they are the fiducials; the marks and word boxes this fit then places are the target, and the fit's own residual grades itself excellent while being a full mark-height wrong — page 7 closes to 0.0912 units and its ink is out by about 1.0. Across 604 pages the residual correlates **0.117** with the real error, and read per component it is worse than uninformative: **0.019** against the irreducible per-page scatter and **−0.022** against `|dty|`, the largest term, while managing 0.302 against `|dtx|`, so it grades hardest on the part that matters least. That is Fitzpatrick's fiducial-vs-target result reproduced in a mus'haf, and it is why three sessions of arguing about this residual moved a number and not the problem — the reasons, the citations and the remedy are in `docs/design/mark-registration.md`. Nothing here is wrong; the caution is about what its output may be asked to do. Correcting it is the open decision `mark-placement`." }, { "file": "packages/etl/scripts/lib/candidate-pages.mjs", @@ -641,7 +666,7 @@ { "file": "packages/etl/scripts/lib/diacritics.mjs", "symbol": "export function readDiacritics", - "note": "A named, exact box per mark, on our frame, grouped by the word it sits on. Fits nothing: `apply` is handed in, rebuilt by `applierFromPin` from the four numbers `word-boxes.pin.json` already records, so a mark cannot end up on a second transform that disagrees with its own word's by a tenth of a unit. The invariant that makes this verifiable offline is that a word's shipped box is the union over *every* path in its segment, marks included — so containment holds by construction, and an escape can only be an alignment error, a mark filed under the wrong word. Throws on a `data-diacritic` core has never heard of rather than inventing an id, and its `attr` docblock records the regex trap that cost a corpus-wide run of `[Infinity, …, -Infinity]` boxes: the leading `\\s` is load-bearing, because `d` otherwise matches the tail of `id=\"md-path-…\"`." + "note": "A named, exact box per mark, on our frame, grouped by the word it sits on. Fits nothing: `apply` is handed in, rebuilt by `applierFromPin` from the four numbers `word-boxes.pin.json` already records, so a mark cannot end up on a second transform that disagrees with its own word's by a tenth of a unit. The invariant that makes this verifiable offline is that a word's shipped box is the union over *every* path in its segment, marks included — so containment holds by construction, and an escape can only be an alignment error, a mark filed under the wrong word. Throws on a `data-diacritic` core has never heard of rather than inventing an id, and its `attr` docblock records the regex trap that cost a corpus-wide run of `[Infinity, …, -Infinity]` boxes: the leading `\\s` is load-bearing, because `d` otherwise matches the tail of `id=\"md-path-…\"`. **The four numbers it is handed are page-global, and a page is measurably the wrong grain for a mark.** `applierFromPin` rebuilds one scale and one translation for the whole page, which is the right shape for a word box 22 units wide and too coarse for a mark 5.6 by 3.6: after the best per-page correction, 19.49% of marks still land further than 0.75 units from their own ink, and adding a scale term to that page-global family buys 1.8% of the across-scatter and makes 189 of 600 pages worse — the four-number family is exhausted, not under-fitted. The grain that survives arithmetic is the **printed line**: ~15 groups a page at ~36 marks each, above the floor a median needs, where word is ~3.75 marks a group and would fit noise, and per-mark is excluded on purpose because letting each rectangle snap to its own nearest ink is how \"it is drawn on the wrong mark\" gets manufactured. The line is also the physically motivated unit — justification is set per line, so two prints that set a line differently slide every word on it together — and it is nearly free here, because `` already carries `data-line-number` and only that one field has to be carried through. What to actually do about it is the open decision `mark-placement`, and the fork this function sits on is its option G: whether a finer correction moves the marks alone or the word boxes with them." }, { "file": "packages/etl/scripts/probe-diacritics.mjs", @@ -671,7 +696,22 @@ { "file": "packages/etl/scripts/probe-mark-ink.mjs", "symbol": "const eligible", - "note": "Asks the question nothing before it asked: does a mark's rectangle sit on the ink a reader's eye goes to? Everything earlier counted ink INSIDE the box, which the base letter satisfies — so a box a full mark-height off scored as a hit. What makes this one different is the null control: every mark is also scored one width left, one width right, and on another mark's rectangle elsewhere on the page, and a claimed placement that cannot beat a deliberately wrong one is not a measurement. It does not. As shipped, separation is **−0.242** (claimed −0.127 against wrong +0.116) and only 12.70% of marks beat their own wrong placement; the error is a shift of **0.79 across and 1.07 down** on marks 5.6 × 3.6, the same direction on all 40 sampled pages, and taking each page's own shift out lifts achievable overlap to 0.901–0.912 everywhere. The pipeline's recorded fit residual correlates 0.117 with the real error, which is why a number that looked like validation was blind to this. Exits non-zero on a breached threshold by design, and is deliberately NOT in `gates`, `make ci` or the Makefile — a probe reports, a gate refuses, and nothing should refuse until a person has worked the evidence page. Consumes the labelling verdict as its eligibility filter, so it is downstream of `probe-mark-labels.mjs` and not independent of it." + "note": "Asks the question nothing before it asked: does a mark's rectangle sit on the ink a reader's eye goes to? Everything earlier counted ink INSIDE the box, which the base letter satisfies — so a box a full mark-height off scored as a hit. What makes this one different is the null control: every mark is also scored one width left, one width right, and on another mark's rectangle elsewhere on the page, and a claimed placement that cannot beat a deliberately wrong one is not a measurement. It does not. Over the full pass — 30,000 marks on all 604 pages, 2026-08-12 — separation is **−0.234** (claimed −0.120 against wrong +0.113), only **13.50%** of marks beat their own wrong placement, and the median rectangle overlaps the ink **0.017** against an achievable 0.909; the error is a shift of **0.81 across and 1.06 down** on marks 5.6 × 3.6, the same direction everywhere. Taking each page's own shift out is most of the repair and measurably not all of it: separation goes to **+0.281** and 75.7% of marks beat their null, but **19.49%** are still further than 0.75 units off and the corrected median overlap is 0.492 — so a single translation per page is a good model of this error, not a complete one, and whatever remains is not a page-level shift. The pipeline's recorded fit residual correlates 0.117 with the real error, which is why a number that looked like validation was blind to this. Exits non-zero on a breached threshold by design, and is deliberately NOT in `gates`, `make ci` or the Makefile — a probe reports, a gate refuses, and nothing should refuse until a person has worked the evidence page. Consumes the labelling verdict as its eligibility filter, so it is downstream of `probe-mark-labels.mjs` and not independent of it. **What `--shift-out` writes describes a SAMPLE of pages, and nothing downstream can tell.** `--sample` sets how many marks are drawn and `minMarksPerPage` throws away any page that got too few, so the emitted file holds a row per page that survived and no row at all for the rest — which is why it now writes a `coverage` block, ahead of the rows it qualifies. The default 4,000 marks at `--pages-n 40` reaches **40 pages of 604**; the full pass (`--pages-n 604 --sample 30000`, ~7 minutes) reaches **600 of 604**, the four missing being pages 1, 2, 603 and 604, whose ornamental frames never clear `minMarksPerPage`. Every by-eye trial needs a proposed move to start from, so both sessions so far could only be built on the original 40 — the correction is still validated exclusively on the pages it was fitted to, which is the open half of `the-correction-is-only-judged-where-it-was-fitted`. **The first 40 pages were representative in their mean and badly unrepresentative in their spread**: measuring the same 40 twice disagrees by about as much as those pages differ from each other, so the apparent variation there was mostly noise, and across all 604 the proposed moves span over 1.75 units rather than 0.31 — which is the leverage a placing session needs to measure the correction's size at all. Write a fuller pass to its own path (`mark-shift.604.json`), never over `mark-shift.json`: both scorers fingerprint the displacements and refuse a ruling made against different ones, so rewriting in place discards the answers of any session already worked. Both files are committed under `docs/validation/rulings/`, named with that fingerprint, so a banked verdict re-derives without rebuilding anything. A run at a finer grain adds a `perLine` block beside the per-page rows rather than replacing them — additive precisely so a page-grain run still writes the bytes it always wrote and every fingerprint already banked stays valid, and present at all because a grain that was measured and never written down is a finding rather than a correction. The per-page rows keep meaning exactly what they meant; the finer rows are read on top of them, and a line with no row falls back to its page." + }, + { + "file": "packages/etl/scripts/lib/registration-grain.mjs", + "symbol": "export const GRAINS", + "note": "Over what group are four numbers enough? Once a corpus-wide pass showed the per-page family exhausted — the best transform that could possibly be fitted to a page still leaves ~0.44 units of scatter across and ~0.34 down, and adding a scale term buys 1.8% of it while making 189 of 600 pages worse — the question stopped being *what are the four numbers* and became *what is the group*. A correction is a median or a least-squares fit over a group, so a grain is usable exactly when its groups still hold enough marks: page ~540, **line ~36** (p10 30 / p90 43, measured), word ~3.7, mark 1. The line is not a guess from our own numbers — two renderings of one text diverge because their glyph advances and line metrics differ, so error accumulates ALONG a line and resets at the next, which predicts a tilt within each line as well as an offset per line; `line-tilt` is that typographic prediction written down and it is the rung that wins. On 62,850 marks over 120 pages, fitted on half of each page and scored on the half it never saw, marks further than 0.75 units from their own ink go **17.5% (page) → 11.1% (line) → 4.5% (line tilt)**, against a shuffle null — every line wearing some other line's correction — at **31.2%**, which is far worse than no line correction at all and is what says the per-line numbers are facts rather than absorbed noise. Trained-on reads 3.1% against held-out 4.5%, so the gap is small and this is not overfitting. Nothing here rasterises: `dx, dy` already say where the ink is relative to the rectangle, so the residual under a candidate grain is arithmetic and a search over grains costs seconds — what it cannot say is how much ink a moved rectangle then COVERS, because overlap is not linear in displacement, and that only comes from re-scoring in `probe-mark-ink.mjs`. Extracted from that probe rather than left in it so the exploratory ladder and the shipped scorer run one implementation. Finer grains layer ON TOP of the page median, so a line too sparse to estimate falls back to its page rather than to nothing, and a page with no measured shift gets zero rather than a guess. **`SPLIT_FLOOR` is the trap this file names in its own docblock**: holding a split-half measurement to the production floor of 20 refuses a printed line that arrives at the fit with eighteen, so what gets measured is how many lines survived the halving rather than how well the grain works — at the production floor only 635 of 1,723 lines earned a correction and the grain read about three times weaker than it is. A floor sweep from 2 to 10 is completely flat, so the answer is not floor-sensitive." + }, + { + "file": "packages/etl/scripts/lib/registration-grain.test.mjs", + "symbol": "describe(\"a per-line correction, against a corpus that has per-line structure and one that does not\"", + "note": "25 tests on prints whose true difference we built ourselves, and the pair at the centre is the whole reason the file exists: two fixtures identical in page displacements, wobble and counts, differing only in whether their lines really move independently — and the flat one must **appear** to improve on the marks it was fitted to and then lose on the marks it never saw. A version of this file where both fixtures pass on trained-on numbers alone is a version that would have let us ship noise. Everything is deterministic (a seeded LCG, not `Math.random`) because a fixture that wobbles between runs cannot be argued from months later, which is the only reason these numbers exist. The floor suite reproduces the real trap at real scale: 36 marks a line, a quarter of them surviving the production floor after halving against 635 of 1,723 in the corpus. Three assertions were rewritten rather than loosened when they failed, and the reasons are in the file: a least-squares slope is not exactly orthogonal to an alternating residual over 40 points; a tilt fitted from 60 marks each carrying a quarter-unit of wobble is worth a tenth of a unit at the line's ends and not more; and `line-tilt` ties `line` on a flat corpus because a median offset and a least-squares intercept have comparable efficiency — so the honest claim is that tilt overfits harder and still loses to the PAGE grain, not that it loses to `line`." + }, + { + "file": "packages/etl/scripts/probe-ornament-witness.mjs", + "symbol": "const shipped", + "note": "The one measurement in this whole line of work that is not fitted to the thing it is graded by. Everything else moves a rectangle until it agrees with the ink and then reports how well it agrees with the ink, which improves with every parameter added — including when the parameter is fitting noise. The ayah-end ornaments are the escape: they are the only objects both corpora label, `word-boxes.pin.json`'s four numbers per page were fitted on them and nothing else, and the ink correction has never seen one. So it asks the inverse question — correct the text to sit on its ink, and where do the ornaments end up? Answer, over 1,305 pairs on 120 pages: **0.053 units under the shipped fit, 1.206 under the ink correction, worse on 120 of 120 pages**, and the same at every grain. The two printings genuinely agree about their ornaments and disagree about their text, so `mark-registration.md`'s old sentence \"about the whole page\" was too generous and now reads \"about the whole page's text\". The consequence for what ships is the reason this file exists rather than being a paragraph: the correction belongs to the words and the marks, and moving the ayah-end markers with it would break the one part of the page that is currently right. Pairs the ornaments with `readingOrder` exactly as `fitFrames` does, so these are the very points the shipped numbers were fitted on rather than a second opinion about which ornament is which; assigns each ornament a printed line from the word bands around it, because their ornaments carry no `data-line-number` and asking a per-line correction about the wrong line would make the test easier than it should be. Reads the rows a scoring run dumped (`probe-mark-ink.mjs --rows-out`) rather than fitting anything itself, takes seconds, writes nothing, and is a probe and not a gate for the usual reason." }, { "file": "packages/etl/scripts/probe-mark-labels.mjs", @@ -686,7 +726,17 @@ { "file": "packages/etl/scripts/lib/mark-ink.mjs", "symbol": "bestPlacement", - "note": "The scorer, and the choice of scorer is load-bearing. Product-moment correlation of two binary masks (`phi`) DECIDES; Jaccard/IoU is reported and decides nothing, because IoU cannot go negative and so cannot tell 'wrong place' from 'nothing there' — the distinction the whole null control turns on. Two-stage search, quarter-unit coarse lattice then one-sample refine, ties broken toward the smaller displacement so a search cannot manufacture a better answer by moving further. Also `wilson`/`sampleSizeFor` for the intervals, `rng` (mulberry32, one stream per purpose so adding a control does not renumber the sample), and `chooseExemplars`, medoid-by-nearest-to-mean. An earlier version reported a correlation of 5.509 where the maximum is 1 — shared area and ink counted over different regions when a displacement pushed a shape off the raster edge — and it now throws rather than printing an impossible number." + "note": "The scorer, and the choice of scorer is load-bearing. Product-moment correlation of two binary masks (`phi`) DECIDES; Jaccard/IoU is reported and decides nothing, because IoU cannot go negative and so cannot tell 'wrong place' from 'nothing there' — the distinction the whole null control turns on. Two-stage search, quarter-unit coarse lattice then one-sample refine, ties broken toward the smaller displacement so a search cannot manufacture a better answer by moving further. Also `wilson`/`sampleSizeFor` for the intervals, `rng` (mulberry32, one stream per purpose so adding a control does not renumber the sample), and `chooseExemplars`, medoid-by-nearest-to-mean. An earlier version reported a correlation of 5.509 where the maximum is 1 — shared area and ink counted over different regions when a displacement pushed a shape off the raster edge — and it now throws rather than printing an impossible number. **Neither stage may step outside the distance it was given, and the refine stage used to.** It sweeps around the coarse winner, so a winner sitting on the boundary let it reach a quarter unit past — an answer whose surroundings were never scored, and one it could not have known was best. The damage is not the quarter unit: the only way anything downstream can tell a mark ran out of room is that its offset came back sitting exactly on the boundary, so every mark that slipped past went invisible as a refusal. 2,252 in the shipped corpus, 1,923 of them accepted and shipped as placed from their own ink at a median match of 0.859 against the 0.909 a good match scores — nearly three times the edge population being counted, all of it on the wrong side." + }, + { + "file": "packages/etl/scripts/lib/mark-ink.mjs", + "symbol": "export function withSecondLook", + "note": "Why a mark that could not be placed gets a second, wider look, and why the wider look is not simply the search. The reader's own answers say the marks the three-unit search gives up on are a median of 4.292 units away, and 230 of 272 are further out than three — it was being asked to find something it was forbidden to reach, and where it ran out of room it was already straining the right way, agreeing with the reader 92 times in 100 across and 99 down. Searching eight units instead takes the distance from where a person put the box from 1.816 to **0.099**, 84% of them within a unit. But widening it for EVERY mark moves 4.11% of the already-accepted ones — 2,722 marks — by more than two units, worst 13.528, because a wider window finds a better-scoring match that is the neighbouring mark's ink and does it confidently; there is no ground truth on those and never will be, so they cannot be adjudicated, only avoided. Hence escalate rather than widen: three units as before, eight only where that refused. The fold-back rule is one-directional on purpose — a wider answer is taken only where the first look refused, only when the wider look does not itself refuse, and only when it matches better — which makes the risk to the 99.43% zero by construction rather than small. Checked on 14,398 accepted marks over 28 pages: 14,046 byte-identical, and all 352 others were marks the old search had reported outside its own boundary, which is the clamp above. `ranOutOfRoom` and `refusedItsOwnInk` live here rather than in `build-mark-report.mjs` so the rule that NAMES a mark a refusal and the rule that rescues one cannot drift apart, and they read `searchedAt` off the row: how far a mark was allowed to look is now a property of the mark, so one file holds rows searched to different distances and testing them all against one number reads a real answer at 3.0 as a wall-hit." + }, + { + "file": "packages/etl/scripts/lib/mark-ink.test.mjs", + "symbol": "describe(\"the search stays inside the distance it was given\"", + "note": "The two properties that make the second look safe, asserted rather than argued. First, the search never reports further than the distance it was given — four distances, each with the ink placed two pixels beyond reach so the sweep is straining against its own boundary the whole way, plus one asserting it lands ON the boundary exactly when the ink is out of reach, which is the signal everything downstream reads. Second, the fold-back is one-directional: a wider answer is taken only where the first look refused, only when the wider look does not itself refuse, and only when it matches better — and a mark the first look placed comes back the same object. That last one is the whole safety argument for widening at all, so it is the assertion that must never be relaxed. The rectangle in these fixtures is drawn wider than the block it holds, as a real one is, and it has to be: a rectangle exactly filled by its own ink has nothing varying inside it to correlate against and every placement of it scores the same — which is what the first version of these tests measured before it measured anything." }, { "file": "packages/etl/scripts/lib/mark-shape.mjs", @@ -708,25 +758,130 @@ "symbol": "planNudge", "note": "The other plan, in the same module and deliberately not in the same session: no choosing, one rectangle, and a reader who drags it where it belongs. It exists because a forced choice yields at most one bit — *is ours preferred to what ships* — and a correction that points the right way and falls a third short wins every trial and is still wrong. Subtracting the machine's proposed move from the reader's landing gives the residual, which is a distance that can be applied rather than a preference that can only be counted. `START_R` puts every rectangle 3 units out in a uniformly random direction: well clear of both placements a forced choice would have offered (about 1 unit apart), so nobody lands by not moving; inside the window with the box's own half-width to spare; and evenly spread, so the pull a hand exerts toward its own starting point cancels in the average instead of being baked in as a fact about the boxes. `REPEAT_GAP` keeps a mark's two showings at least 10 trials apart, and those showings are inserted across the session rather than appended — the pair measures the reader's precision, and a precision built from the last fifteen trials measures the end of an hour. Placement cannot be offered inside a two-panel trial: dragging to fit and then comparing the landing to the two starts *is* the answer key, and offering the drag after the answer leaks the correction's direction across the session." }, + { + "file": "packages/etl/scripts/lib/adjudication.mjs", + "symbol": "selectPages", + "note": "Which pages a sitting is allowed to draw from, and the answer to the largest limitation the first two sittings had: every trial they offered came from one of the forty pages the correction had been fitted to, so neither could ever have said the correction *travels*. `exclude` holds out the pages an earlier sitting used; `pages` says how many to keep. The keeping is deliberately not random — it takes from both ends of the spread on each axis, then the pages furthest from the middle of both, because what makes the SIZE of the correction readable is leverage and not sample size: across the original forty the proposed moves spanned about a third of a unit, nearly all of it measurement noise, and no number of placements on pages that all want the same move can separate a correction that is right from one that is a fifth short. Across the held-out forty the same span is 1.75 across and 3.38 down. The chosen list is written into the session and **replayed** when it is scored rather than recomputed here — the scorer rebuilds its trial list from the seed, so a narrowing applied at build time and not at score time would put every trial index against a different mark with nothing throwing, and recomputing instead of replaying would mean any later improvement to this function silently re-scores every sitting ever banked. Order is preserved for the same reason the seed is: the planner shuffles the array it is handed, so the same pages in a different order are a different session. It keeps a second strategy, `even`, and the two exist because the two sittings estimate different kinds of thing from the same pages. A placing session reads a slope off a regression, and a slope is bought with leverage, so it wants the ends of the range and does not mind that they are unusual pages. A forced choice reports a proportion — how often a reader prefers ours — and a proportion has no leverage to gain and a representativeness to lose: ask it only about the extremes and it fills with the easiest trials and the impossible ones, then reports the result as a fact about the mus'haf. `even` is a systematic every-k-th walk through the print rather than a random draw, for the same reason nothing here has a second seed: the same inputs must give the same list on any engine months later. The half-step offset keeps it off both ends, and consecutive indices are at least one apart by construction, so no page is named twice." + }, + { + "file": "packages/etl/scripts/lib/adjudication.mjs", + "symbol": "sameBuild", + "note": "What has to match before two sittings may be compared with each other, and it lives beside the planners because they are what defines a build: the same seed, the same length, the same displacements and the same page list produce the same trials in the same order from the same starting points, and any one of them differing means the two people were not asked the same question. It returns every mismatch rather than the first, because a wrong file is usually wrong in several ways at once and being told about them one run at a time is how somebody gives up and averages the two anyway. The fifth check is the one worth writing down: two sittings that agree on all four of those *and* on who sat them is one hand compared against itself, which returns beautiful agreement, answers nothing, and — unlike a wrong-build comparison — leaves nothing in the printed output that would look odd to a reader." + }, { "file": "packages/etl/scripts/build-mark-adjudication.mjs", "symbol": "function panel", - "note": "Renders the session to one self-contained page: two panels, one rectangle each, no verdict, no feedback, no answers in the payload. `panel` is its own renderer and must stay that way — the measurement's own crop draws the other printing's outline in green exactly where the corrected rectangle goes, which is the answer key in a colour. Every panel is the same fixed square window (`windowFor`) plus a small framing wobble, so a catch cannot be spotted by its zoom and the corrected box is never dead centre. Asks a second question alongside the first — a tick, `N`, for *neither rectangle closes around the mark* — and it is a tick rather than a fourth button on purpose: a box can be in the right place and the wrong size, which is a different fault with a different repair, and a single \"wrong somehow\" answer would both blend the two and eat the trials where \"which is closer\" still has an answer. `head.asks` records that the question was put, so a ruling worked before it existed scores as *not asked* rather than as nobody-complained. Fingerprints the displacement file into the page so a ruling can be refused later if the measurement moved underneath it; resumes from localStorage keyed on seed + fingerprint. Not checked in and never should be: it draws crops of the mus'haf's own artwork, and the standing rule is that no scripture is committed here." + "note": "Renders the session to one self-contained page: two panels, one rectangle each, no verdict, no feedback, no answers in the payload. `panel` is its own renderer and must stay that way — the measurement's own crop draws the other printing's outline in green exactly where the corrected rectangle goes, which is the answer key in a colour. Every panel is the same fixed square window (`windowFor`) plus a small framing wobble, so a catch cannot be spotted by its zoom and the corrected box is never dead centre. Asks a second question alongside the first — a tick, `N`, for *neither rectangle closes around the mark* — and it is a tick rather than a fourth button on purpose: a box can be in the right place and the wrong size, which is a different fault with a different repair, and a single \"wrong somehow\" answer would both blend the two and eat the trials where \"which is closer\" still has an answer. `head.asks` records that the question was put, so a ruling worked before it existed scores as *not asked* rather than as nobody-complained. Fingerprints the displacement file into the page so a ruling can be refused later if the measurement moved underneath it. Since the full 604-page pass it takes `--exclude` and `--pages` too, handed to `selectPages` with `even` as its default rather than the placing session's `extremes`, and for the reason that function's own note gives: this page only ever asks which of two rectangles is closer, so it estimates nothing from how far apart the proposed moves are and would only lose by bunching at the ends. The narrowing is what the check that releases the mark layer actually needed — until the measurement covered 604 pages, every question this page could ask came from one of the forty the correction was fitted to, so a hundred trials bought a very precise answer about training pages. The page list it settles on goes into the head with its own fingerprint, is replayed rather than recomputed when the ruling is scored, and is folded into both the localStorage key and the name of the file that lands in your downloads — two sittings from one seed that asked about different pages would otherwise share a resume key and stack one's answers onto the other's trials, and arrive in the same folder under the same name. Not checked in and never should be: it draws crops of the mus'haf's own artwork, and the standing rule is that no scripture is committed here." }, { "file": "packages/etl/scripts/score-mark-adjudication.mjs", "symbol": "const CATCH_FLOOR", - "note": "Rebuilds the key from the seed and only then learns what the answers were — which is the whole defence of the result, since nobody had to be trusted not to peek at something that did not exist. Refuses (exit 2) when the displacement file no longer fingerprints to what the page was built from: answers attached to a question that has since changed come out looking clean and mean nothing. Reports four numbers with Wilson intervals and \"can't tell\" excluded from the denominator rather than counted as a miss — folding it in would punish exactly the care the session asks for. A fifth line, on its own axis and out of the headline's arithmetic, counts the trials where the reader said neither rectangle closed around the mark, and names them so somebody can go and look; the headline is then reprinted without them as a comparison, never as a stricter verdict, since dropping trials only widens the interval. Three distinguishable outcomes, and the middle one is why the decoys exist: supported · not supported *and the decoys clear half*, meaning the shift was visible and ours still lost, which is our correction being wrong rather than the test being blunt · neither clears, meaning the session says nothing either way." + "note": "Rebuilds the key from the seed and only then learns what the answers were — which is the whole defence of the result, since nobody had to be trusted not to peek at something that did not exist. Refuses (exit 2) when the displacement file no longer fingerprints to what the page was built from: answers attached to a question that has since changed come out looking clean and mean nothing. It **replays** the page list a narrowed sitting recorded rather than choosing again — the trial list is rebuilt here from the seed, so a session narrowed at build time and scored unnarrowed would resolve every index onto a different mark and throw nothing, and recomputing instead of replaying would let a later change to how pages are chosen silently re-score every ruling ever banked. That is a second refusal on top of the fingerprint, not a duplicate of it: a displacements file can carry the right fingerprint and still have no row for a page the sitting used, and the message says so in those words. Reports four numbers with Wilson intervals and \"can't tell\" excluded from the denominator rather than counted as a miss — folding it in would punish exactly the care the session asks for. A fifth line, on its own axis and out of the headline's arithmetic, counts the trials where the reader said neither rectangle closed around the mark, and names them so somebody can go and look; the headline is then reprinted without them as a comparison, never as a stricter verdict, since dropping trials only widens the interval. Three distinguishable outcomes, and the middle one is why the decoys exist: supported · not supported *and the decoys clear half*, meaning the shift was visible and ours still lost, which is our correction being wrong rather than the test being blunt · neither clears, meaning the session says nothing either way." }, { "file": "packages/etl/scripts/build-mark-nudge.mjs", "symbol": "function inkPaths", - "note": "The placement page: one panel, the page's own outlines, and one rectangle that always starts 3 units off. `shift` — what the measurement proposes — is on the planned trial and is never read here, so it reaches no markup, no data attribute and no comment; anybody can view source and find no target, which is the only version of that claim worth making. What the page *is* given is where the rectangle starts, a random direction from where the app draws it today, which points at the right mark without pointing at the right place. The commit button stays dead until the box is moved, because a rectangle that always starts wrong makes \"I left it alone\" a garbage datum rather than an answer. Pointer capture is on the panel and not the rectangle — a one-unit box is about twenty pixels and asking somebody to grab it exactly would make this a test of aim. Records the start alongside the landing, since the pull of a starting point on a landing can only be measured with both ends of it in the record. Same fingerprint guard, same `window.HIFTH_SESSION` sink, same localStorage resume as the forced-choice page; not checked in, for the same reason it is not — it draws crops of the mus'haf's own artwork." + "note": "The placement page: one panel, the page's own outlines, and one rectangle that always starts 3 units off. `shift` — what the measurement proposes — is on the planned trial and is never read here, so it reaches no markup, no data attribute and no comment; anybody can view source and find no target, which is the only version of that claim worth making. What the page *is* given is where the rectangle starts, a random direction from where the app draws it today, which points at the right mark without pointing at the right place. The commit button stays dead until the box is moved, because a rectangle that always starts wrong makes \"I left it alone\" a garbage datum rather than an answer. Pointer capture is on the panel and not the rectangle — a one-unit box is about twenty pixels and asking somebody to grab it exactly would make this a test of aim. Records the start alongside the landing, since the pull of a starting point on a landing can only be measured with both ends of it in the record. Same fingerprint guard, same `window.HIFTH_SESSION` sink, same localStorage resume as the forced-choice page; not checked in, for the same reason it is not — it draws crops of the mus'haf's own artwork. Since the full 604-page pass it can also be pointed at a subset: `--exclude` names the sittings whose pages must be held out and `--pages` how many to keep, both handed to `selectPages`, and the page list it settles on is recorded in the session head with its own fingerprint. That fingerprint is folded into the localStorage key alongside the seed and the displacements — two sittings from the same seed and the same measurement but different pages would otherwise share a key and stack one's answers onto the other's trials, which is the failure `shows` was already in that key to prevent, arriving by a new route. It writes a companion `.pages.json` beside the page in the same shape a displacements file uses, so the next sitting excludes it with no second format to parse. `--reader` names whoever is sitting it, and it is the only field here that never reaches a trial: the second person must see the same marks in the same order from the same starting points as the first, or the two hands are not comparable and the whole point of asking a second one is gone. It is folded into the resume key and into the name of the file that lands in your downloads, because two readers of the same build otherwise share both — one would resume into the other's answers, and the second download would arrive as a copy of the first." + }, + { + "file": "packages/etl/scripts/build-mark-report.mjs", + "symbol": "function crop", + "note": "The page that asks what *kind* of wrong, and the only instrument here whose answer is not a number. The other two by-eye pages ask which of two rectangles is closer and how far one should move, and both are distances — which is the right currency right up to the point where the correction places most marks from their own ink and what is left over turns out, a third of the time, to be a rectangle with essentially no ink under it at all. There is no distance that says *there is nothing in this place*, and none that separates a box in the right place at the wrong size from a box at the right size in the wrong place, or either of those from a defect in the printing we vendored. So the answer is a vocabulary of five — move it · wrong shape · box the right ink instead · the print is odd · bank it — and a reader may give as many as are true about one mark, which is the deliberate break from every earlier sitting: forced to pick one, a reader picks the most obvious and the second fault is never recorded by anybody. Two of the five are load-bearing on their own. *Wrong shape* carries no coordinates and never will: the two faults have two repairs in two files, and a size complaint expressed as a displacement is a wrong number that looks like a right one. *The print is odd* is the only answer on the page that is not about us, and it routes to `docs/issues.json` rather than into any ruling that grades a correction — counted as a placement failure it would make our correction look worse than it is, and filed the other way round, better. `crop` is why the page can be answered at all: it clips the printed rings once into a wide window and hands the markup two viewBoxes over the same paths, so the toggle that swaps identifying-the-letter for judging-the-rectangle costs nothing and never re-renders. The first sitting was stopped after one screen for want of exactly that. Pointer capture is on the rectangle rather than the panel — the opposite of the placing page, and for a reason that only shows up on a phone: the picture is most of the screen there, and with `touch-action: none` on the whole stage a finger scrolling past it silently recorded a placement nobody made. It was caught because a reload showed an answer that had never been given. Same `window.HIFTH_SESSION` sink, same localStorage resume, same fingerprint guard as the other two, and a retraction is appended as its own event rather than erasing anything: a reader who says a thing and unsays it has told us the mark is a hard one. Counts what is left and nothing else — no score, no running share, no verdict — because a reader who can see the score answers the score. Two fields on the transcript are the difference between a sitting that can be scored and one that cannot. `rule` rides on every answer and says which of the two populations the mark came from — placed from its own ink, or fallen back to the printed line — because a transcript that only says *this is badly placed* cannot be split back apart afterwards, and the two are different options being decided between. It is named `rule` rather than `by` after an hour in which it was called `by`: a corrected placement already answers with a `by`, the amount the reader dragged the rectangle, the payload was spread last, and the drag silently ate the population marker on exactly the answers that carry a correction — invisible in the page, invisible in the file, and visible only as a scorer that cannot separate the two populations where it matters most. `seen` is a high-water mark of how far the reader got, and it is the denominator rather than bookkeeping: for a long time there was deliberately no *this one looks right* button, because a sitting where the common case needs a click is a sitting nobody finishes, so passing a mark IS the verdict that nothing is wrong with it. That held until a reader sat sixty marks, found nothing wrong with any of them, and had no way to say so — silence and *I have not looked yet* are the same bytes, and a reader who cannot affirm starts to wonder whether they are meant to be finding faults. So there is now an affirm button, and `seen` is still the denominator: affirmations are reported *against* it as how much of it somebody explicitly vouched for, never as a denominator of their own. Vouching for none of sixty and vouching for all sixty are the same fault rate and a completely different sitting, and the pair of numbers is the only way to tell them apart. Without the count, a transcript holding four answers is a reader who looked at sixty marks and found four bad ones or a reader who looked at five and shut the tab — 6.7% and 80%, the same bytes, both consistent with everything else in the file. It is a high-water mark and not a position so that going Back and forward again cannot shrink what was seen. Reader-typed notes are escaped where the answer list is built, because that list is concatenated into markup and the buttons after a mangled note are the reader's only way to take an answer back. Not checked in, for the reason none of them are: it draws crops of the mus'haf's own artwork. Before the sixteen sittings that cover every fallen-back mark were sat, the page was read against a design brief, and three of the findings changed what the answers *mean* rather than how long they take. In dark mode the two rectangles were drawn in re-themed colours on paper that is deliberately never re-themed, so ours fell from 5.05:1 against that white to 2.49:1 and the reader's from 4.89:1 to 1.70:1 — and a reader who cannot see the rectangle affirms it, which is a failure that arrives looking exactly like success. The replacement is worth stating as a rule: nothing drawn on the paper is themed. The four rectangle tokens join `--paper` and `--ink` in the never-re-themed set and are defined in bare `:root` only, while `--ours` and `--yours` stay themed because they are chrome and sit on the field. The reader's rectangle is dashed as well as green, so the two rectangles stay distinguishable under deuteranopia and survive anybody re-theming the palette later. Second, the drag handler rebuilt the whole stage from `c.svg` on every pointer frame — 23 KB of path data on the worst card — so `paint` was split into a `mount` that writes the markup once per card and a `paint` that only sets attributes on the three cached nodes. That is what `crop`'s two viewBoxes were always for, and it had never actually delivered it. The stroke width left the per-frame writes at the same time: it was computed from the viewBox and only landed at a sensible thickness because the stage happens to be full-width, so it is a non-scaling stroke measured in CSS pixels now. Third, the answer list sat directly above Back and Next with its take-it-back button right-aligned into the corner Next had just occupied, so two taps of Next could retract the answer just given; it moved to the *start* of its row before it was enlarged to a thumb-sized target, because enlarging a mis-tap target first only doubles it. The answer buttons themselves are now a dock at the bottom of the screen. It is fixed rather than sticky on purpose: sticky can only lift an element that still has somewhere to scroll past, and the dock is the last thing in its own scrolling column, which is exactly where sticky does nothing at all. The main column pays for the strip in bottom padding instead, and the viewport meta carries the cover fit or the safe-area inset resolves to zero on a phone with a home bar. The picture keeps its own natural shape and nothing shrinks, which is why a dock was chosen over a constant-height stage. Four smaller findings landed beside them, all of them costing time and accuracy rather than truth: the affirm button inverts to a solid fill with a check glyph when it is pressed; the explanation splits into a full version and a one-line brief with a quiet toggle between them, remembered the way `seen` is, which takes some 230 px off the fold *without adding a tap to any answer* — that constraint is the whole design, since making a fault cost one more tap than an affirmation biases the exact ratio the sitting exists to measure; the mark's own name reaches all three branches of the sentence that identifies it, and the containing word is drawn after the big letters instead of being thrown away on the 91 cards in 115 that carry both; and four controls between 24 and 34 px reached 44, take-it-back and put-it-back-where-it-was among them, which are the two destructive ones. The count under the card and the count the next build prints were two different numbers for as long as the page existed, and a reader hit it three times: they banked an hour, watched the number stay where it was, banked again, and watched it stay again. Nothing was lost on any of those presses — handing over is a copy, and the deal is fixed when the page is built, so the only thing that could ever move the total was building the sittings again from the answers, which happens on a laptop the reader is not sitting at. But an instrument that cannot show somebody their own work is one they stop believing, and this one asks for forty hours on trust. So the deal and what is left of it are two lists now: `CARDS` is what was dealt and never changes, `DECK` is what is still owed an answer, and handing over retires what it handed over. The retired ids are persisted beside the transcript, because a reload that brought them back would tell the reader their work did not count after all — the same failure, one refresh later. Three things are load-bearing about the split. The arithmetic has to match the builder's exactly — the standing answers, with retractions already spliced out — or the two numbers go on disagreeing and one of them is lying. The transcript is deliberately NOT retired with the deck: every hand-over writes the whole of it under one name, so a later, smaller write would silently destroy an earlier, larger one, which is also why what comes back from the session is filtered against the deal rather than against what is left of it. And the place moves with the deck rather than resetting — a reader stays on the card they were standing on if it survived and lands on the next survivor if it did not, while marks they passed in silence stay counted as looked at and still owed, since those are the ones the sitting has not finished with. That last piece is arithmetic on two indices shifting under each other, so it is the one test here that runs the emitted script instead of reading it. How far one press moves the rectangle is a slider from a hundredth of a unit to half a unit, and it used to be a button swapping between half and a tenth. The two sizes were both defensible — half a unit is a seventh of the height of a mark and the size of the error being corrected, a tenth is well inside it — but they were the wrong shape for what a reader actually does, which is cover the distance in one coarse press and then spend the rest of the correction on whatever is left over. That remainder is different on every mark, and with two numbers on offer a reader rounds their intention to the nearer one; twenty-six marks took two hundred and five nudges in the sitting already banked. The ends of the range are where the two sizes were, the bottom being below the width of the stroke drawing the rectangle at the close framing, which is where pressing again stops changing anything anybody can see. Two things are worth knowing about it. The bounds are written twice, in the slider attributes and in the clamp that guards what comes back out of storage, and drift between them is silent — a value the slider offers and the clamp rejects sends every later press back to the coarse end without saying so, which is why a test asserts they are the same two numbers. And the size survives a reload, because it is the size of every answer about to be banked and a quiet return to the default would change that size without changing anything on the screen. The rectangle can also be placed by **pointing at the ink** instead of pushed onto it, and the reason is a measurement: of the 160 marks the sittings so far worked through, 158 were moved, over 350 separate goes, median 2.105 units of travel and worst 12.229 on a mark 5.6 by 3.6. Each of those goes is a person doing two jobs — which mark is meant, which only a person can do, and how far across and down, which the print already knows — so the second is handed back to the print. `crop` cuts the window's ink into its separately-connected pieces by flooding a raster of it, at the same resolution `probe-mark-ink.mjs` measures at (cutting the print into pieces at a different grain from the one the ink search ran at would let the page and the measurement disagree about what a piece even is), assigns each ring to a piece by a vote of its boundary so a counter inside a letter does not become its own piece, and emits one path per outline carrying the piece's number. A tap toggles a piece and the rectangle takes the bounding box of everything pointed at: pointing **adds** rather than replaces, because a tanween is two strokes and a reader who pointed at one half of one has not yet said where the mark is. What is under the finger is answered from the pieces' extents with the same fingertip of slack the rectangle allows, rather than by asking the browser what is under the point — a haraka is a stroke a few screen pixels wide at the framing that shows the whole word, so exact hit-testing would make this a test of aim. Two rules, in order: the piece the finger landed *on* wins, the smallest of them where several contain the point, which is what hands the finger the mark rather than the letter it sits over and the mark is what is being tagged; and only where it landed on none does slack reach for the nearest piece within a fingertip. Smallest-first was the whole of it for a while, applied flatly across everything within slack, and flat it reaches past the ink the finger is actually on — a point squarely inside a large piece was answered with a small piece it had merely come near, and the rectangle jumped onto ink the reader could see they had not touched. Measured over built sittings before and after: aiming at the dead centre of a piece returned a different piece on 3.7% of aims at the wide framing and 1.2% after, and taps landing on blank paper that reached past nearer ink to something smaller further away, 2.3% of them, stopped. The gesture deliberately adds **no sixth word** to the vocabulary: a tap says the same two things a drag says — it belongs somewhere else, it is the wrong size — and goes through the same 700 ms burst, so the sitting stays comparable across parts and nothing downstream has to learn a new answer. What it does add is `how`, on every answer that came from a tap, plus the pieces chosen; and `nudge` clears it, because a press on top of a pointed answer makes it a hand-finished one. Those two kinds must never be pooled — a hand-placed answer is an eye's estimate and a pointed one is the reader choosing which ink with the extent taken off the outlines, so mixing them reports a tightness that says nothing about whether a reader can see a misplaced rectangle — and the transcript can only be split apart afterwards if the page said which. A tap steals no scroll: it is recognised on the way up, by having gone nowhere, off-rect pointer-downs still return without capture, and the check runs before the move is settled so one gesture is never read as both — a drag that banked a placement is a placement, and pointing at ink on top of it would overwrite the reader's own hand with arithmetic they did not ask for. There is exactly one boundary between a tap and a drag now, ten screen pixels, and it was two boundaries with a clock beside them. A gesture counted as a tap only if it also finished inside 600 ms, and separately a drag banked a placement only once it had moved more than 0.05 page units — but ten screen pixels is about 1.5 page units at the framing that shows the whole word, so the pair left a gap thirty times the size of the floor, and the gesture that falls into it is the one a reader makes most: reaching for the ink under the rectangle. A finger resting there for three-quarters of a second with a few pixels of tremor was past the clock, so not a tap, and far past the floor, so a placement of nearly half a unit that nobody made. The clock went because it never asked anything worth knowing — a slow tap is still a tap, and only distance can say whether a finger stayed still; screen pixels rather than page units because a finger is the same size on every card and a page unit is not, and the fine tool is the pad, from a hundredth of a unit up. Transcripts banked before the fix carry whatever it produced and cannot be cleaned of it afterwards: a phantom placement is a well-formed placement. A tap that reaches no ink at all now says so, in the standing instruction under the picture — repainting and saying nothing is indistinguishable, from the reader's side, from a tap the page never received, so a finger a hair off the stroke and a dead control looked the same, and after two of those a reader stops pointing at ink and goes back to the pad, at which point the answers stop being about the ink. Choosing nothing is a retraction rather than a placement back at the origin, and the pending burst is dropped instead of flushed so no bank-and-retract pair appears in the transcript for a gesture the reader undid. The chosen ink is tinted, and that is the reader's own statement rather than our guess: lighting the *mark's* ink stays ruled out, since where that ink is, is the unknown the sitting exists to measure. Its colour joins the never-re-themed set for the same reason the paper and the ink are in it. And the tint is rebuilt from the standing answers rather than remembered, because the rectangle already is, and remembered separately the two part company the first time somebody takes an answer back — ink still lit under a rectangle that has gone home." + }, + { + "file": "packages/etl/scripts/serve-sittings.mjs", + "symbol": "function answersFor", + "note": "What makes an answer survive the browser losing it. The sittings were served by a static file server, which can hand out a page and do nothing else, so every answer lived in one browser's own store and nowhere else — unbanked until the reader reached the end of a hundred-and-seventeen-card sitting and pressed save, and gone for good if that browser ever cleared its data. Somebody nine answers in asked how to submit them one at a time and watch the count come down; both halves of that need somewhere to post to. It writes one line per answer, appended and flushed per write, to `out/mark-answers.jsonl`, and that file is also what makes the remaining count fall, since the builder's `--answered` drops every mark carrying a standing answer. Append-only because what is being defended against is losing a scarce person's attention: a closed laptop costs one line, a rewritten document costs the file. **It replaces the static server rather than joining it, and that is the load-bearing decision.** Browser storage is per-origin, so a sitting begun at one address cannot be finished at another — put the reader on a different port and every answer they have already given becomes unreachable, silently, with the page cheerfully starting again from card one. Same host, same port, same path, or the fix costs more than the bug. Which is why the address is no longer whatever the caller happened to type: it comes from `lib/tailnet.mjs`, and what is printed on start is one address with the others named as the ones not to use. The default it binds is the private network rather than loopback — a server only this machine can reach is no use to somebody holding a phone, and that default being absent while the flag to override it existed meant every sitting on a phone depended on somebody remembering an incantation written down nowhere. It is deliberately not every interface: there is a write endpoint on this thing, and the network at a café is an interface too. `answersFor` is the other direction, and it exists because the browser store turned out to be the least dependable thing in the chain: the tailnet *name* and the tailnet *address* are two different memories of the same sitting, and a browser may throw either away whenever it likes. This machine has had every answer since the first one, so it hands them back — `banked`, the reduced snapshot a hand-over wrote with retractions already applied, and `log`, the raw append-only stream with retractions in it as their own lines. They are two different kinds of record and the page merges them; filtering is deliberately left to the page, because one log covers every sitting and only the page knows which marks are its own. Each file is read fresh per request, so a rebuilt sitting is live without a restart — which matters more than it sounds, since restarting mints a new token and every page a reader already has open would quietly stop banking. The token is not a secret and is not trying to be: bound to one interface on a private tailnet, it only means a page that was not served from here cannot post." + }, + { + "file": "packages/etl/scripts/serve-sittings.mjs", + "symbol": "function sittingsOnDisk", + "note": "What lets the front door stop being a photograph of the day it was generated. Until this existed the serving side could hand back a reader's answers and nothing else, so the only way a page could know which sittings existed was to have been built out of them — which is why a sitting somebody finished kept its tile, and why a re-deal left the page describing parts that no longer held those marks. It walks the directory on every request rather than holding a listing, for the same reason every other route here reads its file per request: the parts are rebuilt while this is running, routinely, and a server with its own idea of what is on the disk would just be the stale thing instead of the page. It reads each one through `lib/sitting-file.mjs`, the shared reading, so the builder, the auditor and this cannot disagree about how many parts there are; a file that is not a sitting comes back null and is skipped, the output directory being full of other generated pages including the front door itself. It hands back the header, the card ids and any faults and works nothing out — the census belongs to `lib/sittings-view.mjs`, which runs in both runtimes, and a server that did the arithmetic would be a third opinion about it." + }, + { + "file": "packages/etl/scripts/lib/sittings-view.mjs", + "symbol": "export function census", + "note": "The front door's arithmetic and its wording, written once and run twice — in Node to bake a page that is true when it is written and readable with nothing serving it, and in the browser against whatever is on the disk now. Everything here was originally inline in the builder, which was fine while the page was a photograph and became the problem the moment it had to redraw: the alternative to a module is the generated script restating the counting, and a second statement of *how far has this sitting got* is right until the first time somebody changes one copy. **Every function here is closed over nothing but the others** — no imports, no module constants, no shared tables; `howLong` takes the seconds-per-mark estimate and `bandCopy` takes the table of band titles rather than reading either, because a binding at module scope reads perfectly here and is undefined in the page, on a screen nobody looks at until they are holding a phone. That rule is checked rather than asserted: a test re-evaluates every export in a scope with nothing in it and makes it agree with this one. Two things deliberately live elsewhere. The editorial half — what *barely accepted* means — belongs to whoever is explaining the plan, not to the counting. And what to do about parts from two different builds is left to the caller: this reports the disagreement and decides nothing, because the builder refuses to write a page at all and the page says so and carries on, which are both right. One correction worth keeping: the sentence about whether anybody has checked the confidence number counts standing answers rather than reading the already-answered figure off a band's header, that figure being how many marks had been answered when the band was built — a fact about the deal and not about the band, quietly answering a different question than it was asked." + }, + { + "file": "packages/etl/scripts/build-sittings-index.mjs", + "symbol": "function draw", + "note": "The front door to the sittings, and the reason it is a script at all. There was a front door already and it was typed by hand, which meant it was right on the day it was typed and wrong every day after. On 2026-08-15 it was telling a reader that 26 marks were answered and 1,851 were left, cut into sixteen sittings of about 116; the true numbers were 167, 1,710, and 106 or 107 apiece, and the sittings behind the very links on that page had been rebuilt twice since. Nobody noticed for a day, and nothing on the screen could have said so. That is the shape of the mistake rather than a typo: the count is the *only* reason to open this page instead of the sittings directly, and a reader who is told 1,851 budgets sixteen hours against a number a day out of date. So every number here is counted out of the sittings themselves. `build-mark-report.mjs` already writes a `HEAD` block into each page it emits — the population it drew from, how many were already answered and left out, how many it is showing, the fingerprint of the deal — and this reads those back through `lib/sitting-file.mjs`, the one reading of a built sitting, shared with the auditor so the two cannot disagree about how many parts exist. The page therefore cannot disagree with what it links to, because it is built out of it; rebuild the sittings, rebuild this, and the two move together. A sitting missing from disk is simply not listed, a link to a page that is not there being worse than no link. **Working the whole thing out again in the browser is the second half, and it exists because everything above is true only at the moment of the build.** A rebuild drops every answered mark, so at build time every part is untouched and every bar reads zero — the one number a reader cannot use, since what they want from a front door is *which part am I in the middle of*. Baking the rest was worse: a sitting somebody finished kept its tile and looked exactly as unfinished as the fifteen beside it, and a re-deal left the page describing parts that no longer held those marks. Both were correctable only by somebody remembering to regenerate, and automating the regeneration would only have moved the forgetting. So the serving side grew a listing that reads the directory each time it is asked, and the page redoes the whole census against it — which sittings exist, how far each has got, every total, every sentence, and the part to carry on with: the one begun and unfinished, because a part left half-done is the only place the re-deal cannot help. Redone rather than patched, so that no version of the page is half of one census and half of another. Answers in the log for marks in no part on the page are ignored rather than added — one log covers every deal there has ever been, and counting those again would take the remaining figure below zero. Finished sittings fold away rather than disappear, because a front door that silently drops rooms cannot be checked against the directory behind it. None of that arithmetic or wording is restated in the generated script: it lives in `lib/sittings-view.mjs`, written closed over nothing for the same reason `standingIds` is, and this ships those functions' own source text into the page — one reading, two runtimes, with a test pulling the copies back out, evaluating them with nothing in scope and making them agree with the modules they came from. The baked half is kept for the case where nothing is serving: opened from a file, the page still shows the numbers that were true when it was written. The page also bakes in the one address it is meant to be read at and says so on screen when it is being read at another spelling of the same machine, since a browser keeps a reader's place per origin and the two spellings are two memories of one sitting. Three things it deliberately does not do. It does not read the prose describing each band of confidence out of the data — *barely accepted*, *where nine marks in ten live* are editorial and belong to whoever is explaining the plan. It does not average the population, the already-answered and the pool across the parts: those are properties of the DEAL, so they are read off the first part and every other part is checked against it, and parts from two different builds are refused by name rather than added up, that being the one state where an index would quietly mislead about how much work is left. And it stamps the page with the newest sitting's build time in LOCAL time, not UTC, because the reader compares that line against a file listing on the same machine and a stamp five hours off the listing reads as a stale page when the page is fine — which is the exact confusion the line exists to prevent." }, { "file": "packages/etl/scripts/score-mark-nudge.mjs", - "symbol": "function meanCI", - "note": "Turns placements into a distance. Prints the headline in the forced choice's own currency — how often a landing fell nearer our correction than the shipped box, with a Wilson interval — so the two instruments are comparable and a disagreement between them is legible as a finding rather than as arithmetic. Then the part only this can produce: the residual per axis with a normal interval (a mean of continuous displacements, not a count of successes — the two interval shapes are not interchangeable at these sample sizes), and the reader's own precision from the marks shown twice, halving the variance because two independent placements differ by √2 times one placement's wobble. A residual inside that precision is reported as *not distinguishable from the hand*, which is a result and is not the same as the correction being right. The pull of the starting point is regressed and printed rather than corrected for: correcting would be assuming a model of the hand, and this is the instrument that measures one. Refuses (exit 2) on a fingerprint mismatch or on a file that is not a placement session." + "symbol": "const MUSHAF_PAGES", + "note": "Turns placements into a distance. Prints the headline in the forced choice's own currency — how often a landing fell nearer our correction than the shipped box, with a Wilson interval — so the two instruments are comparable and a disagreement between them is legible as a finding rather than as arithmetic. Then the part only this can produce: the residual per axis, and the reader's own precision from the marks shown twice, halving the variance because two independent placements differ by √2 times one placement's wobble. A residual inside that precision is reported as *not distinguishable from the hand*, which is a result and is not the same as the correction being right. The pull of the starting point is regressed and printed rather than corrected for: correcting would be assuming a model of the hand, and this is the instrument that measures one. Refuses (exit 2) on a fingerprint mismatch or on a file that is not a placement session. **Three things a reader must carry away before quoting any number it prints.** Placements are NOT independent: two marks on one page share that page's frame, so every residual gets a page-clustered interval printed beside the naive one and the verdict at the foot reads the clustered one — the first sitting banked a residual as distinguishable from nought on an interval that clustering puts across zero. A preference is not a distance and a distance is not a SIZE: the gain is regressed and printed with the spread of the proposed moves next to it, because that spread is what says whether the estimate could ever have meant anything, and on 40 nearly-identical pages it could not. And it prints coverage first — how many of the mus'haf's 604 pages the correction covers, and whether any placement was made on a page outside them — because every placement so far came from a page the correction was fitted to, which is the largest limit of the whole exercise and appeared nowhere in its own output for a day. The negative results are printed too (not the mark's name, not a stretch, not fatigue), so nobody pays to rediscover them. The coverage line now distinguishes three cases rather than one, because an in-sample check and a held-out check are no longer the same thing: placements on pages the correction was fitted to, placements on pages deliberately held out of it, and the impossible case of a placement on a page with no measurement at all. And it **replays** the page list the session recorded instead of re-deriving it: the trial list is rebuilt from the seed, so a session narrowed at build time and scored unnarrowed would line every index up against a different mark and throw nothing — every residual would be a subtraction of the wrong two numbers. Two guards, not one: the displacement fingerprint, and then a check that the file handed over actually has a row for every page the sitting used, because a file can carry the right fingerprint and still be the wrong file for these pages. Everything it prints from one sitting has two readings it cannot separate — *the print is out by this much* and *this reader puts rectangles this way* produce the identical number — so `--against` takes a second person's sitting on the identical build and reports how far the two hands are from each other against how far each hand is from itself. The difference is taken mark by mark and not average against average, since two hands a fifth of a unit apart in alternating directions have identical averages and could not be further from agreeing. Agreement inside their own wobble is what would make a further correction something you may apply to all 604 pages; a wider gap means it belongs to whoever was sitting there and may be applied to nothing. It prints a comparison and no threshold, and it refuses (exit 2) a second file from a different build, or the same reader twice." + }, + { + "file": "packages/etl/scripts/score-mark-report.mjs", + "symbol": "const FAULTS", + "note": "Reads a sitting of the five-word page, and it is built around four things it refuses to do. **It will not pool the two populations.** Marks placed from their own ink and marks fallen back to the printed line are two different options being decided between, and a single rate over both is weighted by whatever mix the sampler happened to draw — a fact about the sampler wearing the clothes of a fact about the mus'haf. Every number is printed once per population and never once overall. **It will not believe the transcript about which population a mark is in.** The split is re-derived here from the displacements, by re-running the builder's own predicate — at or above the overlap floor, and not sitting on the edge of the search window — and a transcript that disagrees is refused by name rather than read: an answer filed against the wrong option is worse than a missing one, because it looks like evidence. A transcript written before the population was recorded at all is repaired exactly and *said to be repaired* in the output, which is the only honest version of filling something in. **It will not read a sitting against different displacements**, the same fingerprint guard the other two scorers carry, and carried as its own copy on purpose: a shared helper would let one edit silently re-bless every ruling ever banked. **It will not count a defect in the print as our error.** *Odd in the print* comes out of the denominator rather than into the numerator, the rate is printed both ways so the size of that choice is visible, and the marks are named with a pointer at `docs/issues.json`, which is where a fault in vendored data belongs. The denominator is `seen`, the reader's high-water mark, because silence is the verdict on this page; a transcript that predates the field gets explicit bounds instead of a guess, and the bounds are usually wide enough to be their own argument for re-sitting. It exits 2 only when it refuses to read a file, never for bad news — a scorer that fails the build when the sitting goes badly teaches everybody to stop sitting them. The closing paragraph is not decoration: sixty marks cannot tell 2% from 14%, and the interval rather than the point estimate is the finding. **And it will not median an increment.** Where a reader corrected a rectangle it prints one row per mark — the last placement that carries a running total — because the route a rectangle took is not the reader's statement and its resting place is. Medianing the increments instead did two things at once: a reader settles a box by pushing it one way and a little back, so opposite-signed nudges cancel, and one mark nudged forty-four times outvotes twenty-five marks moved once each. On a banked sitting that printed 0.000 across and 0.000 down for twenty-six marks every one of which had been dragged the better part of two units. Then it prints two distances under two separately worded sentences, and says in the prose that they are never differenced. The reader's *hand* is the total minus the correction this pipeline had already applied, and it answers how wrong our rectangle looked to somebody sitting in front of it. The other is measured from the uncorrected box, ours already in it and theirs on top, and it is the one that may be set beside a measurement taken off the ink, because that is measured from the same uncorrected box. The gap between the two is only the correction we already applied, so a reader who subtracts one from the other learns nothing while believing they have found a discrepancy. It also says how many marks and how many nudges those marks took, since a ratio far above one is a finding about the pad rather than about the print — a rectangle needing several goes to settle is one the controls are not letting anybody place. Distances and counts are never combined: a mark called the wrong shape has no distance at all, and a mark moved a hundredth of a unit is not the same finding as one moved a whole mark-height." + }, + { + "file": "packages/etl/scripts/score-mark-report.test.mjs", + "symbol": "describe(\"what it refuses to read\"", + "note": "Sixteen tests, and the only file in this package that runs a script instead of importing a module. That is deliberate and it is the whole design: what is worth holding about this scorer is not an estimator but its four refusals, and all four live in the top-level flow where a `die()` ends the process. Extracting them into a lib to make them importable would leave the tested copy and the running copy as two different things, which is exactly the failure the tests exist to prevent. Every one of the mistakes they guard against produces a *plausible* number rather than an error — answers about marks that were never on the screen, answers about one placement rule filed against the other, a defect in somebody else's print counted as ours — and a wrong percentage looks precisely like a right one once it has been quoted into a register. Forty invented marks on one line of one page, twenty-four of them in the ink population by construction and sixteen out of it, and the sixteen are split between the two ways out on purpose: fifteen whose best match is unconvincing, and one whose match is excellent but which reached the edge of the search window, so the true best may lie outside it and was never seen. That last mark is the entire reason the population test is two clauses rather than one, and it is load-bearing — collapsing the per-axis edge test back to a single radius, which is the error this repo has already paid for once, turns two of these tests red. The hash is written out here a fourth time from the algorithm rather than imported: the instruments each carry their own copy so that no single edit can silently re-bless every ruling ever banked, and this copy is what makes that arrangement checkable instead of merely stated. Vitest globals are imported explicitly, as in every `.mjs` test here." + }, + { + "file": "packages/etl/scripts/lib/mark-settle.mjs", + "symbol": "export function settle", + "note": "What one mark ended up saying, out of everything that was said about it — the one copy of that arithmetic, imported by both the scorer and the settler. A sitting writes events, not verdicts: a reader picks a rectangle up, pushes it left, overshoots, brings it back, decides the shape is wrong as well, stretches it, and moves on. That is six true lines about one mark and none of them is the answer. The answer is where it was when they let go, and reading it any other way has already cost this project a number — the first scorer medianed the presses, and presses cancel, so it printed *nothing moved* for twenty-six marks that had every one of them been dragged the better part of two units. The two obvious repairs fail in the two most convincing directions: averaging says a mark pushed a unit left and a unit right never moved, summing magnitudes says it moved twice as far as it did, and neither reads as an error. So: position and size both settle to the LAST event carrying one, because both fields are running totals rather than increments — which also means a grow-then-shrink collapses the same way a left-then-right does. Words gather and repeat once, since being called moved eleven times is one statement and counting it eleven times lets one stubborn mark outvote a page of easy ones. Retractions need no handling at all, and the note saying so is load-bearing: the sitting page splices a retracted answer out of the transcript rather than annotating it, so what arrives here is only what the reader still stood behind, and somebody will otherwise write the undo a second time. Feeding several sittings oldest-first makes the later look settle the row, which is the only ordering under which asking for a second opinion means anything. How many goes it took is kept but is not part of the verdict about the mark — it is a verdict about the controls, and a rectangle that takes nine presses to settle is one the pad is not letting anybody place. Moving and reshaping are counted apart because they are complaints about different controls with different repairs. `asDrawn` is the other half: it rebuilds the rectangle the reader was actually shown, since a transcript records what was said and never what was on the screen, and if the scorer and the settler rebuilt it differently the same sitting would settle two ways. Its population test is per axis, not per distance — the search window is a square, so a mark whose best match sits against the edge of it may have a better one just outside that was never looked for — and collapsing that back to a radius is an error this repo has already paid for once. `isFault` deliberately reads called-fine-then-moved as a fault, because somebody who says a thing is fine and then changes it has told you it needed changing; the affirmation stays in the row so the contradiction is visible rather than resolved out of sight. `byMark` sorts numerically on both halves of the id, since string order puts page 100 before page 19 and anybody checking a ruling against the print walks it in the print's order or gives up." + }, + { + "file": "packages/etl/scripts/lib/mark-settle.test.mjs", + "symbol": "describe(\"moving a rectangle about and leaving it somewhere\"", + "note": "Nineteen tests, most of them one shape asked several ways: a rectangle pushed about and left somewhere settles WHERE IT WAS LEFT, whatever route it took. The fixtures are built so both wrong answers are visibly wrong rather than merely different — three presses whose average is (−0.167, −0.067) and whose magnitudes sum to 2.5, resting at (−0.5, −0.2), so a version of the module that averages and a version that sums both go red on the same line and neither can be mistaken for a rounding difference. The grow-then-shrink case is there because size is the same kind of field as position and gets the same treatment, and it is the half a reader is most likely to forget when they extend this. Also: eleven presses is one word, not eleven; notes survive whole and unrepeated, since a note is the only thing in a sitting that was not chosen from a list of six; blank notes are dropped; anything that is not an answer about a mark is skipped rather than crashing the run. The last group tests `asDrawn` on three constructed rows — convincing with room, convincing against the window edge, and unconvincing — plus a fourth that is against the edge on the *other* axis, which is the test that fails if the per-axis check is ever collapsed back into a distance." + }, + { + "file": "packages/etl/scripts/settle-mark-report.mjs", + "symbol": "\"mark-settled\"", + "note": "Turns the sittings somebody has sat into a ruling in the repository — one row per mark, checked in beside every other ruling. It exists because a summary in a terminal is not a record: it cannot be diffed, it cannot be quoted with a line number, and the next person asking what we actually concluded about page 19 has to re-run a command they do not know exists against a transcript that is gitignored. An hour of the scarcest thing this project has should not survive only as scrollback. Each row carries two distances and they are never differenced: the reader's own `hand` — how far they moved the rectangle *we drew them*, which is how wrong our correction looked to somebody sitting in front of the print — and `to`, measured from the uncorrected box, which carries our correction plus theirs and is therefore the one to set beside a measurement taken off the ink, because that one is measured from the same uncorrected box. The gap between them is only the correction already applied, and reading it as two instruments disagreeing has already misled somebody here. The resting rectangle is written out whole, so no reader of the file ever adds two offsets together to picture what happened. Sittings are sorted by when they were handed over, so a mark looked at twice settles on the later look; the head records every file that went in, with what each claimed about itself. Two numbers are kept per sitting rather than one, and the pair is the scar: `seen` is what the page claimed and `looked` is the floor the file can actually support, since you cannot say something about a mark you never looked at. The page got that wrong once — it banked what was *left* rather than what had been *seen* — and nothing caught it until the rates it fed went past a hundred per cent, so both are written down and a warning names the file when they disagree. Same two refusals as the scorer, and the same reason: a sitting read against different displacements is a set of statements about marks that were never on the screen, and a transcript disagreeing with the displacements about which rule drew a mark would file answers about one option against the other. `--issues` drafts a register row for the marks called odd in the print and PRINTS it rather than writing it — `docs/issues.json` is hand-edited always, and a script that edited it would be the second thing in this repo claiming authorship of a register meant to have exactly one. It exits 2 only when it refuses to read a file, never for bad news: a sitting that finds everything wrong has done its job, and a build that fails for it teaches everybody to stop sitting them. When every settled mark does carry a complaint it says so and says to check that this is not a measurement of the sitting — a population selected for being hard produces that exact shape. **And it reads the running log, which for a while it did not.** There are two routes an answer takes out of a sitting — banked by the serving side as it is given, or written into a file when the reader hands over — and the builder has always honoured both, so a mark answered either way drops off the screen for good. The settler read only hand-overs, which meant an answer banked and never handed over was taken away from the reader and left out of every ruling: on the first sittings over the fallen-back marks that was twenty-five marks of somebody's work, done and visible nowhere. So a `.jsonl` log may be passed alongside the transcripts. The same statement usually arrives by both routes and it is one statement rather than two — copies are matched on their whole content and counted once, because how many goes a mark took is a finding about the controls and doubling it is a made-up finding. Everything is then ordered by when it was said: a banked answer by its own stamp, a handed-over one by the hand-over that carried it, which is the latest it can have been said. That ordering is what makes the last thing the reader did the last thing this reads, and with no log passed it reproduces the old behaviour exactly. The log is **not** filed as a sitting — it is the same hours arriving by the other route, and listing it among the sittings would double every denominator — so it gets its own line in the head saying how many statements it held, how many of them reached no hand-over, and how many marks are in no hand-over at all. A log carries no head and therefore no fingerprint, which is why it may not be settled on its own: with no transcript beside it there is nothing to check it against, and it is refused in those words. What does check it is the per-answer form of the same guarantee every transcript gets — each answer looked up in the displacements, and refused if the mark is not there or was drawn by the other rule." + }, + { + "file": "packages/etl/scripts/settle-mark-report.test.mjs", + "symbol": "describe(\"the two distances, which are two different questions\"", + "note": "Twenty-four tests over everything the settler does *around* the settling rule, which is held next door. The displacements in the fixture are deliberately not round — every mark is drawn half a unit across and a quarter up from the box we ship — so the reader's hand and the distance from what ships are different numbers by construction rather than by coincidence, and an implementation that measures both from the same origin cannot pass by accident. The rest is the ways a ruling comes out plausible and wrong: a denominator smaller than its own numerator, raised to the floor and said out loud; two sittings handed over in the wrong order, sorted before the later one settles the shared mark; marks walked in the print's order rather than the string order; a defect in somebody else's print counted apart from a complaint about our rectangle even on a mark carrying both. The register draft is asserted to be printed and never written. And two tests hold the exit code at zero on bad news, including the all-wrong sitting, because that is the property that decides whether anybody sits another one. Runs the script rather than importing it, the same choice the scorer's tests make and for the same reason: the refusals live in the top-level flow where a `die()` ends the process, and lifting them somewhere importable would leave the tested copy and the running copy as two different things. The hash is written out a fifth time from the algorithm, which is what makes one-copy-per-instrument checkable instead of merely stated. Every run writes into a scratch directory, so a test can never lay down a real ruling. The last group is about the answers that were banked as they were given rather than handed over: a mark no hand-over ever carried still settles; a statement arriving by both routes is counted once, so the mark keeps the true number of goes it took rather than twice that; an answer banked after the hand-over is the later look and settles the row; the log stays out of the list of sittings so no denominator counts the same hour twice; a log with no hand-over beside it is refused for having no head to check it against; and a banked answer is checked against the displacements exactly as a handed-over one is." + }, + { + "file": "packages/etl/scripts/lib/answered.mjs", + "symbol": "readAnswered", + "note": "Which marks carry a standing answer, and what that set is called — one reading of the word *answered*, imported by everything that needs it. It lived inside `build-mark-report.mjs` until the auditor needed the same question answered, and the tempting thing at that moment — reading the answers a second time, slightly differently — is exactly what this module exists to prevent: two readings drift, and the drift surfaces as a mark the builder drops and the auditor still counts, with neither obviously wrong and nothing on screen saying so. Two shapes arrive and both are legitimate, since the serving side appends one line per answer as it is given while a reader who finishes hands over a single document, and nobody should be punished for having banked their work one way rather than the other; they are read in any mix into one set. It counts rather than collects because a retraction takes a mark back out — a reader who says something and then unsays it has not answered, and the log keeps both statements because it is a log. `answeredKey` is the part of a sitting's identity that the answered set contributes, sorted before hashing because the same marks answered in a different order are the same sitting and a name that disagreed would rebuild every part for nothing. `fingerprint` is the eight-character guard every instrument here carries; nine other scripts still hold their own copy of it, and consolidating those is a separate decision from where this one lives, because changing the function changes every sitting's identity at once. The extraction was checked by rebuilding a part and diffing: byte-identical." + }, + { + "file": "packages/etl/scripts/lib/sitting-file.mjs", + "symbol": "export function readSitting", + "note": "One reading of a built sitting, for the same reason `answered.mjs` exists and with a worse failure if it did not. A sitting is one HTML file with two JSON literals in it, which is not a format but is deliberately the format — the whole point of the instrument is that a part is a single file a reader can open anywhere with nothing installed, and parsing it back is what that costs. It is cheap, because the builder emits both literals on one line each, which is a constraint on the builder that its own tests hold it to. Three things now ask a built sitting what it is — the auditor, the front door, and whatever comes next — and they were about to ask it in three slightly different ways: a reader of the header that tolerates a shape the others reject hands one caller sixteen parts and the other fifteen, and the missing part is invisible in *both* reports, which is strictly worse than a disagreement anybody could see. Faults are returned rather than thrown or printed, because only the caller knows which it wants: the auditor wants the run to fail, and a front door that refuses to render because one of twenty-one files is torn is worse than one that lists the twenty and says which it could not read. A file that is not a sitting at all comes back null rather than as a fault — the output directory holds plenty of other generated pages and both callers walk it whole." + }, + { + "file": "packages/etl/scripts/lib/tailnet.mjs", + "symbol": "export function canonicalAddress", + "note": "The one address a sitting is reached at, and the reason there has to be exactly one. A reader's place and every rectangle they have moved are kept by the browser against an *origin* — scheme, host and port, with the host half compared as text — so this machine reached by its private-network name and this machine reached by its private-network number are two separate memories of the same sitting, with no way for either to see the other and nothing on screen that says so; the page simply opens at card one as though the last hour had not happened. That is not hypothetical, it is the failure that made the serving side hand every answer back on request in the first place, and the recovery works but is still an hour of somebody's confidence. The fix is upstream of the recovery: print one address, always the same one. Everything that tells a reader where to go asks here rather than composing a URL of its own, so there is exactly one answer on this machine at any moment. The address is read off `os.networkInterfaces()` rather than from a command, because a private network of this kind hands each machine an address in the range reserved for carrier-grade translation — 100.64.0.0/10 — and no ordinary home or office network issues one, so an interface holding an address in that range *is* that network; it needs nothing installed and cannot be wrong about whether the network is up. The **name** is nicer to type and cannot be read that way, so it is asked of the control program where one is installed, quietly, failing to null on all four ways that can go wrong — not installed, not running, not logged in, changed output — because every one of them means the same thing to a caller. The name is preferred for printing precisely because it is the spelling already in use, and switching a reader to the other one is the exact move this file exists to prevent. `alternates` is everything else this machine answers to, and it is there so callers can say *do not use these* rather than leaving a reader to discover the rule by losing an hour to it." + }, + { + "file": "packages/etl/scripts/audit-mark-sittings.mjs", + "symbol": "const deals", + "note": "Asks whether the sittings on disk are the ones anybody should still be sitting, which until 2026-08-17 was the only step of the review routine with no command behind it — *confirm the deal did not move* — and therefore the step nobody did. Both failures it catches are invisible from inside a part. A rebuild skipped, or run with one handed-over transcript left out of the list, leaves parts that still open and still count down and re-ask questions somebody already answered, on pages indistinguishable from pages they have not seen. A rebuild against a different measurement leaves every rectangle drawn from displacements that are not the ones on disk, so every answer is about a picture nobody can reconstruct afterwards — worse, because those answers are wrong rather than merely wasted. Neither needed a person: a built part already says what it was built from, since the reader's place is stored under a key made of the measurements' fingerprint, the set, the slice and the seed, with the answered set folded into the slice so that shrinking the pool cannot strand somebody at card ninety of a hundred and seventeen. It announces itself to anything that asks; nothing asked. So this reads each part back — through `lib/sitting-file.mjs`, which parses the two one-line literals the builder emits and is shared with the front door, because a third reader of that header would eventually tolerate a shape the others reject and hand one caller sixteen parts and the other fifteen — and groups them into *deals*, one shuffle dealt into parts, because two deals in one directory are not a fault (the five confidence bands are separate small sittings on purpose) while a gap inside one deal is. Then: every part of the deal present exactly once, one measurement across all of them and it is the one on disk, every part built against the same answers and those the answers on disk, nothing already answered asked again, no mark in two parts, and the count the reader is shown adding up. `--answered` is required to be the same list the rebuild got and is deliberately not defaulted — an auditor that quietly reads the running log and nothing else calls a sitting stale whenever a hand-over exists, or fresh whenever the rebuild used a file it did not, and both verdicts are confident. It prints what it read for that reason. **It is not a gate and will not become one**: the parts are build products and the answers accumulate on whichever machine served them, so in a clean checkout it would pass by being unable to look, which is the same reason the published-page sweep is not called one either." + }, + { + "file": "packages/etl/scripts/audit-mark-sittings.test.mjs", + "symbol": "describe(\"the rebuild that never happened\"", + "note": "Eleven tests, and the reason they exist in this shape is that an auditor which only passes the clean case is indistinguishable from one that always says yes — so each failure is built on purpose: an answer no part knows about, a mark already answered asked again, half a deal rebuilt and half not, a part drawn from other measurements, a part missing entirely, one mark dealt into two parts, a total that does not add up. The fixtures are the two lines a built sitting says about itself and none of the megabyte of cropped artwork around them, because the artwork is not what is audited and forging it would only make these rot whenever the page changes. Two tests hold what it will *not* do: a confidence-band sitting is counted rather than failed for not being part of a deal, and an empty folder says nothing has been built rather than passing." + }, + { + "file": "packages/etl/scripts/lib/placement-stats.mjs", + "symbol": "clusteredCI", + "note": "The estimators behind a placing session, split out of the scorer for one reason: an interval is the only thing in that output nobody can eyeball for correctness, and a bracket half its true width reads exactly like a bracket the right width. `clusteredCI` is the ordinary cluster-robust standard error — sum deviations within each page, then treat those totals as what varies, with the `G/(G−1)` finite-cluster loading — and it exists because `probe-mark-ink.mjs` had said in as many words, since it was written, that two marks on one page are not two facts, and the lesson never travelled to the scorer. `slopeOf` is the same sandwich for a line, used three ways (is the move the right size · does the residual depend on where the mark sits · does it drift as the sitting wears on), returning `null` when the regressor does not vary — itself the answer worth printing. `spreadUnderSplit` answers the question that follows every session: does splitting by some label explain anything, or only spend degrees of freedom? `many >= one` means do not split, and it is what killed an early read that one mark name drove the residual." + }, + { + "file": "packages/etl/scripts/lib/placement-stats.mjs", + "symbol": "agreementOf", + "note": "How far two hands are from each other, and the only estimator here that compares people rather than describing one. It takes the difference **mark by mark** — an average against an average is the tempting version and it is wrong, because two readers who sit a fifth of a unit apart on every rectangle in alternating directions have identical averages and have not agreed about anything. Per axis it reuses the page-clustered interval, for the same reason everything else here does. The scale it reads the gap against is not a threshold somebody chose: it is the two readers' own measured wobble, from the marks each of them was shown twice, added the way independent wobbles add. If either hand repeated nothing, that scale does not exist, and it says so rather than inventing one — an unmeasured wobble is not a small one, and calling it agreement would be the same overstatement this whole file was split out to stop." + }, + { + "file": "packages/etl/scripts/lib/placement-stats.test.mjs", + "symbol": "function perfectlyClustered", + "note": "Twenty tests where the truth is a matter of construction rather than measurement, because the failure being defended against already happened once: ten pages of six identical values must produce a clustered interval about √6 wider than one that counts every placement as its own fact, and the same estimator must agree with the naive one when nothing shares a page. Also that `clusteredCI` declines rather than guessing on a single cluster, that `slopeOf` recovers a planted slope and widens its interval when the points cluster, and that `spreadUnderSplit` says a meaningless label explains nothing and a real one explains a great deal. Two of them are about the second reader and both are constructed to fail the obvious implementation: two hands whose disagreement alternates in sign have identical averages, and must still be called a disagreement; and two hands where either one repeated nothing must come back as *cannot say*, never as agreement." }, { "file": "packages/etl/scripts/lib/adjudication.test.mjs", @@ -1193,7 +1348,7 @@ { "file": "scripts/session.mjs", "symbol": "SHIM", - "note": "The writing surface: one check, served to the phone, every tick and note appended to a transcript as it happens rather than typed from memory at the end. This is the opt-in a check's own tool gets — inject a sink, and a page built for the check posts to it; opened as a plain file it finds nothing and keeps its offline behaviour. Every /api route is token-gated because the server binds 0.0.0.0 for the phone, and these files are treated as evidence." + "note": "The writing surface: one check, served to the phone, every tick and note appended to a transcript as it happens rather than typed from memory at the end. This is the opt-in a check's own tool gets — inject a sink, and a page built for the check posts to it; opened as a plain file it finds nothing and keeps its offline behaviour. `--tool ` overrides the page the ledger pins, which is what lets a check whose population was cut into sixteen parts serve any of them through the sink instead of off a static file server; the transcript records which page it was opened against. Every /api route is token-gated because the server binds 0.0.0.0 for the phone, and these files are treated as evidence." }, { "file": "scripts/lib/session-log.mjs", @@ -1234,6 +1389,16 @@ "symbol": "decisions", "note": "The register. Fifteen records already existed in docs/decisions/ with no way in: their titles are sentences — 'The grain gets finer where the finger already is' is a good title and a useless index entry — so a reader arriving with a question could not find which file answers it. The running-table habit existed and was scoped to one topic (desktop-vs-mobile.md says 'append a row there rather than restating one here') and nothing generalised it, so an open decision could sit in a design doc's eighth section for a month with nothing counting it as open. Stores exactly one sentence of its own, the plain-language `question`, and points at the record for everything else — same anti-duplication rule as issues.json and the ledger. The exception is `related`, which is not a restatement of anything: relatedness is a property of a PAIR, so no single record can hold it, and the second half of a pair is normally written months after the first — by which time the first is not the file anybody opens. Hand-edited, never generated." }, + { + "file": "scripts/stage-docs.mjs", + "symbol": "rewrite", + "note": "The tenet that a design is public or it is not published, as a build step. Runs at the end of the web build and copies every docs/**/*.html (not the .artifact.html inlined copies) into dist/docs at the same path, so a merged page is on the site at an address derived from nothing but its path. Rewrites two kinds of relative link on the way — ../../apps/web/public/assets/… to where the site serves the same bytes, and a markdown record to where the repository host renders it — copies a checked-in file under docs/ that a page links (a screenshot) beside it, and fails the build on any other relative link, because a link that renders as a link and opens as a 404 is the failure this exists to end. Writes the front door, dist/docs/index.html, from the decision register and each page's own ; nothing on it is typed. vite.config.ts denylists /docs/ from the service worker's navigation fallback, or an installed app would hand a reader the shell in place of the page." + }, + { + "file": "scripts/site.mjs", + "symbol": "publicUrl", + "note": "The site's address, once. Every open decision's public link is this plus the page's path, and gate-decisions refuses a row that says anything else — before 2026-09-01 the address was minted on another host and written back here by hand, or not." + }, { "file": "scripts/gate-decisions.mjs", "symbol": "hasArtifact", @@ -1254,6 +1419,11 @@ "symbol": "gate-decisions.mjs", "note": "Scoped to the staged files, silent when a commit touches none of them. This pairing breaks on exactly one kind of commit — the one that adds the artifact link before committing the page, or renames the page and leaves the link — and that is the commit where the information is free, because you are moving the thing right now." }, + { + "file": ".githooks/pre-commit", + "symbol": "render-docs", + "note": "The unscoped half: the use-cases, issues and tasks gates run whole on every commit, because a rendered page's hash is a function of the register and of the documents it reads titles from, so staleness cannot be predicted from the staged paths. Added 2026-09-01 after a commit edited docs/use-cases.json, passed the scoped check, and shipped with the page stale. Three hash comparisons, under a second; the message names `make render-docs`." + }, { "file": ".claude/skills/decide/SKILL.md", "symbol": "name: decide", @@ -1268,10 +1438,49 @@ "file": "scripts/build-palette-options.mjs", "symbol": "fanned", "note": "The second options page, and the one that shows what a generator is FOR: its central claim is one nobody can make in a paragraph — that eighteen editable colours do not fit on cream paper — so it builds the best honest eighteen it can, fanning each family's hue in lightness and a little in hue, and then counts what that costs. Answer: **18 pairs closer together than the closest pair in today's seven**, which is the argument against option C, generated rather than asserted, and it would move on its own if the palette moved. Deliberately shares build-mark-options.mjs's stylesheet verbatim — the two pages are the same kind of document about the same seven colours, a reader will arrive at one from the other, and two option pages that do not look like siblings read as two projects. Unlike its sibling it has NO `--extract` half: every number comes from the shipped shards, `rules.json`, `tokens.css` and `skins.ts`, so there is nothing to stale. The two measurements the record turns on are computed here and nowhere else: `shown` — the share of a rule's own ayahs in which its family ALSO won the ayah's single tint, 1.6% for the commonest madd — and `ambiguous`, the 620 ayahs (10.0%) whose winning family is carried by more than one rule, where an eighteen-colour palette has no non-arbitrary answer at all. `--artifact` emits the body only, without a document shell: the publishing host wraps what it is given, and a second <html> inside the first is discarded along with the <style> it was carrying." + }, + { + "file": "scripts/build-placement-options.mjs", + "symbol": "layerInk", + "note": "The third options page, and the one where the drawing does work no table can: the decision is about a displacement of roughly one unit on a mark 5.6 by 3.6, which is a number in a column and an obvious error in a picture. Every option is drawn twice on page 179 — a band of three printed lines at about phone size, and a ten-times close-up on the mark that option moves furthest — with `layerInk` drawing the measured truth as a dashed outline beside the filled box, so the reader sees the gap shrink from A to I rather than being told it does. Page 179 is chosen by measurement, not by eye: its four rungs (98.7 / 16.0 / 7.8 / 3.4%) sit at the corpus median on every one, and the footer says so, because an options page that quietly picked its best page is an advert. The letters are the RECORD's — A, B, F, G, I, H — not this page's own; numbering them A–F would have given one option two names depending on which document a reader opened, and C, D and E stay in the record where §⑩ names them. The ORDER is not the letters' order: I sits beside F because the two differ by one thing and the board's severity bar should collapse along the reader's eye, and H goes last because its number is not measured the way the others' are. The board's `grid-template-columns` is written by the builder from `OPTIONS.length` (7rem plus 50rem shared out) rather than hard-coded, because a hard-coded five silently pushed a sixth option behind a horizontal scroll nobody drags; the stylesheet keeps an `auto-fit` fallback for the same reason. Two crop rules were found by looking rather than reasoning and are load-bearing: the band is measured off its own boxes and cut through the white space between printed lines (a word box is as tall as its line, so a tight crop lets the neighbour lean in), and the close-up only considers marks at least half its window from either page edge, or it lands half off the paper. Every figure is the held-out half — fitted on one half of each page's marks, scored on the other — and the score is the residual against the same per-mark displacements the dashed outlines are drawn at. `--extract` needs the gitignored cache and a rows file; the render needs only committed bytes. Sibling stylesheet to build-mark-options.mjs, for the reason recorded there." + }, + { + "file": "scripts/build-crop-options.mjs", + "symbol": "COPIES", + "note": "The fourth options page, and the first to write BOTH copies from one pass — `COPIES` is that convention in two lines. Its predecessors took an `--artifact` flag, which meant the checked-in page and the published one were two hand-run commands and could disagree with nobody able to notice; they did disagree once, and the published page lost its subject entirely. Here the only difference between the two is where the print comes from: the checked-in copy points at the page SVG with a relative URL and stays 44 KB, the published copy inlines each leaf once into a hidden <defs> and <use>s it, because the publishing host blocks every external request and a relative URL there does not error, it silently draws blank paper. Both leaves can share one <defs> only because neither page SVG contains a <use>, a url(#…) or an href=\"#…\" — checked, and every `id` is stripped anyway so the two cannot collide. `sweep` is the argument: it walks every shipped adjacency shard and computes, for both sides of all 2,544 drawable pairs, how much of the rectangle the panel draws is the ayah it names (5,088 crops, mean 69.8%, 820 more neighbour than verse, worst 10.3% on 80:36) — recomputed on every build, so the page cannot drift from the tree the way a pasted figure does. Shares nothing with its three siblings' stylesheet on purpose: they argue about marks on a page, this one argues about the app's own panel, so it borrows the app's tokens instead. The specimen keeps the app's paper in BOTH themes — the printer's ink is a fixed dark that would vanish on a dark ground, and the page says so rather than letting it read as an oversight." + }, + { + "file": "scripts/build-decision-board.mjs", + "symbol": "arcs", + "note": "The register drawn rather than tabulated, and the only page that shows the whole of it at once. README.md answers 'where is the row I came for'; this answers the two questions people actually arrive with — what is still open, and what is this one leaning on — and the second is the one a table structurally cannot answer, because relatedness is a fact about a PAIR and a table has to pick one row to write it in. `arcs` is that fix: every `related` edge once (`r.id >= other` drops the reciprocal half the gate insists on), drawn as a curve between two dots on a time line, terracotta if either end is still open. Nothing on the page is typed twice — the question comes from the register, the answer-in-one-line is the record's own H1 read at build time for the reason stated beside decisions.mjs's titleOf(), and the counts, dates, arcs and option strips are all derived; if it ever disagrees with the register the register is right and this page has not been rebuilt. Undated decisions are PARKED to the right of a dashed break rather than given a position, because a time line that placed an unanswered question on a day would be the one lie this picture could tell. The month labels are placed below the deepest stack rather than at a fixed drop — four decisions share 25 July 2026 and a constant looked right until the day that got a fifth. Unlike the four options pages there is one copy, not two: it carries no page artwork and no external asset, so the published copy is the checked-in file. Its last section is the shelf — docs/artifacts.json, read the same way, drawn as cards with the pages that have no checked-in copy first." + }, + { + "file": "scripts/build-sitting-hosting-options.mjs", + "symbol": "specimen", + "note": "The fifth options page, and the first whose subject is the instrument rather than the app — where the checking sessions should live, one laptop on a private network or a published link. It is the smallest of the five and deliberately so: nothing is blocked behind the question, so the page says that in its third section rather than manufacturing urgency. `specimen` is the part worth reading. The decide skill's rule is that an option is drawn on real data at the size it would really be used, and here the real data is one card lifted out of a built sitting — which arrives carrying two fields of Arabic and paths already coloured `var(--paper)` and `var(--ink)`. Both are hazards. The Arabic is dropped at extract time and the render refuses to emit a page containing an Arabic codepoint at all, because this repo ships none; the two colour names are re-pinned on the card itself, because the page uses those same names as theme-flipping tokens and a dark theme would otherwise print black on black — the defect the sitting instrument was already fixed for once, reachable a second time through a name collision nobody would look for. `--extract` reads the sixteen built parts and the answer log out of the gitignored out directory and writes the findings; the default render reads only committed bytes. One copy, not two: the page carries no external reference, so the checked-in file is what gets published." + }, + { + "file": "docs/artifacts.json", + "symbol": "artifacts", + "note": "Every page published to the web, and the only place that knows they exist. Written after counting: nine had gone out and the tree named five, because gate:decisions only ever asks the question of a page attached to a decision. The other four were built in the scratchpad, published, discussed, and the scratchpad was later cleared — so a diagnosis, a comparison carrying a recommendation, a plan and a finding survive only as an address on a host we do not own, which is exactly what 'a link with no copy dies the day the host does' was written about. Stores two facts nothing else here can hold — that a public address exists at all, and what the page calls itself there — plus one sentence of its own prose, `shows`. For the four that belong to a decision it does NOT repeat the checked-in path or the build command; decisions.json owns those. `url` IS repeated, deliberately: it is the identity, and an inventory that omits four of its nine rows is not an inventory. Hand-edited, never generated." + }, + { + "file": "scripts/artifact-sweep.mjs", + "symbol": "PUBLISHED", + "note": "Answers 'has anything been published that no register names?' and CANNOT be a gate — which is why it is not called one. A published page's address is minted by the publish and never written back into the tree; the only record that a publish happened is the session log it happened in, and those live under ~/.claude/projects on one laptop. CI cannot see them, so a gate here would pass by being unable to look, and `pnpm gates` must not grow a check like that. `PUBLISHED` is the confirmation line it reads, and it captures the source path as well as the address because that is the one moment anything records whether the page came from the tree or from a scratch directory. `--hook` reads a PostToolUse payload instead of the logs — at hook time the publish may not be flushed to disk yet, and the payload carries the same sentence. It reports and never edits: a register this repo writes for itself is one nobody reads." + }, + { + "file": "scripts/hook-artifact.sh", + "symbol": "command -v node", + "note": "The reminder, fired by the PostToolUse hook in .claude/settings.json the moment a page is published — while the page, its subject and the reason for it are all still in hand. Reminding later is asking somebody to reconstruct. It exists as a shell wrapper rather than a bare `node …` in the settings file because settings.json is checked in and shared, and a hook runs in whatever environment the editor has: where node lives under nvm and nothing sourced it, the bare form fails with 'command not found' and is reported as an error against a publish that succeeded, which teaches people to delete the hook. So it looks for node, and if there is genuinely none it says nothing and leaves. `set -e` is deliberately absent for the same reason." } ], "extend": [ "A new decision is a record in docs/decisions/ (or a section of the design doc that owns it), a row in docs/decisions.json, and `make decisions-doc`. The gate refuses a record with no row, which is the direction nothing else checks.", + "The board redraws itself from the register — a new decision appears on it with no edit here. It is only two commands because it is not a decision page: nothing gates it, so rebuild it in the same commit that moves a row or it silently shows last week's board.", + "Publishing a page is not finished until docs/artifacts.json has a row for it and the board is rebuilt. The hook will say so within seconds of the publish; nothing in CI ever will, because the evidence that a publish happened does not exist inside this repository. Treat the reminder as the mechanism, not as a nag on top of one.", + "A page built in the scratchpad and published is one `rm -rf` away from existing only as a link. If it is worth publishing it is worth a checked-in copy and a script that rebuilds it — and if it turns out to be a decision, it wants a row in the decision register rather than a row on the shelf.", + "One run writes both copies of an options page — the checked-in one and the published one. A flag that emits one at a time is two commands, and the repo cannot tell you when the second was never run.", "Write the question in plain words or the build says so. No file names, no symbols, no commands — the record's body is where those belong. See CLAUDE.md for why this is a tenet and not a style note.", "An open decision needs two or more options and something to look at, published AND checked in. If there is nothing to look at yet, the decision is not open, it is unstarted.", "Never paraphrase a decision into the register. It stores the question; the record owns the answer, and `make decisions` reads the answer out of the record as you run it.", @@ -1308,6 +1517,21 @@ "symbol": "NON_COMMERCIAL", "note": "Enforces that binding, and refuses a non-commercial claim nobody recorded — the exact defect that once told every reader the KFGQPC artwork was more restricted than it is." }, + { + "file": "scripts/gate-notices.mjs", + "symbol": "BUCKETS", + "note": "The other half of the licence question, and the one nothing was asking. gate:license-copy checks that a quotation did not drift; this checks that the SET of quotations is still complete. The adjacency shards were bucketed under one upstream for the whole life of the spans feature while build-adjacency.mjs reached the GPL'd corpus two hops out through morphology.mjs and wrote the result into 2,544 of 3,002 shipped edges — invisible because the drift arrived as a FEATURE, and nobody edits a licence file to ship a feature. Three ties, each of which could rot alone: the LICENSES.md bucket table against the shipped NOTICE.txt files in both directions; the names against SOURCES.md; and a trace of each builder's transitive relative imports against the vendored files that actually exist on disk, where an input the trace finds and BUCKETS does not mention fails the build. The trace is the one that matters — it is the only check here that reads the code rather than two documents agreeing with each other, and it is what would have caught the original. Read the header before adding a verdict: the `open:` word defers an input to an issue that must exist AND still be open, which is the property that keeps this from becoming the quiet allow-list gate-gates.mjs warns about. It found a third candidate upstream on its first run (every edge carries a page number out of a KFGQPC-derived table) and that deferral is live. A fourth tie was added 2026-08-16 and is a different kind: UNBUCKETED plus a readdirSync of apps/web/public/assets, so the gate reads the FOLDER and not only its own declarations. Checks 1-3 compared a list against a table and never looked in the directory, which is how 604 files of word geometry and a 24KB manifest shipped for loops in no row of anything and passed this gate every time. The same run fixed a false verdict: ayah-pages.json was declared `ours` for the roots bucket on the grounds that no page number reaches a root shard, and build-roots.mjs writes pageOf(abs) into all 44,431 occurrence tuples — one bucket calling a file ours while another bucket's read of the same file was an open question WAS the drift." + }, + { + "file": "scripts/gate-scripture.mjs", + "symbol": "PHRASE", + "note": "Refuses running scripture anywhere in the tree — the check the repo's most-repeated sentence had been standing in for since before there was a repo to say it about. Twelve fully vowelled ayahs sat in packages/core/src/verse-text.ts and shipped in the bundle, and a pipeline test held a four-word phrase; the rule against exactly that was asserted 22 times across 20 files including three shipped notices, and nothing checked it. gate:notext is not it and never was — that one forbids <text> elements in page artwork because of a Safari content-visibility paint bug, and misreading its name cost two loops. The measure is the longest run of CONSECUTIVE FULLY-VOWELLED WORDS, a word counting as vowelled at half or more of its letters carrying a mark, and the threshold of 3 is placed in a gap the tree provides rather than chosen: the two offenders ran to 11 and 5, everything else in the repository sits at 1 or 2, and NOTHING sat at 3 or 4. Three discriminators were measured and discarded first (raw Arabic character count flagged ~100 files, longest Arabic run flagged our own locale file hardest, and harakat DENSITY put single ETL specimen words at 1.0-1.29, above scripture) — the header records all of it so a later reader re-derives the number instead of nudging it. SPECIMENS names the files holding five or more lone vowelled words with a reason each, so ~90 tajweed and encoding specimens read as reviewed; the phrase rule overrides it, so no allowlist entry can ever admit a passage. Enumerates like gate-text-sources.mjs, untracked files included, for the reason that file's header argues." + }, + { + "file": "packages/etl/scripts/morphology.mjs", + "symbol": "copyrightBlock", + "note": "One quotation, two derived trees. The corpus's third term of use asks that its copyright notice be reproduced in works derived from it, and both assets/roots/** and assets/adj/** are such a work — so the block is read out of the source file on every build rather than copied into each builder, because two readers of one quotation is exactly how a reproduction stops being one. It finds the block by its last horizontal rule inside the first 40 lines and THROWS rather than returning a short one: a re-pin that adds a header line should extend the notice, and a silently truncated copyright block is the failure this exists to prevent. Extracted from build-roots.mjs when build-adjacency.mjs needed it; the root shards rebuilt byte-identical, which is what makes that a refactor rather than a claim about one." + }, { "file": "apps/web/src/provenance.ts", "symbol": "SOURCE_REPO", diff --git a/docs/tasks.md b/docs/tasks.md new file mode 100644 index 00000000..b7ae0ba5 --- /dev/null +++ b/docs/tasks.md @@ -0,0 +1,170 @@ +<!-- generated by scripts/build-tasks-doc.mjs — do not edit; run `make tasks-doc` --> +<!-- tasks-hash: 64d7643c873d --> + +# What is still open? + +Everything this repository has written down as unfinished, ordered by **whose turn it is**. +[`issues.md`](issues.md) holds the same items ordered worst-first, which is the page to read +when you are deciding what to fix; this is the page to read when you have an hour and want to +know what only you can move. + +Nothing here is authored. Every title is read out of the document that owns the item, and every +link goes there. Change an item where it lives, then run `make tasks-doc`. + +## What is waiting on a decision? — 4 + +Questions this project has opened and not closed. Each one has a page you can open in a browser, +with every option drawn on a real page of the mus'haf at the size it would actually be used — +a wash you cannot see at that size is an answer, and it is one no paragraph would have given you. +Nothing downstream of these moves until they are settled. + +| question | options | read it | it is written up in | +| --- | --- | --- | --- | +| When a reader lets go of the page bar near a juz marker, should the bar pull the page onto it? | 3 | [the page](design/page-bar-options.html) · [in a browser](https://blog.bytesofpurpose.com/hifth/docs/design/page-bar-options.html) | [decisions/page-bar.md#when-a-reader-lets-go-near-a-marker-should-the-bar-pull-the-page-onto-it](decisions/page-bar.md#when-a-reader-lets-go-near-a-marker-should-the-bar-pull-the-page-onto-it) | +| When a juz begins partway down a page, which juz is that page in? | 3 | [the page](design/page-bar-options.html) · [in a browser](https://blog.bytesofpurpose.com/hifth/docs/design/page-bar-options.html) | [decisions/page-bar.md#when-a-juz-begins-partway-down-a-page-which-juz-is-that-page-in](decisions/page-bar.md#when-a-juz-begins-partway-down-a-page-which-juz-is-that-page-in) | +| Should a reader be able to copy their confusion map off the phone — and if so, by which means? | 3 | [the page](design/confusion-map-export.html) · [in a browser](https://blog.bytesofpurpose.com/hifth/docs/design/confusion-map-export.html) | [decisions/confusion-map-export.md](decisions/confusion-map-export.md) | +| Should the app line up what it draws one page at a time, or one printed line at a time? | 6 | [the page](design/mark-placement.html) · [in a browser](https://blog.bytesofpurpose.com/hifth/docs/design/mark-placement.html) | [design/mark-registration.md](design/mark-registration.md) | + +`make decisions` lists them · the `decide` skill walks one from question to record. + +## What can only a person check? — 9 + +A machine can prove this app is well-formed, fast and legible. It cannot judge whether a hop is +*true*, whether a screen reader sounds like something you would say, or whether a page still +opens after eight days in a bag. These are the ones that need a human, and each carries its own +runbook — what you need, what to run, and what you should expect to see. + +| check | it releases | run it with | +| --- | --- | --- | +| On-device pan/zoom + highlight-toggle verdict | loop-4b, loop-6b, loop-7 | `make validate CHECK=perf-verdict-on-device` | +| VoiceOver / TalkBack gesture walkthrough | loop-7 | `make validate CHECK=screen-reader-walkthrough` | +| Read the Complex's published terms at the source | — | `make validate CHECK=kfgqpc-terms-primary-source` | +| Edge spot-audit — 20 sampled pairs against a printed mushaf | web-v1.0 | `make validate CHECK=edge-spot-audit` | +| 8+ day offline survival, installed vs browser tab | loop-6b | `make validate CHECK=offline-survival-8-day` | +| Two days of real revision produce two days in the record | loop-7 | `make validate CHECK=revision-record-lands-on-a-phone` | +| Placement correction by eye — a hundred forced choices between the corrected box and the one we ship | mark-C | `make validate CHECK=placement-correction-by-eye` | +| Placement on pages the correction has never seen — does it still land, is it the right size, and is the leftover the page or the hand? | mark-C | `make validate CHECK=placement-holds-off-its-own-pages` | +| What kind of wrong — the marks nothing can place, named one at a time instead of counted | mark-D, mark-C | `make validate CHECK=placement-what-kind-of-wrong` | + +`make validate` lists them · `make guide` renders them for a phone · `make record` banks a result. + +## What else is waiting on you? — 34 + +Open items nobody but the person who owns this project can settle — a judgement about how the +app should feel, a licence somebody has to read, a device somebody has to hold. The checks above +are not repeated here. + +| item | status | severity | waiting on | +| --- | --- | --- | --- | +| [How far is our correction still out](design/mark-registration.md#-how-far-is-our-correction-still-out--open) | open | question | — | +| [A third upstream reaches the adjacency shards](design/track-b-native.md#-a-third-upstream-reaches-the-adjacency-shards--open) | open | question | a licensing opinion | +| [Arabic number agreement outside `distance`](design/i18n.md#-arabic-number-agreement-outside-distance--open) | open | defect | a hafiz | +| [Does a captured slip become navigable like a corpus edge?](design/confusion-points.md#-does-a-captured-slip-become-navigable-like-a-corpus-edge--open) | open | question | — | +| [Does a real fore-edge stack vary?](design/page-transition.md#-does-a-real-fore-edge-stack-vary--open) | open | question | — | +| [Whether the GPL/App-Store reading in ①–③ is right](design/track-b-native.md#-whether-the-gplapp-store-reading-in--is-right--open) | open | risk | a licensing opinion | +| [How are slip-candidates ranked, and is the opening-word signal worth building?](design/confusion-points.md#-how-are-slip-candidates-ranked-and-is-the-opening-word-signal-worth-building--open) | open | risk | — | +| [How durable must a confusion map be, given iOS wipes it?](design/confusion-points.md#-how-durable-must-a-confusion-map-be-given-ios-wipes-it--open) | open | question | — | +| [Is the rule "nothing leaves the device," or "nothing leaves unless it serves the reader"?](design/revision-record.md#-is-the-rule-nothing-leaves-the-device-or-nothing-leaves-unless-it-serves-the-reader--open) | open | question | — | +| [Would a person pick our rectangle over the one we ship](design/mark-registration.md#-would-a-person-pick-our-rectangle-over-the-one-we-ship--open) | open | question | placement-correction-by-eye | +| [Whether an answer given by pointing is the same evidence as one given by hand](design/mark-registration.md#-whether-an-answer-given-by-pointing-is-the-same-evidence-as-one-given-by-hand--open) | open | question | — | +| [Whether the marks a reader called odd are odd in the print or odd in our reading of it](design/mark-registration.md#-whether-the-marks-a-reader-called-odd-are-odd-in-the-print-or-odd-in-our-reading-of-it--open) | open | question | — | +| [Should a confusion map ever leave the device?](design/confusion-points.md#-should-a-confusion-map-ever-leave-the-device--open) | open | question | — | +| [Should a student be able to hand their confusion map to a teacher?](design/confusion-points.md#-should-a-student-be-able-to-hand-their-confusion-map-to-a-teacher--open) | open | question | — | +| [Should the app learn where *most* readers slip, not just where you do?](design/confusion-points.md#-should-the-app-learn-where-most-readers-slip-not-just-where-you-do--open) | open | question | — | +| [Whether to move the structural metadata to a public-domain source](design/what-we-depend-on.md#-whether-to-move-the-structural-metadata-to-a-public-domain-source--open) | open | question | — | +| [Whether the pagination question covers three outputs rather than one](design/what-we-depend-on.md#-whether-the-pagination-question-covers-three-outputs-rather-than-one--open) | open | question | a licensing opinion | +| [Whether the tajweed spans should move to a pause-aware engine](design/what-we-depend-on.md#-whether-the-tajweed-spans-should-move-to-a-pause-aware-engine--open) | open | question | a hafiz's judgement on the divergences | +| [Whether Track B should exist at all after ④ and ⑤](design/track-b-native.md#-whether-track-b-should-exist-at-all-after--and---open) | open | question | web v1.0 and somebody using it | +| [Whether our own code could be put under more permissive terms at all](design/what-we-depend-on.md#-whether-our-own-code-could-be-put-under-more-permissive-terms-at-all--open) | open | question | twelve-verses-of-scripture-ship-in-the-bundle, the-transliteration-table-is-copied-into-our-code, a licensing opinion | +| [Whether a store build should carry every tree](design/what-we-distribute.md#-whether-a-store-build-should-carry-every-tree--open) | open | question | inputs-are-not-sorted-into-ingredients-and-instruments | +| [Whether what is left over is even ours to fix](design/mark-registration.md#-whether-what-is-left-over-is-even-ours-to-fix--open) | open | question | placement-what-kind-of-wrong | +| [Who sets a seam's state — the reader, or the app's guess from the data?](design/confusion-points.md#-who-sets-a-seams-state--the-reader-or-the-apps-guess-from-the-data--open) | open | question | — | +| [Does the fold read at all?](design/page-transition.md#-does-the-fold-read-at-all--blocked) | blocked | question | a hafiz on the acceptance phone | +| [The same measurement on real mid-tier Android](backlog.md#-the-same-measurement-on-real-mid-tier-android--blocked) | blocked | risk | an Android phone | +| [On-device perf verdict — `perf-verdict-on-device`](backlog.md#-on-device-perf-verdict--perf-verdict-on-device--blocked) | blocked | risk | perf-verdict-on-device | +| [License confirmation](PLAN.md#open-follow-ups) | blocked | risk | kfgqpc-terms-primary-source | +| [On-device perf verdict](PLAN.md#open-follow-ups) | blocked | risk | perf-verdict-on-device | +| [On-device VoiceOver/TalkBack pass](PLAN.md#open-follow-ups) | blocked | risk | screen-reader-walkthrough | +| [The tajweed golden row](PLAN.md#open-follow-ups) | blocked | question | a hafiz, loop-7 | +| [Does the second mount actually hold the frame budget?](design/desktop.md#-does-the-second-mount-actually-hold-the-frame-budget--blocked) | blocked | risk | perf-verdict-on-device | +| [The site answers on plain `http`, so the offline app silently isn't one](backlog.md#-the-site-answers-on-plain-http-so-the-offline-app-silently-isnt-one--blocked) | blocked | defect | the Cloudflare zone dashboard | +| [Everything gated on `perf-verdict-on-device`](design/page-transition.md#-everything-gated-on-perf-verdict-on-device--blocked) | blocked | risk | perf-verdict-on-device | +| [Warmth thresholds](design/revision-record.md#-warmth-thresholds--blocked) | blocked | question | a hafiz | + +## What is waiting on whoever picks it up next? — 37 + +Open items that need reading, measuring or writing rather than deciding. `confirmed` means it was +reproduced with numbers in its source; `suspected` means it was read from the code and never +reproduced; `blocked` means the next step is named and unavailable. + +| item | status | severity | waiting on | +| --- | --- | --- | --- | +| [Counting what a reader pressed stopped meaning anything when tapping the ink was added](design/mark-registration.md#-counting-what-a-reader-pressed-stopped-meaning-anything-when-tapping-the-ink-was-added--confirmed) | confirmed | defect | — | +| [Whether a mark reached by pointing can ever be called only one kind of wrong](design/mark-registration.md#-whether-a-mark-reached-by-pointing-can-ever-be-called-only-one-kind-of-wrong--confirmed) | confirmed | defect | — | +| [The same sitting, sat the rest of the way, says the outlier in ㉙ was not one mark](design/mark-registration.md#-the-same-sitting-sat-the-rest-of-the-way-says-the-outlier-in--was-not-one-mark--confirmed) | confirmed | risk | — | +| [Run as an escalation over every mark the book currently refuses, the rule mostly holds up and one way it does not](design/mark-registration.md#-run-as-an-escalation-over-every-mark-the-book-currently-refuses-the-rule-mostly-holds-up-and-one-way-it-does-not--confirmed) | confirmed | risk | — | +| [Doubling the ground truth moves the cutoff, not just confirms it](design/mark-registration.md#-doubling-the-ground-truth-moves-the-cutoff-not-just-confirms-it--confirmed) | confirmed | risk | — | +| [The re-deal, made: run the checked guard now, and record what it decides](design/mark-registration.md#-the-re-deal-made-run-the-checked-guard-now-and-record-what-it-decides--confirmed) | confirmed | risk | — | +| [The guard ㉜ describes catches one disagreement in four; read both ways, it catches three](design/mark-registration.md#-the-guard--describes-catches-one-disagreement-in-four-read-both-ways-it-catches-three--confirmed) | confirmed | risk | — | +| [A rule that reaches for the ink, rather than resizing toward it, was checked against the whole book](design/mark-registration.md#-a-rule-that-reaches-for-the-ink-rather-than-resizing-toward-it-was-checked-against-the-whole-book--confirmed) | confirmed | risk | — | +| [`skins.ts` still says the print's glyphs are anonymous](design/sub-word-marks.md#-skinsts-still-says-the-prints-glyphs-are-anonymous--confirmed) | confirmed | defect | diacritic-shards-need-a-caller | +| [The marks that ran out of room and still shipped as placed were never looked at, until now](design/mark-registration.md#-the-marks-that-ran-out-of-room-and-still-shipped-as-placed-were-never-looked-at-until-now--confirmed) | confirmed | risk | — | +| [The doubled marks are drawn too small, and no amount of moving them will help](design/mark-registration.md#-the-doubled-marks-are-drawn-too-small-and-no-amount-of-moving-them-will-help--confirmed) | confirmed | defect | — | +| [A mark id is only meaningful against an array's order](design/sub-word-marks.md#-a-mark-id-is-only-meaningful-against-an-arrays-order--open) | open | risk | diacritic-shards-need-a-caller | +| [Are the shards worth 2.28 MB, and against what ceiling](design/sub-word-marks.md#-are-the-shards-worth-228-mb-and-against-what-ceiling--open) | open | question | tajweed-span-to-named-mark | +| [Whether this document should be generated rather than written](design/etl-pipeline.md#-whether-this-document-should-be-generated-rather-than-written--open) | open | question | — | +| [Which encumbered inputs are ingredients and which are only instruments](design/what-we-distribute.md#-which-encumbered-inputs-are-ingredients-and-which-are-only-instruments--open) | open | question | — | +| [Whether the names agree with the text they belong to](design/mark-labels.md#-whether-the-names-agree-with-the-text-they-belong-to--open) | open | question | — | +| [Nothing checks the licences of the installed package tree](design/what-we-depend-on.md#-nothing-checks-the-licences-of-the-installed-package-tree--open) | open | risk | — | +| [Nothing measures what the hop does not contain](design/what-we-depend-on.md#-nothing-measures-what-the-hop-does-not-contain--open) | open | question | — | +| [Do the pages count the marks they should](design/mark-registration.md#-do-the-pages-count-the-marks-they-should--open) | open | question | — | +| [What separates the look-alike pairs](design/mark-registration.md#-what-separates-the-look-alike-pairs--open) | open | risk | the placement decision | +| [What should refuse a piece union too large to trust](design/mark-registration.md#-what-should-refuse-a-piece-union-too-large-to-trust--open) | open | risk | — | +| [Whether a spacing-aware comparison is worth building](design/mark-labels.md#-whether-a-spacing-aware-comparison-is-worth-building--open) | open | question | — | +| [Structural metadata reaches the adjacency shards and nothing names it](design/what-we-depend-on.md#-structural-metadata-reaches-the-adjacency-shards-and-nothing-names-it--open) | open | question | — | +| [Does a tajweed span land on a mark a reader can be shown](design/sub-word-marks.md#-does-a-tajweed-span-land-on-a-mark-a-reader-can-be-shown--open) | open | question | a hafiz looking at the drawn marks | +| [Does the correction hold on a page nobody measured](design/mark-registration.md#-does-the-correction-hold-on-a-page-nobody-measured--open) | open | risk | — | +| [The look-alike panel draws one rectangle where the geometry gives it several](design/word-indexing.md#-the-look-alike-panel-draws-one-rectangle-where-the-geometry-gives-it-several--open) | open | defect | — | +| [Is there anything stopping the correction going stale](design/mark-registration.md#-is-there-anything-stopping-the-correction-going-stale--open) | open | risk | the placement decision | +| [Why the marks were ever in the wrong place, and what has to change](design/mark-registration.md#-why-the-marks-were-ever-in-the-wrong-place-and-what-has-to-change--open) | open | defect | — | +| [Whether to compute the adjacency word ranges from the print instead](design/what-we-depend-on.md#-whether-to-compute-the-adjacency-word-ranges-from-the-print-instead--open) | open | question | — | +| [The page table is derived from one source and corroborated by none](design/what-we-depend-on.md#-the-page-table-is-derived-from-one-source-and-corroborated-by-none--open) | open | risk | — | +| [The segmentation disagreement has no disinterested witness](design/what-we-depend-on.md#-the-segmentation-disagreement-has-no-disinterested-witness--open) | open | question | — | +| [Whether the weakest matches go wrong by class rather than at random](design/mark-registration.md#-whether-the-weakest-matches-go-wrong-by-class-rather-than-at-random--open) | open | question | — | +| [`PLAN.md` states an unachievable order and one wrong citation](design/track-b-native.md#-planmd-states-an-unachievable-order-and-one-wrong-citation--open) | open | defect | gpl-and-the-app-store | +| [Which of the two printings' fonts produced the pages](design/mark-registration.md#-which-of-the-two-printings-fonts-produced-the-pages--open) | open | question | — | +| [The CI frame budget is a number from an emulator](backlog.md#-the-ci-frame-budget-is-a-number-from-an-emulator--blocked) | blocked | risk | perf-verdict-on-device | +| [Loop-assigned deferrals](PLAN.md#open-follow-ups) | blocked | question | loop-7 | +| [Two ayah boxes are cut short of the line, and the pen draws them thin](PLAN.md#open-follow-ups) | blocked | defect | the upstream page cache, a 348 MB fetch nobody starts without asking | + +## Where is the build up to? — 4 loops not finished + +The roadmap of record is [`PLAN.md`](PLAN.md#status--tracking) and this is read out of it. A loop +marked complete is gone from this table; one that shipped with something owed is not, because the +thing owed is exactly what this page is for. + +| loop | where it stands | what would finish it | +| --- | --- | --- | +| 1 — Select + perf | complete-with-deferral | Tap-to-select on-device; RTL page-turn decided; perf verdict → follow-up ① | +| 6b — Pin-a-juz packs | complete-with-deferral (the 8-day half is a user check) | Airplane-mode revision of a pinned juz works after 8+ days | +| 7 — Polish + beta | in flight — its four engineering items are all in (popover ordering, keyboard map, shard prefetch, the 5-page golden sweep, 2026-08-07); what remains is not a loop but a person | Hafiz revision session, zero friction notes → **web v1.0** | +| Track B — Capacitor | gated on web v1.0 | Same web build wrapped for iOS/Android; Universal Links | + +## Where each of these actually lives + +Every register is hand-edited and this page is generated from all of them. Change an item where +it lives; never here. + +| register | question it answers | driver | +| --- | --- | --- | +| [`decisions.json`](decisions.json) | what did we decide, and why? | `make decisions` · the `decide` skill | +| [`validation/ledger.json`](validation/ledger.json) | what can only a human check? | `make validate` | +| [`issues.json`](issues.json) | what is still open, worst first? | `make issues` · the `issues` skill | +| [`PLAN.md`](PLAN.md#status--tracking) | where is the build up to? | edited at the end of every loop | +| [`map.json`](map.json) | where does this feature live? | `make map` · the `extend` skill | +| [`use-cases.json`](use-cases.json) | who uses this, and what proves it? | `make use-cases` | + +--- + +`make tasks` prints this in a terminal · `make tasks-doc` re-renders this page · `pnpm gate:tasks` +refuses a copy built from a source that has since moved. diff --git a/docs/tasks/backlog.md b/docs/tasks/backlog.md new file mode 100644 index 00000000..ccea8951 --- /dev/null +++ b/docs/tasks/backlog.md @@ -0,0 +1,99 @@ +# Confusion-points feature — open backlog + +The open-side companion to [`done.md`](done.md). That file archives finished tasks; this one +holds what is still open on the **confusion-points feature**, so a clean session that starts +with an empty session tracker still knows where the work stands and can be useful in its first +five minutes. + +Like every register in this repo, it **indexes — it does not restate.** Each open question below +is already a row in `docs/issues.json` (rendered into `docs/issues.md`) with its full reasoning; +here it gets one line and a pointer. If a line here disagrees with the issue row, the row wins. + +## Where this sits in the bigger plan + +This is **Tier C1 — "make it theirs"** in `.claude/prompts/hifth-continue.md`, the file that +governs the whole road to web v1.0. That file is the map of the whole project; this one is the +confusion-points thread in detail. Confusion-points is *not* a v1.0 gate — it deepens trust after +the app stops lying about the page — so nothing here blocks a ship. Keep the two in sync: if this +feature's tier or framing changes, change it there too. + +## What is already done — so a clean session does not redo it + +- **The feature is designed and drawn.** `docs/design/confusion-points.md` holds the whole + feature; the eight-screen walkthrough *Where You Slip* is published (artifact + `b0b6d0b5-5bec-4309-8dc4-701ab767e4a1`) and checked in at `docs/design/where-you-slip.html`. + It is the page the owner has been leaving feedback on. +- **The export question is opened, drawn, and published.** Decision *"Should a reader be able to + copy their confusion map off the phone?"* — record `docs/decisions/confusion-map-export.md`, + data `docs/design/confusion-map-export.data.json`, generator + `scripts/build-confusion-export-options.mjs`, checked-in page + `docs/design/confusion-map-export.html`, artifact + `1d9c4cff-f0a5-4aa3-a70b-2717f0814b1a`. Registered in `docs/decisions.json` and + `docs/artifacts.json`. Both artifact comment threads are resolved. +- **The privacy stance was sharpened** from *"nothing leaves the phone"* to **"nothing leaves + unless it is in the reader's interest, and under their control,"** carried into + `docs/design/revision-record.md` item ④. +- Committed in `a5d41af` (feature) and `6dc0b81` (memory). All gates green. + +## The open questions — each is already a row in `docs/issues.json` + +Owner is **`user`** on every one: these are the owner's to answer, not an agent's. An agent's job +is to *draw and redraw the options* on a real mus'haf page, keep the records honest, and open the +next question — never to choose. + +Ordered by what to look at first. + +1. **`should-a-confusion-map-leave-the-device`** — *Should a confusion map ever leave the device?* + **This one is live now:** the published `confusion-map-export` decision answers its + file-backup half and awaits the owner's pick (options A/B/C on the page). Sync and + teacher-sharing are named there as the heavier mechanisms it deliberately does not take yet. + +2. **`is-the-privacy-rule-nothing-leaves-or-nothing-that-doesnt-serve`** — *Is the rule "nothing + leaves the device," or "nothing leaves unless it serves the reader"?* The app-wide framing + question the export decision forced. Sourced in `docs/design/revision-record.md` ④; does **not** + loosen the revision-privacy gate — it asks whether the sentence that gate defends has the right + wording. + +3. **`should-the-app-learn-where-most-readers-slip`** — *Should the app learn where most readers + slip, not just where you do?* Flagged as the **sharpest privacy tension in the feature**: + pooling across readers contradicts the on-device-private design. Answer this before any + analytics work is even sketched. + +4. **`should-a-student-share-a-map-with-a-teacher`** — *Should a student be able to hand their + confusion map to a teacher?* A genuinely valuable use the app's navigation nature does not + otherwise serve; likely its **own future feature**, not a toggle on the export page. + +5. **`who-sets-a-confusion-seams-state`** — *Who sets a seam's state — the reader, or the app's + guess from the data?* + +6. **`does-a-captured-slip-become-navigable`** — *Does a captured slip become navigable like a + corpus edge?* + +7. **`how-are-slip-candidates-ranked`** — *How are slip-candidates ranked, and is the opening-word + signal worth building?* (severity: risk) + +8. **`how-durable-must-a-confusion-map-be`** — *How durable must a confusion map be, given iOS + wipes it?* Tied to #1 — a durable backup is one reason a map would want to leave the device. + +## What a clean session should do first + +1. Read `CLAUDE.md`, then `docs/design/confusion-points.md`, then this file. +2. `node scripts/gate-issues.mjs` and `node scripts/gate-decisions.mjs` — confirm the tree is + consistent before touching anything. (`pnpm` is not on PATH in this shell; call the scripts + directly.) +3. If picking up an open question, the move is almost always: **draw the options on a real page** + with a committed generator, publish (with a per-action ask), and register the decision — the + `decide` skill walks the whole thing. The owner then answers. + +## Standing constraints a clean session must not trip over + +- **No Qur'an scripture or Arabic codepoints in any committed page** — enforced by the shipped + bytes, not only policy. Generators guard against it; keep that guard. +- **Artifacts speak English** — mocks and walkthroughs use English chrome; only scripture inside + them stays Arabic. +- **Never publish or republish an artifact without a per-action ask.** Publishing mints a public + address; it always needs the owner's yes first. +- **Never commit unless explicitly asked.** +- After any publish, `scripts/artifact-sweep.mjs` (a hook, not a gate) checks the page got a row + in `docs/artifacts.json` — it only runs where the session log lives, so do not assume CI caught + a missing row. diff --git a/docs/tasks/done.md b/docs/tasks/done.md new file mode 100644 index 00000000..1ab0a690 --- /dev/null +++ b/docs/tasks/done.md @@ -0,0 +1,37 @@ +# Completed tasks + +An archive, not a register — the session task tracker (`TaskList`) only shows what's still open, +so a task moves here once it's done rather than staying in that list forever. This file is not +the record of what was decided or found; that lives in the design docs, decisions, and commits +each task fed into. This is a dated pointer back to those, kept so a finished task isn't simply +deleted and forgotten. + +Nothing here is hand-restated from those sources beyond a one-line summary — read the linked +commit or design-doc item for the actual reasoning. + +## #209 — Rebuild the piece-union candidate script (reach-for-the-ink) + +**Done:** 2026-08-19, commit `650ecbd` "Rebuild the piece-union script the doc's numbers came +from, so ㉜ has one". + +The "reach for the ink" script behind item ㉘/㉙'s numbers had been run twice and discarded — +never committed, so nothing backed the two aggregate figures on record. Rebuilt as +`packages/etl/scripts/probe-piece-union.mjs`, reusing `readPageInk`/`shapeOf` from +`packages/etl/lib/ink.mjs`. Needed before #210 could test a guard against real per-mark candidate +output rather than two remembered numbers. + +## #210 — Score an area-ratio cutoff against the 45 doubled ground-truth marks + +**Done:** 2026-08-19, commit `e333652` "The guard ㉜ proposed catches a quarter of what it needs +to; both ways, three in four" — full writeup at +[`docs/design/mark-registration.md` item ㉝](../design/mark-registration.md). + +Ran the rebuilt script against all 89 ground-truth marks with a settled reader answer. The guard +exactly as ㉜ proposed it — refuse only when a candidate grows past the shipped rectangle — +catches just 5 of 20 (25%) real disagreements, because 15 of those 20 are candidates that +*shrank*, which a one-sided "too big" test structurally can't see. The same measure read +**symmetrically** (flag both far-too-big and far-too-small) catches 15 of 20 (75%) at one false +flag among 52 good marks (94% precision). Verdict: the guard as literally proposed — kill; the +symmetric reframing — confirmed, and escalated as a revision to ㉜'s own test. Later doubled to +181 marks and re-cut at 1.75/0.571 by item ㉞ — see +[`hifth-continue.md`](../../.claude/prompts/hifth-continue.md) §1 for where that stands now. diff --git a/docs/use-cases.json b/docs/use-cases.json index de99a436..72f23e4c 100644 --- a/docs/use-cases.json +++ b/docs/use-cases.json @@ -270,8 +270,12 @@ "feature": "the-hop", "code": [ { - "file": "packages/core/src/verse-text.ts", - "symbol": "diffPair" + "file": "packages/core/src/verse-diff.ts", + "symbol": "wordDiff" + }, + { + "file": "packages/core/src/verse-diff.ts", + "symbol": "divergentRuns" }, { "file": "apps/web/src/components/DiffView.tsx", @@ -281,7 +285,11 @@ "proof": [ { "file": "apps/web/e2e/share-a11y.spec.ts", - "test": "expanding a hop row reveals the token diff of the two readings" + "test": "expanding a hop row draws both ayahs from the page, with the leftover washed" + }, + { + "file": "apps/web/src/components/DiffView.test.tsx", + "test": "washes the words each side does not share, on both sides" } ], "includes": [ @@ -547,7 +555,7 @@ }, { "file": "apps/web/e2e/desktop.spec.ts", - "test": "the stepper magnifies the one leaf, and is off while there are two" + "test": "the stepper magnifies one leaf alone, and both leaves of a spread together" }, { "file": "apps/web/e2e/desktop.spec.ts", diff --git a/docs/use-cases.md b/docs/use-cases.md index 0310b203..32cfddab 100644 --- a/docs/use-cases.md +++ b/docs/use-cases.md @@ -1,4 +1,4 @@ -<!-- use-cases-hash: fa62747ce744 --> +<!-- use-cases-hash: 451c3fcb6122 --> <!-- GENERATED by scripts/build-use-cases.mjs from docs/use-cases.json. Edit the JSON, then run `make use-cases-doc`. Hand edits are lost and gate:use-cases fails when this file and the source disagree. --> @@ -102,7 +102,7 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| | happens in | [`packages/core/src/gestures.ts:64`](../packages/core/src/gestures.ts#L64) — `LONG_PRESS_MS` | -| happens in | [`apps/web/src/components/PageStage.tsx:338`](../apps/web/src/components/PageStage.tsx#L338) — `PageStage` | +| happens in | [`apps/web/src/components/PageStage.tsx:395`](../apps/web/src/components/PageStage.tsx#L395) — `PageStage` | | proven by | [`apps/web/e2e/marquee.spec.ts:68`](../apps/web/e2e/marquee.spec.ts#L68) — “long-press then drag inks the ayahs the marquee crossed” | | proven by | [`apps/web/e2e/marquee.spec.ts:96`](../apps/web/e2e/marquee.spec.ts#L96) — “an immediate drag is a pan, and a pan paints nothing” | @@ -114,9 +114,9 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest |---|---| | happens in | [`packages/core/src/words.ts:120`](../packages/core/src/words.ts#L120) — `class WordIndex` | | happens in | [`packages/core/src/gestures.ts:204`](../packages/core/src/gestures.ts#L204) — `heldIntent` | -| happens in | [`apps/web/src/components/PageStage.tsx:1585`](../apps/web/src/components/PageStage.tsx#L1585) — `applyWords` | +| happens in | [`apps/web/src/components/PageStage.tsx:1740`](../apps/web/src/components/PageStage.tsx#L1740) — `applyWords` | | happens in | [`packages/core/src/adjacency.ts:384`](../packages/core/src/adjacency.ts#L384) — `refineByWords` | -| happens in | [`apps/web/src/App.tsx:846`](../apps/web/src/App.tsx#L846) — `const handleSelectWords` | +| happens in | [`apps/web/src/App.tsx:964`](../apps/web/src/App.tsx#L964) — `const handleSelectWords` | | proven by | [`apps/web/e2e/word.spec.ts:109`](../apps/web/e2e/word.spec.ts#L109) — “a hold inside the selected ayah drops to words; a drag extends the run” | | proven by | [`apps/web/e2e/word.spec.ts:151`](../apps/web/e2e/word.spec.ts#L151) — “Escape climbs back to the whole ayah, not out of the selection” | | proven by | [`apps/web/e2e/word.spec.ts:177`](../apps/web/e2e/word.spec.ts#L177) — “Enter again descends to words; ← carries the run and Shift+← grows it” | @@ -135,7 +135,7 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest |---|---| | happens in | [`packages/core/src/adjacency.ts:420`](../packages/core/src/adjacency.ts#L420) — `class Adjacency` | | happens in | [`packages/core/src/adjacency.ts:155`](../packages/core/src/adjacency.ts#L155) — `bucketEdges` | -| happens in | [`apps/web/src/components/PageStage.tsx:1296`](../apps/web/src/components/PageStage.tsx#L1296) — `async navigateTo` | +| happens in | [`apps/web/src/components/PageStage.tsx:1397`](../apps/web/src/components/PageStage.tsx#L1397) — `async navigateTo` | | proven by | [`apps/web/e2e/hop.spec.ts:8`](../apps/web/e2e/hop.spec.ts#L8) — “tap 2:48 → rail → popover → cross-page hop to 2:123 → bead back” | | proven by | [`apps/web/e2e/hop.spec.ts:55`](../apps/web/e2e/hop.spec.ts#L55) — “un-vendored hop targets are surfaced but disabled (no ghost pages)” | @@ -146,7 +146,7 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| | happens in | [`packages/core/src/adjacency.ts:300`](../packages/core/src/adjacency.ts#L300) — `mergeRangeEdges` | -| happens in | [`apps/web/src/components/HighlightMenu.tsx:52`](../apps/web/src/components/HighlightMenu.tsx#L52) — `HighlightMenu` | +| happens in | [`apps/web/src/components/HighlightMenu.tsx:53`](../apps/web/src/components/HighlightMenu.tsx#L53) — `HighlightMenu` | | proven by | [`apps/web/e2e/range.spec.ts:25`](../apps/web/e2e/range.spec.ts#L25) — “a range surfaces the merged menu, and the URL keeps the range form” | | proven by | [`apps/web/e2e/range.spec.ts:82`](../apps/web/e2e/range.spec.ts#L82) — “a merged row hops from the range member that produced it” | @@ -156,9 +156,11 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| -| happens in | [`packages/core/src/verse-text.ts:115`](../packages/core/src/verse-text.ts#L115) — `diffPair` | -| happens in | [`apps/web/src/components/DiffView.tsx:44`](../apps/web/src/components/DiffView.tsx#L44) — `DiffView` | -| proven by | [`apps/web/e2e/share-a11y.spec.ts:99`](../apps/web/e2e/share-a11y.spec.ts#L99) — “expanding a hop row reveals the token diff of the two readings” | +| happens in | [`packages/core/src/verse-diff.ts:70`](../packages/core/src/verse-diff.ts#L70) — `wordDiff` | +| happens in | [`packages/core/src/verse-diff.ts:94`](../packages/core/src/verse-diff.ts#L94) — `divergentRuns` | +| happens in | [`apps/web/src/components/DiffView.tsx:131`](../apps/web/src/components/DiffView.tsx#L131) — `DiffView` | +| proven by | [`apps/web/e2e/share-a11y.spec.ts:99`](../apps/web/e2e/share-a11y.spec.ts#L99) — “expanding a hop row draws both ayahs from the page, with the leftover washed” | +| proven by | [`apps/web/src/components/DiffView.test.tsx:54`](../apps/web/src/components/DiffView.test.tsx#L54) — “washes the words each side does not share, on both sides” | ### Show me every ayah built on the same Arabic root — the other way ayahs rhyme. @@ -224,7 +226,7 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest |---|---| | happens in | [`packages/core/src/keymap.ts:121`](../packages/core/src/keymap.ts#L121) — `appKeyAction` | | happens in | [`packages/core/src/gestures.ts:334`](../packages/core/src/gestures.ts#L334) — `turnCommit` | -| happens in | [`apps/web/src/assets.ts:89`](../apps/web/src/assets.ts#L89) — `loadPage` | +| happens in | [`apps/web/src/assets.ts:90`](../apps/web/src/assets.ts#L90) — `loadPage` | | proven by | [`apps/web/e2e/wayfinding.spec.ts:115`](../apps/web/e2e/wayfinding.spec.ts#L115) — “arrows turn pages, and stop honestly at the ends of the book” | | proven by | [`apps/web/e2e/page-turn.spec.ts:389`](../apps/web/e2e/page-turn.spec.ts#L389) — “a sideways flick turns the page, and the band tracks the finger” | | proven by | [`apps/web/e2e/offline.spec.ts:227`](../apps/web/e2e/offline.spec.ts#L227) — “a page that would not load is said, not silently swapped for another” | @@ -235,7 +237,7 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| -| happens in | [`apps/web/src/components/PageSlider.tsx:78`](../apps/web/src/components/PageSlider.tsx#L78) — `PageSlider` | +| happens in | [`apps/web/src/components/PageSlider.tsx:123`](../apps/web/src/components/PageSlider.tsx#L123) — `PageSlider` | | happens in | [`packages/core/src/pages.ts:36`](../packages/core/src/pages.ts#L36) — `nearestPage` | | proven by | [`apps/web/e2e/pagebar.spec.ts:31`](../apps/web/e2e/pagebar.spec.ts#L31) — “spans the printed mus'haf and says how much of it is here” | | proven by | [`apps/web/e2e/pagebar.spec.ts:105`](../apps/web/e2e/pagebar.spec.ts#L105) — “letting go in the gap lands on the nearest page we have, and says so” | @@ -248,12 +250,12 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| | happens in | [`packages/core/src/pages.ts:166`](../packages/core/src/pages.ts#L166) — `spreadOf` | -| happens in | [`apps/web/src/components/PageSpread.tsx:141`](../apps/web/src/components/PageSpread.tsx#L141) — `PageSpread` | +| happens in | [`apps/web/src/components/PageSpread.tsx:152`](../apps/web/src/components/PageSpread.tsx#L152) — `PageSpread` | | happens in | [`apps/web/src/useMediaQuery.ts:43`](../apps/web/src/useMediaQuery.ts#L43) — `DESKTOP_QUERY` | -| proven by | [`apps/web/e2e/desktop.spec.ts:168`](../apps/web/e2e/desktop.spec.ts#L168) — “puts the lower page number on the right” | -| proven by | [`apps/web/e2e/desktop.spec.ts:98`](../apps/web/e2e/desktop.spec.ts#L98) — “appears above the breakpoint and does not exist below it” | +| proven by | [`apps/web/e2e/desktop.spec.ts:177`](../apps/web/e2e/desktop.spec.ts#L177) — “puts the lower page number on the right” | +| proven by | [`apps/web/e2e/desktop.spec.ts:107`](../apps/web/e2e/desktop.spec.ts#L107) — “appears above the breakpoint and does not exist below it” | | proven by | [`packages/core/src/pages.test.ts:136`](../packages/core/src/pages.test.ts#L136) — “puts the lower page number on the right” | -| proven by | [`apps/web/e2e/desktop.spec.ts:357`](../apps/web/e2e/desktop.spec.ts#L357) — “still turns pages with the arrow keys the header advertises” | +| proven by | [`apps/web/e2e/desktop.spec.ts:366`](../apps/web/e2e/desktop.spec.ts#L366) — “still turns pages with the arrow keys the header advertises” | > **Not proven:** Only the desktop project ever renders a spread, and it runs one file. The rest of the suite — every aria snapshot, every golden image, the axe pass — is recorded on a phone, so a regression that only shows at desktop width has exactly these four assertions standing between it and a release. Zoom on a spread is now covered — see choose-one-page-or-two — but the frame budget is not: two mounted pages share a budget that was measured with one, which is desktop.md ④ and still blocked. @@ -264,10 +266,10 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| | happens in | [`apps/web/src/components/DesktopChrome.tsx:58`](../apps/web/src/components/DesktopChrome.tsx#L58) — `DesktopChrome` | -| happens in | [`apps/web/src/components/PageStage.tsx:297`](../apps/web/src/components/PageStage.tsx#L297) — `ZOOM_STEPS` | -| proven by | [`apps/web/e2e/desktop.spec.ts:842`](../apps/web/e2e/desktop.spec.ts#L842) — “the toggle closes the book and opens it again” | -| proven by | [`apps/web/e2e/desktop.spec.ts:873`](../apps/web/e2e/desktop.spec.ts#L873) — “the stepper magnifies the one leaf, and is off while there are two” | -| proven by | [`apps/web/e2e/desktop.spec.ts:920`](../apps/web/e2e/desktop.spec.ts#L920) — “crossing the breakpoint leaves the two leaves agreeing” | +| happens in | [`apps/web/src/components/PageStage.tsx:354`](../apps/web/src/components/PageStage.tsx#L354) — `ZOOM_STEPS` | +| proven by | [`apps/web/e2e/desktop.spec.ts:1035`](../apps/web/e2e/desktop.spec.ts#L1035) — “the toggle closes the book and opens it again” | +| proven by | [`apps/web/e2e/desktop.spec.ts:1066`](../apps/web/e2e/desktop.spec.ts#L1066) — “the stepper magnifies one leaf alone, and both leaves of a spread together” | +| proven by | [`apps/web/e2e/desktop.spec.ts:1163`](../apps/web/e2e/desktop.spec.ts#L1163) — “crossing the breakpoint leaves the two leaves agreeing” | | proven by | [`apps/web/src/components/DesktopChrome.test.tsx:209`](../apps/web/src/components/DesktopChrome.test.tsx#L209) — “is a radiogroup, not a checkbox labelled 'two pages'” | | proven by | [`apps/web/src/components/DesktopChrome.test.tsx:243`](../apps/web/src/components/DesktopChrome.test.tsx#L243) — “gets a reader back onto the ladder from wherever a hop left them” | @@ -281,9 +283,9 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest |---|---| | happens in | [`packages/core/src/packs.ts:170`](../packages/core/src/packs.ts#L170) — `juzPageIndex` | | happens in | [`packages/core/src/gestures.ts:443`](../packages/core/src/gestures.ts#L443) — `nextWheelTurn` | -| proven by | [`apps/web/e2e/desktop.spec.ts:749`](../apps/web/e2e/desktop.spec.ts#L749) — “shift+wheel jumps a juz, and says which one” | -| proven by | [`apps/web/e2e/desktop.spec.ts:722`](../apps/web/e2e/desktop.spec.ts#L722) — “ctrl+wheel does nothing at all — it is somebody else's pinch” | -| proven by | [`apps/web/e2e/desktop.spec.ts:777`](../apps/web/e2e/desktop.spec.ts#L777) — “the juz axis and the page axis do not spend each other's travel” | +| proven by | [`apps/web/e2e/desktop.spec.ts:945`](../apps/web/e2e/desktop.spec.ts#L945) — “shift+wheel jumps a juz, and says which one” | +| proven by | [`apps/web/e2e/desktop.spec.ts:918`](../apps/web/e2e/desktop.spec.ts#L918) — “ctrl+wheel does nothing at all — it is somebody else's pinch” | +| proven by | [`apps/web/e2e/desktop.spec.ts:973`](../apps/web/e2e/desktop.spec.ts#L973) — “the juz axis and the page axis do not spend each other's travel” | | proven by | [`packages/core/src/packs.test.ts:164`](../packages/core/src/packs.test.ts#L164) — “opens a juz on the leaf where it begins, not where the one before it ends” | > **Not proven:** A wheel is the only way in, so this affordance does not exist on a phone or for a keyboard user — the jumper (jump-to-a-place-i-can-name) is the equivalent path and reaches the same pages by name. The ctrl branch is a deliberate dead end rather than a binding: on macOS a trackpad pinch arrives as ctrl+wheel and the browser cannot tell it apart. @@ -294,12 +296,12 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| -| happens in | [`apps/web/src/components/PageSpread.tsx:141`](../apps/web/src/components/PageSpread.tsx#L141) — `PageSpread` | -| happens in | [`apps/web/src/i18n.tsx:460`](../apps/web/src/i18n.tsx#L460) — `facingAbsent` | -| proven by | [`apps/web/e2e/desktop.spec.ts:274`](../apps/web/e2e/desktop.spec.ts#L274) — “announces the missing facing page instead of showing blank paper” | +| happens in | [`apps/web/src/components/PageSpread.tsx:152`](../apps/web/src/components/PageSpread.tsx#L152) — `PageSpread` | +| happens in | [`apps/web/src/i18n.tsx:488`](../apps/web/src/i18n.tsx#L488) — `facingAbsent` | +| proven by | [`apps/web/e2e/desktop.spec.ts:283`](../apps/web/e2e/desktop.spec.ts#L283) — “announces the missing facing page instead of showing blank paper” | | proven by | [`apps/web/src/components/PageSpread.test.tsx:82`](../apps/web/src/components/PageSpread.test.tsx#L82) — “draws the missing facing page as absent, not as blank paper” | | proven by | [`apps/web/src/components/PageSpread.test.tsx:133`](../apps/web/src/components/PageSpread.test.tsx#L133) — “leaves the far side blank and unlabelled at the end of an odd-length print” | -| proven by | [`apps/web/e2e/desktop.spec.ts:168`](../apps/web/e2e/desktop.spec.ts#L168) — “puts the lower page number on the right” | +| proven by | [`apps/web/e2e/desktop.spec.ts:177`](../apps/web/e2e/desktop.spec.ts#L177) — “puts the lower page number on the right” | ### On a screen with room, stop making me open a sheet to change the language, and tell me the keyboard can turn pages. @@ -308,7 +310,7 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| | happens in | [`apps/web/src/components/DesktopChrome.tsx:58`](../apps/web/src/components/DesktopChrome.tsx#L58) — `DesktopChrome` | -| proven by | [`apps/web/e2e/desktop.spec.ts:331`](../apps/web/e2e/desktop.spec.ts#L331) — “gives the header the controls the phone had to hide” | +| proven by | [`apps/web/e2e/desktop.spec.ts:340`](../apps/web/e2e/desktop.spec.ts#L340) — “gives the header the controls the phone had to hide” | | proven by | [`apps/web/src/components/DesktopChrome.test.tsx:145`](../apps/web/src/components/DesktopChrome.test.tsx#L145) — “only hints keys that actually do the thing the hint claims” | | proven by | [`apps/web/e2e/chrome-fit.spec.ts:53`](../apps/web/e2e/chrome-fit.spec.ts#L53) — “no horizontal overflow, and a constant header height, from 320 to 430” | @@ -329,7 +331,7 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| -| happens in | [`packages/core/src/skins.ts:204`](../packages/core/src/skins.ts#L204) — `class Tajweed` | +| happens in | [`packages/core/src/skins.ts:314`](../packages/core/src/skins.ts#L314) — `class Tajweed` | | happens in | [`apps/web/src/components/SkinToggle.tsx:34`](../apps/web/src/components/SkinToggle.tsx#L34) — `SkinToggle` | | proven by | [`apps/web/e2e/skin.spec.ts:50`](../apps/web/e2e/skin.spec.ts#L50) — “switching on marks ayahs with their rules” | | proven by | [`apps/web/e2e/skin.spec.ts:35`](../apps/web/e2e/skin.spec.ts#L35) — “plain → tajweed → plain leaves the page geometry byte-identical” | @@ -385,15 +387,15 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | happens in | [`packages/core/src/revision.ts:201`](../packages/core/src/revision.ts#L201) — `lastSeen` | | happens in | [`packages/core/src/revision.ts:143`](../packages/core/src/revision.ts#L143) — `scopesOf` | | happens in | [`apps/web/src/revision-store.ts:183`](../apps/web/src/revision-store.ts#L183) — `recordLook` | -| happens in | [`apps/web/src/components/RevisionMap.tsx:162`](../apps/web/src/components/RevisionMap.tsx#L162) — `holdings` | +| happens in | [`apps/web/src/components/RevisionMap.tsx:190`](../apps/web/src/components/RevisionMap.tsx#L190) — `holdings` | | happens in | [`scripts/gate-revision-privacy.mjs:50`](../scripts/gate-revision-privacy.mjs#L50) — `ALLOWED_IMPORTERS` | | proven by | [`packages/core/src/revision.test.ts:158`](../packages/core/src/revision.test.ts#L158) — “answers with the most recent day, whatever order the log is in” | | proven by | [`apps/web/src/revision-store.test.ts:101`](../apps/web/src/revision-store.test.ts#L101) — “knows its own age before a single look is recorded” | -| proven by | [`apps/web/src/App.test.tsx:206`](../apps/web/src/App.test.tsx#L206) — “does not record the second tap that dismisses a selection” | -| proven by | [`apps/web/src/App.test.tsx:222`](../apps/web/src/App.test.tsx#L222) — “credits the source of a hop, never the ayah the app moved them to” | +| proven by | [`apps/web/src/App.test.tsx:236`](../apps/web/src/App.test.tsx#L236) — “does not record the second tap that dismisses a selection” | +| proven by | [`apps/web/src/App.test.tsx:252`](../apps/web/src/App.test.tsx#L252) — “credits the source of a hop, never the ayah the app moved them to” | | proven by | [`apps/web/src/components/RevisionMap.test.tsx:128`](../apps/web/src/components/RevisionMap.test.tsx#L128) — “draws absent as a different kind of thing from cold, not a paler one” | -| proven by | [`apps/web/src/components/RevisionMap.test.tsx:206`](../apps/web/src/components/RevisionMap.test.tsx#L206) — “opens the division when its cell is pressed, and gets out of the way” | -| proven by | [`apps/web/src/components/RevisionMap.test.tsx:234`](../apps/web/src/components/RevisionMap.test.tsx#L234) — “offers no control on a cell with no paper behind it” | +| proven by | [`apps/web/src/components/RevisionMap.test.tsx:309`](../apps/web/src/components/RevisionMap.test.tsx#L309) — “opens the division when its cell is pressed, and gets out of the way” | +| proven by | [`apps/web/src/components/RevisionMap.test.tsx:337`](../apps/web/src/components/RevisionMap.test.tsx#L337) — “offers no control on a cell with no paper behind it” | | proven by | [`apps/web/e2e/revision.spec.ts:61`](../apps/web/e2e/revision.spec.ts#L61) — “a tap on an ayah is still on the map after a reload” | | proven by | [`apps/web/e2e/revision.spec.ts:93`](../apps/web/e2e/revision.spec.ts#L93) — “pressing a hizb on the map opens it” | | proven by | [`apps/web/e2e/revision.spec.ts:149`](../apps/web/e2e/revision.spec.ts#L149) — “a wiped record reads as a young record, not as an empty one” | @@ -406,12 +408,12 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| -| happens in | [`packages/core/src/highlighter.ts:225`](../packages/core/src/highlighter.ts#L225) — `enhancePolygons` | +| happens in | [`packages/core/src/highlighter.ts:232`](../packages/core/src/highlighter.ts#L232) — `enhancePolygons` | | happens in | [`apps/web/src/components/LiveAnnouncer.tsx:28`](../apps/web/src/components/LiveAnnouncer.tsx#L28) — `LiveAnnouncer` | | happens in | [`apps/web/src/format.ts:115`](../apps/web/src/format.ts#L115) — `export function digits` | -| proven by | [`apps/web/e2e/share-a11y.spec.ts:119`](../apps/web/e2e/share-a11y.spec.ts#L119) — “keyboard-only: focus an ayah, select with Enter, open rail, hop, land” | -| proven by | [`apps/web/e2e/share-a11y.spec.ts:243`](../apps/web/e2e/share-a11y.spec.ts#L243) — “the mushaf page is a labelled group of ayah buttons” | -| proven by | [`apps/web/e2e/share-a11y.spec.ts:231`](../apps/web/e2e/share-a11y.spec.ts#L231) — “the chrome announces every control by word, never by glyph” | +| proven by | [`apps/web/e2e/share-a11y.spec.ts:133`](../apps/web/e2e/share-a11y.spec.ts#L133) — “keyboard-only: focus an ayah, select with Enter, open rail, hop, land” | +| proven by | [`apps/web/e2e/share-a11y.spec.ts:257`](../apps/web/e2e/share-a11y.spec.ts#L257) — “the mushaf page is a labelled group of ayah buttons” | +| proven by | [`apps/web/e2e/share-a11y.spec.ts:245`](../apps/web/e2e/share-a11y.spec.ts#L245) — “the chrome announces every control by word, never by glyph” | | proven by | `pnpm gate:notext` | > **Not proven:** axe and aria snapshots check the tree, not the experience. Whether VoiceOver's rotor actually gets a hafiz through a hop is ledger check screen-reader-walkthrough. @@ -423,11 +425,11 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| | happens in | [`apps/web/src/lang.ts:121`](../apps/web/src/lang.ts#L121) — `detectLang` | -| happens in | [`apps/web/src/i18n.tsx:888`](../apps/web/src/i18n.tsx#L888) — `LangProvider` | +| happens in | [`apps/web/src/i18n.tsx:917`](../apps/web/src/i18n.tsx#L917) — `LangProvider` | | happens in | [`apps/web/src/components/Colophon.tsx:220`](../apps/web/src/components/Colophon.tsx#L220) — `langRow` | | proven by | [`apps/web/e2e/lang.spec.ts:28`](../apps/web/e2e/lang.spec.ts#L28) — “an English phone opens an English chrome” | -| proven by | [`apps/web/e2e/lang.spec.ts:88`](../apps/web/e2e/lang.spec.ts#L88) — “the switch is in the colophon, and the choice survives a reload” | -| proven by | [`apps/web/e2e/lang.spec.ts:43`](../apps/web/e2e/lang.spec.ts#L43) — “the mus'haf, the rail, the trail and the page bar stay right-to-left” | +| proven by | [`apps/web/e2e/lang.spec.ts:102`](../apps/web/e2e/lang.spec.ts#L102) — “the switch is in the colophon, and the choice survives a reload” | +| proven by | [`apps/web/e2e/lang.spec.ts:57`](../apps/web/e2e/lang.spec.ts#L57) — “the mus'haf, the rail, the trail and the page bar stay right-to-left” | | proven by | [`apps/web/src/i18n.test.tsx:71`](../apps/web/src/i18n.test.tsx#L71) — “leaves no Arabic in the English chrome” | > **Not proven:** Two languages, and the second one is the author's second language. Nothing here judges whether the English reads well to a hafiz who uses it — only that it is English, complete, and has not leaked into the places that must stay Arabic. @@ -438,9 +440,9 @@ Someone memorising the Qur'an, holding a phone, usually mid-revision. Every gest | | | |---|---| -| happens in | [`apps/web/src/messages/ar.json:127`](../apps/web/src/messages/ar.json#L127) — `distancePages` | +| happens in | [`apps/web/src/messages/ar.json:129`](../apps/web/src/messages/ar.json#L129) — `distancePages` | | happens in | [`apps/web/src/messages/plural.ts:39`](../apps/web/src/messages/plural.ts#L39) — `export function plural` | -| happens in | [`apps/web/src/i18n.tsx:522`](../apps/web/src/i18n.tsx#L522) — `buildStrings` | +| happens in | [`apps/web/src/i18n.tsx:542`](../apps/web/src/i18n.tsx#L542) — `buildStrings` | | proven by | [`apps/web/src/i18n.test.tsx:121`](../apps/web/src/i18n.test.tsx#L121) — “agrees with the plural rules of the language, not with a ternary” | | proven by | [`apps/web/src/i18n.test.tsx:99`](../apps/web/src/i18n.test.tsx#L99) — “writes counts inside aria-labels in the language's own digits” | @@ -469,7 +471,7 @@ The person doing what a machine cannot — a phone in one hand, a printed mushaf | | | |---|---| | happens in | [`docs/validation/ledger.json:25`](../docs/validation/ledger.json#L25) — `runbook` | -| happens in | [`scripts/build-validation-guide.mjs:411`](../scripts/build-validation-guide.mjs#L411) — `GUIDE_PATH` | +| happens in | [`scripts/build-validation-guide.mjs:137`](../scripts/build-validation-guide.mjs#L137) — `GUIDE_PATH` | | proven by | `pnpm gate:validation` | > **Not proven:** The gate proves every outstanding check has a followable runbook and that the phone guide was regenerated. It cannot prove the runbook's Expect lines match what the phone shows — only walking it does, which is the point of walking it. @@ -496,7 +498,7 @@ Whoever is changing the app next, including this project's own future self. Arri |---|---| | happens in | [`scripts/messages-compile.mjs:378`](../scripts/messages-compile.mjs#L378) — `export function compileAll` | | happens in | [`apps/web/src/lang.ts:80`](../apps/web/src/lang.ts#L80) — `LOCALES` | -| happens in | [`apps/web/src/i18n.tsx:522`](../apps/web/src/i18n.tsx#L522) — `buildStrings` | +| happens in | [`apps/web/src/i18n.tsx:542`](../apps/web/src/i18n.tsx#L542) — `buildStrings` | | proven by | [`apps/web/src/i18n.test.tsx:53`](../apps/web/src/i18n.test.tsx#L53) — “builds a bundle for every locale that has a catalog” | | proven by | [`apps/web/src/i18n.test.tsx:62`](../apps/web/src/i18n.test.tsx#L62) — “carries the same keys in every language” | | proven by | [`apps/web/src/i18n.test.tsx:88`](../apps/web/src/i18n.test.tsx#L88) — “gives each language the digits it declared” | @@ -529,7 +531,7 @@ Whoever is changing the app next, including this project's own future self. Arri | | | |---|---| -| happens in | [`packages/etl/scripts/build-adjacency.mjs:165`](../packages/etl/scripts/build-adjacency.mjs#L165) — `addEdge` | +| happens in | [`packages/etl/scripts/build-adjacency.mjs:167`](../packages/etl/scripts/build-adjacency.mjs#L167) — `addEdge` | | happens in | [`packages/core/src/quran-meta.ts:52`](../packages/core/src/quran-meta.ts#L52) — `toAbsoluteAyah` | | proven by | `pnpm gate:edges` | | proven by | `pnpm gate:ci-artifacts` | diff --git a/docs/validation/guide.html b/docs/validation/guide.html index b4909236..2bb96580 100644 --- a/docs/validation/guide.html +++ b/docs/validation/guide.html @@ -1,5 +1,5 @@ <!doctype html> -<html lang="en" data-ledger-hash="7ccdaa5c6d26"> +<html lang="en" data-ledger-hash="cb24e2e108b8"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> @@ -123,11 +123,11 @@ <header class="top"> <p class="kicker">Hifth · حِفظ</p> <h1>Field guide</h1> - <p class="lede">The checks a machine cannot run. 8 of 9 outstanding.</p> + <p class="lede">The checks a machine cannot run. 9 of 11 outstanding.</p> <p class="rule">Every result here must tighten something automated — a threshold, a fixture, a gate. That is what <b>tunes</b> is for at the foot of each card. A verdict that tunes nothing has to be bought again by hand, forever.</p> - <p class="src">Generated from <code>docs/validation/ledger.json</code> · <code>7ccdaa5c6d26</code> + <p class="src">Generated from <code>docs/validation/ledger.json</code> · <code>cb24e2e108b8</code> · never edit this file — run <code>make guide</code></p> </header> @@ -687,7 +687,7 @@ <h2>Placement correction by eye — a hundred forced choices between the correct <p class="id"><code>placement-correction-by-eye</code> · blocks <b>mark-C</b></p> </div> - <p class="why">The measurement says nearly every mark rectangle sits about a letter away from its own ink, and that one number per page fixes it. Only arithmetic has ever said so, and it marked its own homework: the rasteriser that found the displacement is the rasteriser that scores the correction. Nobody has looked at a single verdict. Until somebody does, the thresholds in the placement record stay unenforced, and nothing that points at a mark can ship — which is the whole reason to hold three hundred thousand rectangles.</p> + <p class="why">The measurement says nearly every mark rectangle sits about a letter away from its own ink, and that one number per page fixes it. Only arithmetic has ever said so, and it marked its own homework: the rasteriser that found the displacement is the rasteriser that scores the correction. Nobody has looked at a single verdict. Until somebody does, the thresholds in the placement record stay unenforced, and nothing that points at a mark can ship — which is the whole reason to hold three hundred thousand rectangles. And the hundred trials are drawn from pages the correction was never fitted on, because a correction asked about the pages it was fitted to answers a question nobody needed: it will ship on all six hundred and four. THIS IS AN ACCEPTANCE CHECK, AND THAT IS NOT A DETAIL. It is sat ONCE, on a correction that has already been chosen by measurement, and it is never sat to help choose one. A sitting was opened on 2026-08-12 and stopped after a handful of trials because the reader found the rectangles wrong in four independent ways at once — which is a fact about the model and not something a hundred forced choices could have improved. Buying a reader's half hour to compare two placements neither of which is right is the most expensive way there is to learn something arithmetic already knew. The measurement that followed found the cause, and it is not a per-page displacement at all: the alignment between the two printings is fitted on the ayah-end ornaments and then used to place text the fit has never seen. So the setup command below is still pinned to the per-page correction, and is KNOWN to be building the wrong thing until whatever ships is decided and committed. Rebuild it from the winner before anybody sits this.</p> @@ -697,59 +697,64 @@ <h3>What you need</h3> </section> <section class="block"> <h3>Setup — on the laptop</h3> - <pre class="cmd">node packages/etl/scripts/probe-mark-ink.mjs --sample 4000 --seed 7 --pages-n 40</pre> - <p class="expect">About fifty seconds, ending in a failure code — that is expected, and it is the reason this check exists. It writes packages/etl/out/mark-shift.json: one measured displacement for each of forty pages. That file is the thing on trial.</p> -<pre class="cmd">pnpm adjudicate:marks --seed 11 --count 100</pre> - <p class="expect">packages/etl/out/mark-adjudication.html in about a second, and a line naming the seed, how many pages the trials spread over, and a short fingerprint of the displacements. Note the fingerprint. Open the file in any browser — it needs nothing running, and it reaches no network.</p> + <pre class="cmd">pnpm adjudicate:marks --seed 11 --count 100 --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json --pages 40 --out packages/etl/out/mark-adjudication.heldout.html</pre> + <p class="expect">A line reading 40 of 600 measured, 40 held out · even, then a hundred trials over forty pages in about a second, and two short fingerprints — one for the corrections, one for the pages. Note both. It writes the page and a companion file of the same name ending .pages.json. Open the page in any browser: it needs nothing running and it reaches no network.</p> </section> <section class="block"> <h3>Steps — on the phone</h3> <ol class="steps"> <li> - <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:0"><span>Read the five short answers the page opens with, then start. Do not read the code that built it first.</span></label> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:0"><span>If you are also going to sit the placing session, sit this one first.</span></label> + <p class="expect">Nothing to run. It is an order, not a command.</p> + + <p class="why">The placing session shows the same rectangles and invites you to drag them onto the mark, which is the most effective possible way to learn where our correction tends to sit. Come to these hundred trials afterwards and you are answering from that lesson rather than from the ink, and the count that comes out is a measure of how well you learned it. The reverse order costs nothing: knowing which of two rectangles you preferred does not tell you where to drag anything.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:1"><span>Read the five short answers the page opens with, then start. Do not read the code that built it first.</span></label> <p class="expect">A first trial: one mark, drawn twice side by side, a rectangle on each, and two keys.</p> <p class="why">The trials are built so that nothing on the screen says which rectangle is ours, and the answers exist nowhere until the scorer runs. Reading the builder first is the one way to spoil that, and there is no way to unspoil it — the session would have to be thrown away and rebuilt under a seed you had not seen.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:1"><span>For each trial pick the panel whose rectangle sits on the mark — A or B — or press space when you honestly cannot tell.</span></label> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:2"><span>For each trial pick the panel whose rectangle sits on the mark — A or B — or press space when you honestly cannot tell.</span></label> <p class="expect">No feedback, ever: the next trial, and a bar creeping along. Some pairs are obvious. Most are not, and a few are the same rectangle twice, where space is the only honest answer.</p> <p class="why">Feedback would turn this into training. Within twenty trials a person who is told they were right learns which way the corrected box tends to sit and answers from that rule rather than from the ink, and the count that comes out measures how well they learned it. Space is not a wasted trial: it is left out of the arithmetic rather than counted as a miss, so guessing to keep the tally up is the one thing that would cost you.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:2"><span>When the mark pokes out of both rectangles, press N to say neither closes around it — and then still pick whichever of the two is closer.</span></label> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:3"><span>When the mark pokes out of both rectangles, press N to say neither closes around it — and then still pick whichever of the two is closer.</span></label> <p class="expect">The tick lights up and clears itself on the next trial. It changes nothing else on the screen, and it never skips the trial.</p> <p class="why">A rectangle can be in the right place and still be the wrong size, and this whole session is about where they sit. Those are two faults with two different repairs, so a fourth button meaning 'wrong somehow' would blend them into one count nobody could act on — and it would eat the trials that still carry a usable answer, because which of two wrong rectangles is closer is a question you can answer. The tick is counted on its own line and the marks you tick are named, so somebody can go and look at those boxes rather than argue about whether they exist.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:3"><span>Stop whenever you are tiring. Close the tab and come back to the same file later.</span></label> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:4"><span>Stop whenever you are tiring. Close the tab and come back to the same file later.</span></label> <p class="expect">It resumes on the trial you left, with the answers you already gave. Nothing reshuffles.</p> <p class="why">Pushing through a hundred trials tired means the last thirty are answered from fatigue, and in the numbers fatigue is indistinguishable from 'the correction does not help' — it would read as a finding about our data rather than about the hour.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:4"><span>At the end, press the button that saves the ruling, and keep the file somewhere you can name on a command line.</span></label> - <p class="expect">mark-ruling-11.json in your downloads. It holds your answers, the seed, and the fingerprint — and no key, because there still isn't one.</p> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:5"><span>At the end, press the button that saves the ruling, and keep the file somewhere you can name on a command line.</span></label> + <p class="expect">mark-ruling-11.28042a4d.json in your downloads. It holds your answers, the seed, and both fingerprints — and no key, because there still isn't one. The second half of the name is the page list: two sittings under this seed that asked about different pages are different sittings, and without it they arrive in the same folder as the same file.</p> <p class="why">The ruling is deliberately worthless on its own. That is what makes it evidence: it could not have been edited towards an answer, because at the moment it was written nothing on the machine knew what the answers were.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:5"><span>Score it: pnpm adjudicate:score ~/Downloads/mark-ruling-11.json</span></label> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:6"><span>Score it: pnpm adjudicate:score ~/Downloads/mark-ruling-11.28042a4d.json --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json</span></label> <p class="expect">Four lines with intervals — as shipped, the decoy, the catches, twins — then, if you ticked any, how many marks poked out of both rectangles and the headline again without them. One sentence of verdict at the end. It exits 0 only when the session was valid and the corrected rectangle actually won.</p> - <p class="why">This is the first moment anyone knows what the right answers were, you included. If it refuses outright, the displacements were re-measured between building the page and scoring it: score against the file it was built from, or build a fresh session and work it again. A ruling scored against displacements it was not worked against comes out looking clean and means nothing.</p> + <p class="why">This is the first moment anyone knows what the right answers were, you included. The corrections have to be named because the ones sitting loose in the build directory are whatever was measured last, and this sitting was built from a particular committed file. If it refuses outright, that is the two of them disagreeing: score against the file it was built from, or build a fresh session and work it again. A ruling scored against corrections it was not worked against comes out looking clean and means nothing.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:6"><span>Bank the verdict with the record command below, whichever way it went.</span></label> + <label class="do"><input type="checkbox" data-step="placement-correction-by-eye:7"><span>Bank the verdict with the record command below, whichever way it went.</span></label> <p class="expect">The ledger gains the four numbers and the date, and this check stops being pending.</p> <p class="why">A refusal is worth as much here as a confirmation and is far more likely to be dropped on the floor. 'A person looked and did not prefer our correction' is the single most valuable sentence this project could learn about the mark layer, and it is unrecoverable a week later if nobody wrote the numbers down.</p> @@ -760,26 +765,26 @@ <h3>Steps — on the phone</h3> <section class="block"> <h3>Reading the result</h3> - <ul class="bullets"><li>'The correction is supported' → adopt the per-page displacement. The two numbers per page get recorded beside the fit, and the measurement's thresholds become something a gate is allowed to enforce, which is the change this check unlocks.</li><li>'NOT supported, and the decoys say why' → the strongest result this can produce, and the reason it is worth twenty minutes. It means you could see a shift of that size perfectly well and still did not prefer ours: our correction is wrong, not the test blunt. Do not adopt it, and say so in the placement record — a rejected option with a measured reason is a settled question.</li><li>'cannot see a shift this small' → the session says nothing about the correction either way. Try once more with bigger panels (--panel 640). If the decoys still do not clear half, the difference is below what an eye resolves at this size and no number of trials will change that; the case for the correction then rests entirely on the ink arithmetic and its control, and that is what the record should say rather than pretending this was run.</li><li>'SESSION INVALID' or 'SUSPECT' → discard it and do not argue with it. The catches are rectangles a whole letter off their mark and everyone gets those right while looking; missing them means the session was worked while distracted. Rerun under a different seed, which draws different marks in a different order.</li><li>A run of 'neither box closed on it' is a second finding, and it does not change the verdict above — it starts a different question. The rectangles come from one printing and the ink from another, so a box can arrive in the right place and the wrong size, and no amount of moving it will help. If the count is more than a handful, open the named marks in the encoding inspector and compare the box against the ink before adopting anything; the repair is a re-measurement, not a displacement.</li><li>One session is one person. If the verdict is going to decide whether marks ship, a second reader under a different seed costs another twenty minutes and turns one person's eyes into a result — and two people disagreeing is itself the finding.</li></ul> + <ul class="bullets"><li>'The correction is supported' → adopt the per-page displacement. The two numbers per page get recorded beside the fit, and the measurement's thresholds become something a gate is allowed to enforce, which is the change this check unlocks.</li><li>'NOT supported, and the decoys say why' → the strongest result this can produce, and the reason it is worth twenty minutes. It means you could see a shift of that size perfectly well and still did not prefer ours: our correction is wrong, not the test blunt. Do not adopt it, and say so in the placement record — a rejected option with a measured reason is a settled question.</li><li>'cannot see a shift this small' → the session says nothing about the correction either way. Try once more with bigger panels (--panel 640). If the decoys still do not clear half, the difference is below what an eye resolves at this size and no number of trials will change that; the case for the correction then rests entirely on the ink arithmetic and its control, and that is what the record should say rather than pretending this was run.</li><li>'SESSION INVALID' or 'SUSPECT' → discard it and do not argue with it. The catches are rectangles a whole letter off their mark and everyone gets those right while looking; missing them means the session was worked while distracted. Rerun under a different seed, which draws different marks in a different order.</li><li>A run of 'neither box closed on it' is a second finding, and it does not change the verdict above — it starts a different question. The rectangles come from one printing and the ink from another, so a box can arrive in the right place and the wrong size, and no amount of moving it will help. If the count is more than a handful, open the named marks in the encoding inspector and compare the box against the ink before adopting anything; the repair is a re-measurement, not a displacement.</li><li>One session is one person. If the verdict is going to decide whether marks ship, a second reader under a different seed costs another twenty minutes and turns one person's eyes into a result — and two people disagreeing is itself the finding.</li><li>Whichever way it goes, it goes for the whole print and not for forty pages. Every mark here is on a page the correction was fitted without, so a 'supported' is a statement about pages nobody had looked at, which is the only kind worth having before shipping this on all six hundred and four. Write the held-out pages into the result: a reader a year from now cannot tell from a percentage which question it answered.</li><li>If the trials look wrong in more ways than the question asks about — off the mark AND the wrong size AND on the neighbouring mark — stop and say so, and do not work the remaining trials. That is not a bad sitting, it is the sitting telling you the correction it was built from is not the one that should ship, and it is worth more than a hundred answers would have been. It has happened once, on 2026-08-12, and finding out cost a handful of trials rather than a half hour because the reader stopped.</li></ul> </section> <section class="block record"> <h3>Record it</h3> - <pre class="cmd">make record CHECK=placement-correction-by-eye RESULT='seed 11 · 100 trials · as shipped <pct> [<lo>–<hi>], decoy <pct>, catches <pct>, twins <pct> → <supported|not supported|says nothing>'</pre> + <pre class="cmd">make record CHECK=placement-correction-by-eye RESULT='seed 11 · 100 trials over 40 held-out pages · as shipped <pct> [<lo>–<hi>], decoy <pct>, catches <pct>, twins <pct> → <supported|not supported|says nothing>'</pre> <p class="tunes-lead">Then do what it tunes:</p> <ul class="tunes"><li>packages/etl/data/pages/word-boxes.pin.json — a supported correction is two more numbers per page recorded there. This is the artifact the check exists to release.</li><li>packages/etl/scripts/probe-mark-ink.mjs — its thresholds exit non-zero today and are wired into nothing. A verdict here is the named prerequisite for turning them into a gate.</li><li>docs/design/mark-registration.md §⑦ — the recommendation is provisional pending exactly this, and §⑩ ① is this question.</li><li>packages/etl/scripts/lib/adjudication.mjs — an invalid session tunes the mix of trials, not the conclusion about the marks.</li></ul> <p class="expect">Written up in docs/design/mark-registration.md</p> </section> </article> -<article class="card" id="placement-residual-by-hand"> +<article class="card is-done" id="placement-residual-by-hand"> <div class="head"> - <span class="badge">outstanding</span> + <span class="badge ok">done</span> <h2>Placement by hand — put each rectangle where it belongs, and measure how far our correction still misses</h2> <p class="id"><code>placement-residual-by-hand</code> · blocks <b>mark-C</b></p> </div> <p class="why">The forced-choice session can settle whether our proposed move is preferred to what ships. It cannot settle whether it is far enough, because the only two answers on offer are the two we thought of: a correction that points the right way and falls a third short wins every trial and is still wrong, and nothing in that session can say so. This asks for a placement instead of a preference, and the gap between where a reader puts a rectangle and where we wanted it is a distance in page units — a number that can be applied to the boxes rather than voted on.</p> - + <p class="verdict"><b>Verdict 2026-08-12:</b> seed 23 · 60 placements over 40 pages · nearer ours 98.3% [91.1–99.7] · residual (-0.073, -0.110) units against a precision of 0.03, but clustered by the pages they came from neither component clears nought (down [-0.230, +0.009], across [-0.223, +0.077]) → adopt the correction as measured, do not apply the residual. Two further limits the sitting could not see past: the 40 pages placed on are the same 40 the correction was fitted to, so nothing here speaks for the other 564; and those pages all propose nearly the same move, so whether the correction is the right size was not measured at all. Ruled out along the way, and worth not re-buying: it is not the mark's name, not a scale error across the page, not the starting point, and not fatigue.</p> <section class="block"> @@ -805,17 +810,17 @@ <h3>Steps — on the phone</h3> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:1"><span>Read the five short answers the page opens with, then start. Do not read the code that built it first.</span></label> - <p class="expect">A first trial: one mark, one red rectangle sitting obviously off it, and a button that will not do anything yet.</p> + <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:1"><span>Read the six short answers the page opens with, then start. Do not read the code that built it first.</span></label> + <p class="expect">A first trial: a line naming one mark, a small piece of print with that mark's letters under a soft blue wash, one red rectangle sitting obviously off it, and a button that will not do anything yet.</p> <p class="why">The builder knows where we think each rectangle goes, and this page deliberately does not. Reading it first is the one way to spoil that, and the session would have to be thrown away and rebuilt under a seed you had not seen.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:2"><span>Drag the rectangle onto the mark nearest to where it started, then press enter. Arrow keys nudge it if you want to be finer; hold shift for a bigger step.</span></label> + <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:2"><span>Read the line above the picture, find the mark it names, drag the rectangle onto that mark and press enter. Arrow keys nudge it if you want to be finer; hold shift for a bigger step.</span></label> <p class="expect">The button wakes up as soon as you move it, and nothing at all happens when you place it except the next trial. No target, no snapping, no 'close'.</p> - <p class="why">The rectangle always starts in the wrong place, so leaving it alone is never an answer — that is why the button stays dead until you move. And the mark nearest where it started is the one being asked about: the starting position is a random direction away from where the app draws the box today, so it points at the right mark without pointing at the right place.</p> + <p class="why">The rectangle always starts in the wrong place, so leaving it alone is never an answer — that is why the button stays dead until you move. And a crop this size holds several marks, often two of the same name, so the line names one and the blue wash points at the letters it belongs to: a placement on the neighbouring mark is a whole letter out and would be recorded as though the boxes were a whole letter out. The wash is deliberately too soft to line the rectangle up against, because anything crisp enough to line up against would be showing you where we think the box goes.</p> </li> <li> @@ -840,15 +845,15 @@ <h3>Steps — on the phone</h3> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:6"><span>At the end, press the button that saves the placements, and keep the file somewhere you can name on a command line.</span></label> - <p class="expect">mark-placements-23.json in your downloads. It holds where you put each rectangle and where each one started, plus the seed and the fingerprint — and nothing about where we thought they went.</p> + <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:6"><span>At the end, press the button that saves the placements, then move the file into docs/validation/rulings/ named for this sitting and this seed — the transcript already there shows the pattern.</span></label> + <p class="expect">A file named for this seed and whoever placed it arrives in your downloads. It holds where you put each rectangle and where each one started, plus the seed and the fingerprint — and nothing about where we thought they went. In the repo it becomes docs/validation/rulings/<when>-placement-residual-by-hand.seed23.json.</p> - <p class="why">Same property as the forced-choice ruling, for the same reason: at the moment it was written, nothing on the machine that wrote it knew the answer, so it could not have been edited toward one.</p> + <p class="why">Same property as the forced-choice ruling, for the same reason: at the moment it was written, nothing on the machine that wrote it knew the answer, so it could not have been edited toward one. And it does not stay in a downloads folder, because every number banked below is derived from it — a verdict whose working lives on one laptop until the browser clears it is a verdict nobody else can argue with. docs/validation/rulings/README.md says what else belongs there.</p> </li> <li> - <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:7"><span>Read it: pnpm nudge:score ~/Downloads/mark-placements-23.json</span></label> - <p class="expect">A headline in the same currency the forced choice uses — how often you landed nearer our correction than the shipped box — then the residual on each axis with an interval, your own precision from the repeated marks, and how much your landings tracked their starting points. It exits 0 only when the placements went the right way.</p> + <label class="do"><input type="checkbox" data-step="placement-residual-by-hand:7"><span>Read it: pnpm nudge:score docs/validation/rulings/<the file you just moved></span></label> + <p class="expect">It opens with which pages the session can speak for, then a headline in the same currency the forced choice uses — how often you landed nearer our correction than the shipped box — then the residual on each axis with two intervals, one treating every placement as its own fact and one clustered by the page it came from. Read the clustered one. Then your own precision from the repeated marks, whether the proposed move is the right size, and a list of the things that turn out not to explain what is left over. It exits 0 only when the placements went the right way.</p> <p class="why">This is the first moment anyone knows what the correction was, you included. If it refuses outright, the displacements were re-measured between building the session and reading it: read it against the file it was built from, or build a fresh session and place them again.</p> @@ -865,17 +870,294 @@ <h3>Steps — on the phone</h3> <section class="block"> <h3>Reading the result</h3> - <ul class="bullets"><li>'the residual is inside this hand's own precision' → our correction is as close as this session can resolve. Adopt it as proposed and say in the placement record that a reader placing by hand could not distinguish it from their own wobble — which is a result, and is not the same as the correction being exactly right.</li><li>'the residual is larger than this hand's precision' → the printed pair of numbers is a further move to apply on top of what the measurement proposes. Apply it to the recorded displacements, not to the code that computes them: the arithmetic is not what is wrong, the frame it is measured against is, and burying a hand-measured constant inside a rasteriser hides that distinction from whoever reads it next.</li><li>'the correction goes the WRONG way' → stop. Do not adopt anything, and do not reach for a sign flip, which would be fitting the correction to sixty placements by one person. This contradicts the forced-choice session if that one passed, and two instruments disagreeing about the same rectangles is a finding worth more than either of them — write it in the placement record and re-measure before shipping any of it.</li><li>'no direction either way' → the placements did not separate the two positions. Look at the precision line before concluding anything: if it is large, this session measured a tired hand rather than a set of boxes, and rerunning under a different seed on a different day is the cheap answer.</li><li>'the landings track their starting points' → a hand that stopped short of where the eye said. It does not tilt the residual, because the starts are spread evenly around the shipped box, but it widens every interval above and usually means the placements were made faster than they were judged. Above about a quarter, treat the whole session as suggestive and rerun.</li><li>A run of 'the box was the wrong size' is the same second finding the forced-choice session can produce, and it belongs in the same place: the boxes come from one printing and the ink from another, so a rectangle can be in the right place and still not close around its mark. The repair is a re-measurement, not a displacement, and nothing in this check addresses it.</li><li>One session is one hand. If the residual is going to move three hundred thousand rectangles, a second person under a different seed costs another twenty-five minutes — and two hands landing in the same place is a much stronger claim than one hand landing anywhere.</li></ul> + <ul class="bullets"><li>'the residual is inside this hand's own precision' → our correction is as close as this session can resolve. Adopt it as proposed and say in the placement record that a reader placing by hand could not distinguish it from their own wobble — which is a result, and is not the same as the correction being exactly right.</li><li>'the residual is larger than this hand's precision' → a further move on top of what the measurement proposes, but only if the clustered interval says so. Beating the precision floor says the number is bigger than one hand's wobble; it does not say the number is bigger than nought, and those are different claims. Read the line under the residual: it names which components clear nought once the placements are clustered by the pages they came from, and nothing not named there may be applied. When it does say apply, apply it to the recorded displacements and not to the code that computes them — the arithmetic is not what is wrong, the frame it is measured against is, and burying a hand-measured constant inside a rasteriser hides that distinction from whoever reads it next.</li><li>'clustered by page, neither component is distinguishable from nought' → adopt the correction and leave the residual alone. Two marks on one page share that page's frame error, so a session of sixty placements over forty pages carries forty pages' worth of information; the interval that counts them as sixty is narrower than the truth. This is not a refutation — most of the interval usually still sits on one side of nought — it is the session declining to settle a distance it never had the sample for. The way to settle it is more pages, not more placements on these ones.</li><li>'undecidable from this sample' under the gain → the pages in this session all propose nearly the same correction, so nothing here separates a correction that is exactly right from one that is a fifth short. On pages like these a wrong gain and a constant residual are literally the same number. Do not read the gain, and do not buy more placements on the same pages hoping to sharpen it.</li><li>The coverage line at the top bounds everything under it. A trial cannot exist without a proposed move and a proposed move only exists for a measured page, so a placing session is always an in-sample check: it says the correction holds where it was fitted, and is structurally incapable of saying it holds anywhere else. If that line reads well short of 604, the next thing this project needs is a wider measurement, not a wider session.</li><li>'the correction goes the WRONG way' → stop. Do not adopt anything, and do not reach for a sign flip, which would be fitting the correction to sixty placements by one person. This contradicts the forced-choice session if that one passed, and two instruments disagreeing about the same rectangles is a finding worth more than either of them — write it in the placement record and re-measure before shipping any of it.</li><li>'no direction either way' → the placements did not separate the two positions. Look at the precision line before concluding anything: if it is large, this session measured a tired hand rather than a set of boxes, and rerunning under a different seed on a different day is the cheap answer.</li><li>'the landings track their starting points' → a hand that stopped short of where the eye said. It does not tilt the residual, because the starts are spread evenly around the shipped box, but it widens every interval above and usually means the placements were made faster than they were judged. Above about a quarter, treat the whole session as suggestive and rerun.</li><li>A run of 'the box was the wrong size' is the same second finding the forced-choice session can produce, and it belongs in the same place: the boxes come from one printing and the ink from another, so a rectangle can be in the right place and still not close around its mark. The repair is a re-measurement, not a displacement, and nothing in this check addresses it.</li><li>One session is one hand. If the residual is going to move three hundred thousand rectangles, a second person under a different seed costs another twenty-five minutes — and two hands landing in the same place is a much stronger claim than one hand landing anywhere.</li></ul> </section> <section class="block record"> <h3>Record it</h3> - <pre class="cmd">make record CHECK=placement-residual-by-hand RESULT='seed 23 · 60 placements · nearer ours <pct> [<lo>–<hi>] · residual (<x>, <y>) units against a precision of <p> → <adopt as proposed|adopt with the residual applied|contradicts the forced choice|says nothing>'</pre> + <pre class="cmd">make record CHECK=placement-residual-by-hand RESULT='seed 23 · <n> placements over <pages> pages · nearer ours <pct> [<lo>–<hi>] · residual (<x>, <y>) units against a precision of <p>, clustered by page <clustered intervals> → <adopt as proposed|adopt with the residual applied|contradicts the forced choice|says nothing>, and say which pages it can speak for'</pre> <p class="tunes-lead">Then do what it tunes:</p> <ul class="tunes"><li>packages/etl/data/pages/word-boxes.pin.json — the residual is the number that changes what is recorded there, where the forced choice can only say whether to record anything at all.</li><li>packages/etl/scripts/probe-mark-ink.mjs — a residual larger than a hand's precision is a measured statement that the correction it computes is short, which is the only evidence that could justify changing what it measures against.</li><li>docs/design/mark-registration.md §⑦ and §⑩ ① — §⑦'s recommendation is provisional pending a magnitude, not just a preference.</li><li>packages/etl/scripts/lib/adjudication.mjs — a session whose precision swamps its residual tunes the starting radius and the number of repeated marks, not the conclusion about the boxes.</li></ul> <p class="expect">Written up in docs/design/mark-registration.md</p> </section> </article> +<article class="card" id="placement-holds-off-its-own-pages"> + <div class="head"> + <span class="badge">outstanding</span> + <h2>Placement on pages the correction has never seen — does it still land, is it the right size, and is the leftover the page or the hand?</h2> + <p class="id"><code>placement-holds-off-its-own-pages</code> · blocks <b>mark-C</b></p> + </div> + + <p class="why">The last sitting placed sixty rectangles and answered one question decisively and three not at all. The pages placed on were the same forty the correction had been worked out from, so it could only report that the correction holds where it was already fitted — a claim nobody doubted. Those forty pages also all wanted very nearly the same move, and on pages like that a correction that is exactly right and one that is a fifth short are literally the same number, so the size was never measured. And with barely more than one rectangle per page there was no way to tell how much of the leftover distance belongs to the page and how much to the hand holding the mouse. All 604 pages have since been measured, which makes a different sitting possible: rectangles from pages no eye has ever been asked about, chosen so that the move they want is as different from page to page as the mus'haf allows.</p> + + + + <section class="block"> + <h3>What you need</h3> + <ul class="bullets"><li>The same two printings every mark check wants: the pages the app ships, and the gitignored ligature cache the mark boxes are read from. If either mark check has been run on this machine, nothing more is needed.</li><li>About twenty-five minutes for the first sitting and twelve for the second. They do not have to be the same day, and it is better if they are not.</li><li>A mouse or a trackpad you are comfortable with, or a touchscreen. Not a phone. The whole measurement is in fractions of a letter.</li><li>Nobody has to be a hafiz. It is a question about where a rectangle sits.</li><li>Ideally a second person for the third sitting, which is the only thing that can tell a print that is off by this much from one reader who places rectangles this way. It is built and it is optional: about ten minutes of somebody else's time, and this check can be recorded without it as long as the record says so.</li></ul> + </section> + <section class="block"> + <h3>Setup — on the laptop</h3> + <pre class="cmd">pnpm nudge:marks --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json --pages 40 --seed 31 --count 47 --out packages/etl/out/mark-nudge.heldout.html</pre> + <p class="expect">A line reading 40 of 600 measured, 40 held out · extremes, then 47 placements over 40 pages with 7 shown twice and none dropped. It writes the page itself and a companion file of the same name ending .pages.json. Note the two fingerprints it prints — one for the displacements, one for the pages chosen — because both have to match when it is read.</p> +<pre class="cmd">pnpm nudge:marks --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json,packages/etl/out/mark-nudge.heldout.pages.json --pages 5 --seed 37 --count 23 --out packages/etl/out/mark-nudge.structure.html</pre> + <p class="expect">5 of 600 measured, 80 held out, then 23 placements over 5 pages with 3 shown twice. Those five pages are none of the forty above and none of the original forty, so the two sittings never ask about the same page twice.</p> +<pre class="cmd">pnpm nudge:marks --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json --pages 40 --seed 31 --count 47 --reader B --out packages/etl/out/mark-nudge.heldout.B.html</pre> + <p class="expect">Word for word the same summary as the first command except that it says reader B, because it is the same sitting: same seed, same pages, same forty-seven rectangles in the same order from the same starting points. Only for the optional third sitting, and only if somebody else is going to place them.</p> + </section> + + <section class="block"> + <h3>Steps — on the phone</h3> + <ol class="steps"> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:0"><span>Before opening anything, read the four predictions in the reading notes below and satisfy yourself they were written down first.</span></label> + <p class="expect">Four statements about numbers that do not exist yet, each one saying what it would mean if it came back wrong.</p> + + <p class="why">The last sitting was read after the fact, and reading a result after the fact is how a session that measured nothing comes to look like a session that confirmed something. A prediction written before anybody places a rectangle can fail. One written afterwards cannot, whatever it says.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:1"><span>Do this on a different day from the forced-choice session, or before it — not straight after. The same goes for the previous placing session if you were the one who did it.</span></label> + <p class="expect">Nothing on the screen tells you either way. This one is on you.</p> + + <p class="why">Placing sixty rectangles teaches a hand which way the correction tends to go, and a hand that has learned the rule answers from the rule rather than from the ink. That is the one fault none of these sessions can detect afterwards, because somebody answering from a learned rule looks exactly like somebody who can see very well. It matters more here than last time: these pages were picked because they want unusual moves, and a hand carrying last time's habit would place them all as though they wanted the usual one.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:2"><span>Open the first page. Read the short answers it opens with, then start. Do not read the code that built it first.</span></label> + <p class="expect">A line naming one mark, a small piece of print with that mark's letters under a soft blue wash, one red rectangle sitting obviously off it, and a button that will not do anything yet.</p> + + <p class="why">The builder knows where we think each rectangle goes and the page deliberately does not. Reading it first is the one way to spoil that, and the sitting would have to be thrown away and rebuilt under a seed you had not seen.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:3"><span>Read the line above the picture, find the mark it names, drag the rectangle onto that mark and press enter. Arrow keys nudge it; hold shift for a bigger step. Press S if the rectangle is plainly the wrong size for its mark, and place it as well as you can anyway.</span></label> + <p class="expect">The button wakes as soon as you move it, and nothing happens when you place it except the next trial. No target, no snapping, no 'close'. Some rectangles start further out than you may remember from last time.</p> + + <p class="why">The rectangle always starts wrong, so leaving it alone is never an answer. A crop this size holds several marks and often two of the same name, so the line names one and the wash points at the letters it belongs to — a placement on the neighbour is a whole letter out and would be recorded as though the boxes were a whole letter out. And where a box sits and how big it is are two faults with two repairs; the tick keeps them apart, and never costs the placement.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:4"><span>Place each one honestly and move on. Do not labour over any single rectangle, and do not try to be consistent with what you did the last time you saw a mark.</span></label> + <p class="expect">Some marks come round a second time, starting somewhere else. You may or may not notice.</p> + + <p class="why">The pair is the scale for everything else. Two independent goes at the same mark differ by however far your hand wanders, and that spread is what separates a real offset from noise. Reproducing your earlier answer replaces a measurement of your hand with a measurement of your memory, and then nothing below has a scale. The existing mechanism measured a hand to a fiftieth of a unit last time, which is why it is unchanged here.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:5"><span>Stop whenever you are tiring. Close the tab and come back to the same file later. Save and move the first sitting before you open the second.</span></label> + <p class="expect">It resumes on the placement you left, with the ones you already made, and nothing reshuffles. The saved file lands in your downloads; it becomes docs/validation/rulings/<when>-placement-holds-off-its-own-pages.seed31.json.</p> + + <p class="why">A tired hand wanders, and a wandering hand does not read as fatigue in the output — it reads as a wide precision estimate, which quietly makes every real finding undetectable. And the two sittings are stored under different seeds because they answer different questions; scoring one against the other's answers is the mistake the fingerprints exist to catch, but only if the files are named apart in the first place.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:6"><span>Do the second sitting the same way, and save it as docs/validation/rulings/<when>-placement-holds-off-its-own-pages.seed37.json.</span></label> + <p class="expect">Twenty-three placements, four or five on each of five pages. The same five pages come round repeatedly and you will start to recognise them. That is fine and it is the point.</p> + + <p class="why">Four rectangles on one page is the only arrangement that can say how much of the leftover distance is a property of the page and how much is the hand. The previous sitting had roughly one and a half per page, which is why its clustered interval had to assume the worst and crossed nought. Recognising a page does not spoil this one, because you are still not being told where the box goes.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:7"><span>Optional, and the only part somebody else has to do: sit a second person at the third page, ask them for twenty or so placements, and let them stop whenever they like. Save it as docs/validation/rulings/<when>-placement-holds-off-its-own-pages.seed31.readerB.json. Tell them nothing about the first sitting beyond how to work the page.</span></label> + <p class="expect">The same forty-seven rectangles in the same order from the same starting positions, because it is deliberately the same build. They will not get through all of them and they do not need to — only the ones both of you placed are compared.</p> + + <p class="why">Everything else here measures one hand against the print and cannot say which of the two it is reading: 'the print is out by a tenth' and 'this reader places rectangles a tenth low' come out as the same number. Two hands separate them, and nothing else does. It has to be the identical build or there is nothing to compare mark for mark, and it has to be somebody who has not seen your answers or you are measuring how persuasive you are.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:8"><span>Read each one: pnpm nudge:score docs/validation/rulings/<the file you just moved></span></label> + <p class="expect">It opens by saying how many pages the sitting can speak for and that they were held out of the measurement, which the previous sitting could not say. Then the headline in the same currency as before, the residual with a naive and a clustered interval, your own precision from the repeats, and — this is the new part — whether the pages varied enough for the size of the correction to be readable. It should now say decidable rather than undecidable.</p> + + <p class="why">This is the first moment anybody knows what the correction was, you included. If it refuses outright, the displacements or the page selection moved between building the sitting and reading it: read it against the files it was built from, or build a fresh sitting and place them again. It refuses on either fingerprint, and it refuses again if the displacements file has no entry for a page the sitting used — because a file can carry the right fingerprint and still be the wrong file for these pages.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:9"><span>If a second person sat the third page, read the two together by adding --against and the second person's file to the command above.</span></label> + <p class="expect">A new block saying how far the two hands are from each other on each axis, and how that compares with how far each hand is from itself. If it refuses, the two sittings were not built the same way and it says which part differs — that is not something to work around; rebuild and place again.</p> + + <p class="why">This is the one reading that can say a further correction may be applied to all 604 pages rather than to nobody. Agreement inside the two hands' own wobble means the leftover belongs to the print. A wider gap means it belongs to whoever was sitting there, and the leftover must not be applied to anything — which is a finding, and a more valuable one than the number it would have replaced.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:10"><span>Compare what came back against the four predictions, and write down which of them failed.</span></label> + <p class="expect">Some of them will not hold. That is what they are for.</p> + + <p class="why">A prediction that survives is worth a sentence; a prediction that fails is worth the whole sitting, because it is the only thing that can tell us we were reasoning about the wrong quantity. The reading notes say what each failure would mean before anyone has seen a number.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-holds-off-its-own-pages:11"><span>Bank the numbers with the record command below, whichever way it went.</span></label> + <p class="expect">The ledger gains the headline, the residual, the size estimate, the page-versus-hand split and the date, and this check stops being pending.</p> + + <p class="why">Everything the mark boxes do next turns on these numbers, and a number nobody wrote down is one somebody has to buy again at half an hour of a person's attention.</p> + + </li> + </ol> + </section> + + <section class="block"> + <h3>Reading the result</h3> + <ul class="bullets"><li>PREDICTION ONE, direction on pages never fitted: nearer ours comes back at 90% or better. This is the load-bearing one. Below about 80% and the correction is a property of the forty pages it was worked out on rather than of the print, which would stop the mark shards where they stand — every page would need its own measurement checked before it could be trusted, and the cheap global story would be dead.</li><li>PREDICTION TWO, the leftover distance: it comes back either within about six hundredths of the previous sitting's (-0.073, -0.110), or at nought within about the same. Those two outcomes are different findings and this is the first sitting able to tell them apart. Near the old number means the leftover is real and belongs to the print, and the case for applying it gets much stronger. Near nought means the old number was an artefact of the particular forty pages it was measured on, and nothing should be applied at all. Notably larger than the old number in the same direction means it was being applied to the wrong thing.</li><li>PREDICTION THREE, the size: the estimate comes back at 1.0, and its interval is roughly a fifth of a unit across and an eighth down — where the previous sitting's was two thirds and could not be read at all. This is the whole reason the pages were chosen from the extremes. If the estimate lands reliably below 1, the correction is systematically short and the repair is to scale it, not to add a constant to it — which is a different repair from anything considered so far. If the interval is still too wide to read, the extremes strategy did not buy the leverage it was supposed to and the page-picking is what needs fixing, not the sitting.</li><li>PREDICTION FOUR, page versus hand, from the second sitting only: the spread of the leftover between the five pages is larger than the spread within any one of them. If it is, the clustered interval is doing the right thing and the previous sitting was honest to widen it. If the within-page spread dominates instead, the clustering was over-conservative, the previous sitting was closer to settled than it looked, and the interval should be read again.</li><li>PREDICTION FIVE, only if somebody else sat the third page: the two hands come back within about five hundredths of a unit of each other, which is roughly what two hands that each wobble by a fiftieth should manage. Inside that, the leftover distance is a fact about the print and may be applied to all 604 pages. Wider than that, it is a fact about whoever was sitting there and must be applied to nothing — no averaging the two, no splitting the difference. That second outcome is the more valuable one, because it is the only thing that can stop a number from being spread over three hundred thousand rectangles on one person's say-so.</li><li>If nobody sat the third page, this check cannot tell a print that is off by this much from one reader who places rectangles this way, and any leftover distance banked here is one hand's leftover distance. The record must say so in those words.</li><li>'the correction goes the WRONG way' on pages held out → stop, and do not reach for a sign flip. It would mean the correction was fitted to its own pages and does not describe the print, which contradicts a forced-choice session that has already passed. Two instruments disagreeing about the same rectangles is worth more than either of them; write it in the placement record and re-measure before shipping anything.</li><li>'no direction either way' → look at the precision line before concluding anything. If it is large, the sitting measured a tired hand rather than a set of boxes, and running it again under a different seed on a different day is the cheap answer.</li><li>'the landings track their starting points' → a hand that stopped short of where the eye said. It does not tilt the leftover, because the starts are spread evenly around the shipped box, but it widens every interval and usually means the placements were made faster than they were judged. Above about a quarter, treat the whole sitting as suggestive and run it again.</li><li>A run of 'the box was the wrong size' is the same second finding both earlier sessions can produce, and it belongs in the same place: the boxes come from one printing and the ink from another, so a rectangle can be in the right place and still not close around its mark. The repair is a re-measurement, not a displacement, and nothing in this check addresses it.</li><li>If the two sittings disagree with each other — the first says the leftover is real and the second says it is all one page's frame error — the second is the one to believe about structure and the first about size. They were built on pages that do not overlap precisely so that they can disagree.</li></ul> + </section> + + <section class="block record"> + <h3>Record it</h3> + <pre class="cmd">make record CHECK=placement-holds-off-its-own-pages RESULT='seed 31 · <n> placements over <pages> held-out pages · nearer ours <pct> [<lo>–<hi>] · leftover (<x>, <y>) against a precision of <p>, clustered <intervals> · size <gain> [<lo>–<hi>], <decidable|undecidable> · seed 37 · <n> placements over 5 pages · between-page spread <a> vs within-page <b> → <the correction travels and may be applied as measured|the leftover is real and should be applied|the leftover was an artefact of the original forty|the correction is short and should be scaled, not shifted|it does not travel — stop>, and then either <two hands agree to <g> against a wobble of <w>, so the leftover is the print> or <two hands differ by <g> against a wobble of <w>, so the leftover is one reader and must not be applied> or <nobody sat the third page, so this is still one hand>'</pre> + <p class="tunes-lead">Then do what it tunes:</p> + <ul class="tunes"><li>packages/etl/data/pages/word-boxes.pin.json — whether the leftover distance is applied to what is recorded there, which the previous sitting was unable to decide and this one is built to decide.</li><li>packages/etl/scripts/probe-mark-ink.mjs — its page count defaulted to forty and nothing downstream could tell. If the correction travels to pages it never saw, forty was enough and the default stands; if it does not, every page needs its own measurement checked and the default is wrong.</li><li>packages/etl/scripts/score-mark-nudge.mjs — the clustered interval currently assumes the worst about how much of the spread belongs to the page, because no sitting has ever had enough rectangles on one page to know. The second block measures it, and what it measures is what that interval should assume.</li><li>packages/etl/scripts/lib/adjudication.mjs — the page-picking strategy that drew these pages from the extremes is justified only if the size estimate comes back readable. If it is still too wide, that strategy is what failed and it is what changes.</li><li>docs/design/mark-registration.md §⑦ — its recommendation is still provisional pending a magnitude, and this is the sitting that either supplies one or says the magnitude was never there.</li><li>packages/etl/scripts/score-mark-nudge.mjs — the second reader's block prints a comparison and no threshold. What two hands on this screen actually manage is the number that belongs there, and until this sitting happens nobody has one.</li></ul> + <p class="expect">Written up in docs/design/mark-registration.md</p> + </section> +</article> +<article class="card" id="placement-what-kind-of-wrong"> + <div class="head"> + <span class="badge">outstanding</span> + <h2>What kind of wrong — the marks nothing can place, named one at a time instead of counted</h2> + <p class="id"><code>placement-what-kind-of-wrong</code> · blocks <b>mark-D</b>, <b>mark-C</b></p> + </div> + + <p class="why">Every instrument this project has built for the mark boxes answers in one currency. The forced choice answers which of two rectangles is better; the placing sittings answer how far a rectangle should move. Both are distances, and a distance cannot say the three things now left over. When the correction places each mark from its own printed ink and falls back for the ones it cannot match, about six marks in a thousand still land badly out — and of those, a third have almost no ink underneath them at all: the median share of the rectangle covered by ink is a ninth for the weak matches and a twentieth for the ones whose search ran to the edge of its window, against a fifth for every mark it did place, and between a quarter and a third of them are effectively on blank paper. A rectangle sitting on blank paper is not in the wrong place; there is nothing in that place. No distance can say so, and neither can it say that a rectangle is in the right place and the wrong size, or that what is odd is the printed page rather than anything we did. So this sitting asks for a kind of wrong rather than a number, and the reader may give more than one. It grew a second half for a plainer reason: everything above is about the six in a thousand, and nobody has ever looked at the other 99.4% at all. Those were graded by holding half the marks back from the fit and scoring on the other half, which is the right defence against a correction that fits noise and is no defence whatever against a correction that is confidently wrong in the same way everywhere — the ink says it landed on ink, and the ink is the only thing that has ever been asked. A rate of a few per cent there is thousands of marks and a fact about the whole book; the same rate in the tail is a rounding error. The expensive population is the one nobody was looking at, so it gets its own sixty and its own rate.</p> + + + + <section class="block"> + <h3>What you need</h3> + <ul class="bullets"><li>The same two printings every mark check wants: the pages the app ships, and the gitignored ligature cache the mark boxes are read from. If any mark check has been run on this machine, nothing more is needed.</li><li>About half an hour for sixty marks, and there are two sittings of sixty. They do not have to be done on the same day, or in either order. It is slower than choosing and faster than placing, because most of the time goes on recognising what you are looking at rather than on aiming.</li><li>A phone is fine, and is the device this page was drawn for. Nothing here is measured in fractions of a letter.</li><li>Nobody has to be a hafiz to say a rectangle is on blank paper. It helps for the answer about the print, and that answer may always be left unsaid.</li></ul> + </section> + <section class="block"> + <h3>Setup — on the laptop</h3> + <pre class="cmd">node packages/etl/scripts/probe-mark-ink.mjs --pages-n 604 --sample 400000 --grain line-tilt --rows-out packages/etl/out/mark-rows.line-tilt.json --out packages/etl/out/mark-ink.line-tilt.html --shift-out packages/etl/out/mark-shift.line-tilt.json</pre> + <p class="expect">Upwards of an hour: it rasterises every mark in the book, 326,515 of them over 604 pages, and the row dump is about 73 MB. Skip it entirely if you already have that file from the placement options work and have not touched it since — re-measuring between building a sitting and reading it makes both meaningless. Note the two output paths: never let this write over an existing mark-shift.json, because the two by-eye scorers pin their rulings to the displacements they were built from and would refuse a ruling made against a different measurement.</p> +<pre class="cmd">pnpm report:marks --rows packages/etl/out/mark-rows.line-tilt.json --set placed --count 60 --out packages/etl/out/mark-report.placed.html && pnpm report:marks --rows packages/etl/out/mark-rows.line-tilt.json --set fallback --count 60 --out packages/etl/out/mark-report.fallback.html</pre> + <p class="expect">About a minute each, ending in a line reading how many marks are in that set out of how many in the book, how many were drawn, and a short fingerprint of the displacements — 324638 of 326515 for the placed set and 1877 of 326515 for the fallback, 60 drawn each, at the time of writing. The two fingerprints must be the same string; if they are not, the measurement moved between the two builds and both sittings are void. Note it. Each writes a page that needs nothing running and reaches no network. Both are named explicitly here rather than left to the default, because the second would otherwise silently overwrite the first and there would be no sign of it except a sitting that asked about the wrong population.</p> +<pre class="cmd">make session CHECK=placement-what-kind-of-wrong</pre> + <p class="expect">A URL for this machine and a second one for the phone, both carrying a token. Open the tool through it and every answer lands in docs/validation/sessions/ as you give it, so closing the tab loses nothing. Optional: the page works opened straight off the filesystem and hands you a download instead, and the only thing lost that way is the running transcript.</p> + </section> + + <section class="block"> + <h3>Steps — on the phone</h3> + <ol class="steps"> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:0"><span>Read the explanation at the top once, then press the button under it to shorten it. It stays short for the rest of the sitting, and the long version is one press away whenever you want it back.</span></label> + <p class="expect">A single line in its place, and a button offering the long one back. The answer buttons sit in a strip pinned to the bottom of the screen and stay there while the picture scrolls behind them, so they are in the same place on every card whatever size that card's picture is.</p> + + <p class="why">The explanation is about 275 px of text and it sat above every card, every time, which put the button meaning nothing-is-wrong below the fold on a phone — so the commonest answer was the one you had to scroll to reach. Shortening it after the first read takes some 230 px off that. What was deliberately not done is putting the fault buttons behind a disclosure to save the same space: that would add a tap to reporting a fault while saying nothing-is-wrong stayed free, and the ratio between those two is the entire thing this sitting measures. The weight of the controls changed; what any answer costs did not.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:1"><span>Before answering anything, read the line above the picture, then look at the whole word. Press the button under the picture only once you know which letter the rectangle is sitting on.</span></label> + <p class="expect">Above the picture: the mark's name, the word it sits in, and — when the crop holds more than one mark of that name — a sentence saying which of them is meant, counted from the right. The picture itself opens wide enough to show the neighbouring letters, and the button swaps between that and a close view of the rectangle alone. The answers you have already given stay put either way.</p> + + <p class="why">The first sitting of this page was stopped after one screen because the crop was tight enough to judge a placement and too tight to say which letter was being talked about — and a reader who cannot name the letter cannot tell a rectangle on the wrong mark from a rectangle on no mark. The wide view is for identifying, the close one for judging, and doing them in that order is the whole reason there are two. The line above them took two further goes. A reader looking at a crop with several of the same mark in it asked, reasonably, which one — so the sentence that picks one out was added. Then it turned out the card already knew the containing word on ninety-one cards in a hundred and fifteen and was throwing it away, that the mark's own name reached only one of the three ways that sentence can be written, and that the whole line was set in the dimmest type on the screen. It is the question, not a footnote, and it is set like one now.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:2"><span>When nothing is wrong with a mark, say so. It is a real answer, and it is the one you should expect to give most often.</span></label> + <p class="expect">The button fills in solid with a tick, and the sitting moves on. It joins the list under the picture like any other answer and can be taken back the same way.</p> + + <p class="why">The marks are drawn at random, so most of them are fine and most answers here are this one. For a long time it could not be given at all: passing a mark silently was the verdict, which made the common case free but left the file unable to tell somebody who looked and was satisfied from somebody who had not got there yet. Sixty marks were sat that way, every one of them fine, and the transcript could only report that nobody had complained. Both are kept now — how far you got is still the denominator, and what you explicitly vouched for is reported against it.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:3"><span>Say every answer that is true about the mark, not the truest one. A mark can be in the wrong place and the wrong size and sitting on a page that is itself odd.</span></label> + <p class="expect">Each answer appears in a short list under the buttons as you give it. Nothing is exclusive and nothing advances the page except moving on yourself.</p> + + <p class="why">The single-verdict shape is what every earlier sitting had, and it is what made the residual unreadable: forced to pick one, a reader picks the most obvious, and the second fault is never recorded by anybody. These five are not a ranking, they are five different repairs in five different files, and one mark can need two of them.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:4"><span>When the rectangle is the right size and simply on the wrong mark, tap the mark it should be on. Tap it again to take that back, and tap the other half as well when the mark is printed in two pieces. Dragging it there by hand still works and so does the pad of arrows — grab the rectangle itself, because a drag that starts anywhere else scrolls the page.</span></label> + <p class="expect">A tap lights the ink you chose and puts the rectangle round it, at the size of that ink. A tap that lands on blank paper moves nothing and says so in the line under the picture — the marks are small, so aim at the stroke itself or look closer first. Resting a finger on the rectangle without moving it is a tap however long you rest there, and never a nudge. A drag follows your finger and stops where you let go. Either way the answer records where the rectangle ended up relative to the box that ships, there is a button that puts it back where it started if you would rather begin again, and it does not matter how many times you change your mind: only where it finally rests is read.</p> + + <p class="why">This is the one answer that is a distance, and it is the only one that can be applied to anything arithmetically — so it has to mean exactly one thing. A rectangle dragged because it was too small records a displacement that is really a size complaint, and that number would then be averaged in with real ones. It grabs only on the rectangle for a plainer reason: on a phone the picture is most of the screen, and a page where scrolling past it silently records a placement nobody made is worse than a page that records nothing. Tapping was added after the first sitting measured what the by-hand loop costs: 127 of the 128 rectangles were moved, over 226 separate goes, a median of two units of travel each. Choosing which mark is meant is the judgement only you can give; how far across and how far down is something the print already knows, so pointing hands that half back. What is recorded is which way you answered — and a press of the arrows on top of a tap makes it a hand-placed answer again — because the two are read apart and never averaged together.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:5"><span>Press the wrong-shape answer when the rectangle is the wrong size or the wrong proportions for its mark, and do not drag it as well unless it is also in the wrong place.</span></label> + <p class="expect">It records that and nothing else. No distance is attached to it and none is asked for.</p> + + <p class="why">Where a box sits and how big it is are two faults with two repairs, and the whole line of work behind this sitting turns on keeping them apart — a size fault expressed as a distance is a wrong number that looks like a right one. The boxes come from one printing and the ink from another, so a rectangle can be exactly where it belongs and still not close around its mark; that is a re-measurement, not a shift.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:6"><span>When you can see the ink this mark should have been boxing, press the draw answer and drag a rectangle around it from scratch.</span></label> + <p class="expect">The picture switches to a crosshair and takes the whole gesture, and the rectangle you draw is recorded with its own position and its own size. The mode ends when you move on.</p> + + <p class="why">This is the only positive answer on the page. Everything else says what is wrong; this one says where the right answer is, and it is the only thing that can settle whether a mark that matched nothing was in the wrong neighbourhood or was in the right neighbourhood with nothing there. A handful of these is worth more than any number of complaints, because they can be measured against what the search found.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:7"><span>Use the answer about the print when what is strange is the mus'haf page rather than our rectangle — a letter that looks doubled, a mark that is not where that word should carry one, ink that does not belong. Write a sentence about what you are seeing.</span></label> + <p class="expect">A box asking what looks wrong, and the sentence is kept with the answer.</p> + + <p class="why">This is the only answer on the page that is not about us, and mixing it in with the others would corrupt both: a defect in somebody else's file counted as a placement failure makes our correction look worse than it is, and a placement failure filed as a printing defect makes it look better. It goes to the issue catalog rather than into any ruling that grades a correction, which is why it is asked for in words — a count of odd pages is not something anybody can act on, and a sentence naming what was odd is.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:8"><span>Use the bank-it answer whenever none of the four fits, and do not force one that nearly does. A sentence is welcome and is not required.</span></label> + <p class="expect">It records and moves you on to the next mark.</p> + + <p class="why">A vocabulary is a guess about what the residual is made of, and this is the one answer that can prove the guess wrong. If a reader has to force a fifth of these marks into a category that nearly fits, the categories are what needs fixing, and there is no way to learn that from a page whose only options are the four we thought of.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:9"><span>Take back anything you change your mind about, using the button beside it in the list.</span></label> + <p class="expect">It leaves the list and the ones after it renumber. If you are working through a session, the transcript keeps a line saying you took it back.</p> + + <p class="why">A reader who says a thing and then unsays it has told us the mark is a hard one, and that is a fact worth more than the tidiness of pretending the first answer never happened. Erasing it would also make the transcript disagree with the saved file, and then neither could be trusted.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:10"><span>Stop whenever you are tiring and come back to the same file later.</span></label> + <p class="expect">It opens on the mark you left, with everything you have already said. Nothing reshuffles.</p> + + <p class="why">Unlike the placing sittings, tiredness here does not widen an error bar — it collapses the vocabulary. A tired reader stops distinguishing and reaches for whichever answer is nearest the thumb, and sixty marks answered that way look exactly like forty marks that genuinely all had the same fault.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:11"><span>At the end of each sitting press the save button, and if you were not working through a session move the downloaded file into docs/validation/rulings/ named for this sitting and the set it was drawn from. Two sittings, two files, and never one file for both.</span></label> + <p class="expect">Through a session it says where it was banked and needs no download. On its own it lands in your downloads, and in the repo it becomes docs/validation/rulings/<when>-placement-what-kind-of-wrong.placed.json and a second one ending .fallback.json. Each carries the seed, the fingerprint, which set it drew from, how far you got, and every answer with the mark it was about.</p> + + <p class="why">Same property as the other rulings: nothing on the machine that wrote it knows which answer was wanted, so it could not have been edited toward one. And it does not stay in a downloads folder, because everything read below is derived from it. The two files stay two because the whole reason for sitting both is to get a rate for each: the marks placed from their own ink are almost the entire book and the ones that fell back are six in a thousand, so a single rate over the pile would be a fact about how many of each the sampler happened to draw. docs/validation/rulings/README.md says what else belongs there.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:12"><span>Read each file with the scoring command, one at a time, and read the interval rather than the percentage.</span></label> + <p class="expect">Per sitting: how many marks you looked at, what was said by word with an interval on each, and one figure for how often our placement was wrong — moved, wrong shape, or boxed elsewhere. The marks you called odd in the print are named and taken out of the denominator, with the rate printed both ways underneath so you can see what that choice was worth. How much of what you looked at you explicitly vouched for is printed beside the fault rate, never in place of it. And where you corrected a rectangle it prints two distances under two separately worded sentences — how far your own hand moved the rectangle we drew, and where the rectangles ended up measured from the box before any correction — with one row per mark however many goes it took to settle. It refuses to read a file whose displacements have moved, and it refuses to read one whose answers disagree with the measurement about which set a mark came from.</p> + + <p class="why">Every share in the list below is a proportion out of sixty, and sixty cannot tell one in fifty from one in seven. Doing this by hand invites reading the point estimate, which is the number that is almost certainly wrong; the interval is the finding. The scorer also does the two pieces of bookkeeping nobody does reliably by eye — it never mixes the two sittings, and it never lets a fault in somebody else's printing be counted as a fault in our placement.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:13"><span>Open a row in the issue catalog for each mark the scorer named as odd in the print, quoting the sentence you wrote about it.</span></label> + <p class="expect">A handful at most, each printed with its page, its line and the mark's name. They are the only answers here that are about a file we did not write.</p> + + <p class="why">They are a different repair on a different timescale — a vendored corpus is re-fetched, not shifted — and they are already out of the rate you just read, which means that if they never become rows they simply disappear. That is the failure worth guarding against: an answer that was counted as not-our-fault and then filed nowhere has been used once to make our number look better and never again for anything.</p> + + </li> + <li> + <label class="do"><input type="checkbox" data-step="placement-what-kind-of-wrong:14"><span>Bank the shares with the record command below, whichever way they went.</span></label> + <p class="expect">The ledger gains what the residual is actually made of and the date, and this check stops being pending.</p> + + <p class="why">The share of the residual that is a placement problem is the number that decides whether the mark boxes are finished. Six in a thousand landing badly out is a shipping decision if most of them are marks with no ink under them, and is not one if most of them are rectangles that could have been dragged onto their marks by anybody looking.</p> + + </li> + </ol> + </section> + + <section class="block"> + <h3>Reading the result</h3> + <ul class="bullets"><li>Read the two sittings apart, always, and never average them. They come from populations 173 times different in size, so any figure over the pile is a fact about how many of each the sampler drew rather than about the mus'haf. The scorer will not produce one; do not produce one by hand either.</li><li>The placed sitting is the one that matters most, and it was sat on 2026-08-14: sixty marks, every one explicitly vouched for, no fault of any kind and no printing oddity. Read that as it stands. A clean sixty is not zero — it bounds the failure rate at roughly one in twenty, which is the honest ceiling sixty marks can buy, and a sitting that had come back with three or four faults would have been a rate of a few per cent over 324,638 marks, thousands of them, a finding about the whole book rather than about its tail. But the caveat travels with the number wherever it goes: that population is defined by a match of 0.55 or better and a displacement under three units on a mark roughly 5.6 by 3.6, so a gross error was structurally impossible on those cards before anybody looked. Sixty clean answers from a population that cannot contain the fault being looked for is weak evidence about the fault and strong evidence about something else — that the instrument works and a reader can sit it. The thresholds in mark-registration §⑦ stay untested, and what tests them is the fallback sitting.</li><li>The headline of each is a split, not a score: of the sixty, how many were moved, how many called the wrong shape, how many had their real ink pointed at, how many were about the print, and how many nobody could classify. There is no pass mark here and the page deliberately offers none. What the shares mean is below, and they are written about the fallback sitting unless they say otherwise.</li><li>Mostly moved, with the drawn rectangles landing near where the reader dragged to → these marks are a placement problem after all, and the rule that refused to place them is refusing too much. That is a statement about the match floor and the search radius rather than about the model: the search was not allowed to look far enough, or was told a match this weak could not be trusted when it could. Widening either is cheap and is measurable against these very answers.</li><li>Mostly the print, or mostly banked → the residual is not ours, and no further work on placement will move it. This is the outcome the ink measurements point at, since between a quarter and a third of this set has essentially nothing under the rectangle to be placed onto. If it comes back this way, the mark boxes are done at the accuracy they have, the remaining six in a thousand get written down as a property of the two printings, and the shipping decision stops waiting on them.</li><li>A run of wrong-shape answers → the size fault the earlier sittings kept flagging and nothing has ever measured. It belongs to the extraction rather than to the correction: the rectangle is the bounding box of a path in one printing, drawn over ink from another, so a systematic size complaint means the two printings set that mark differently and the repair is a re-measurement. Nothing in this sitting or the correction addresses it, and the count is what decides whether it is worth a piece of work of its own.</li><li>Drawn rectangles that land far from anything the search considered → the most valuable answers here, and the ones to look at individually rather than as a share. They say the search was looking in the wrong neighbourhood, which is a different failure from looking too timidly in the right one, and a single clear example is enough to justify examining the window the search is given.</li><li>An even spread across all five → the sample is heterogeneous and sixty is not enough to say what any category is made of. Do not read the shares; draw a second sixty under a different seed, and if it spreads the same way, the honest conclusion is that this residual has no single character and is best described as what is left over.</li><li>No share from the fallback sitting says how often the mark boxes are right. It draws only from the marks the correction already refused to place, which is six in a thousand chosen precisely because it is the worst of the book. Quoting it as a property of the mus'haf would be quoting the tail as the distribution — the placed sitting is the one that speaks for the book, and it speaks for it only as wide as its interval.</li><li>The two distances are never differenced, and the scorer says so in its own output because somebody will otherwise try. One is measured from the rectangle you were shown and answers how wrong our correction still looks to a person sitting in front of it. The other is measured from the box before any correction, so it holds ours and yours together, and it is the one to set beside a measurement taken off the ink because that is measured from the same starting point. The gap between them is only the correction already applied — subtract one from the other and you learn nothing while believing you have found a disagreement. And a distance and a count answer different questions: a mark called the wrong shape has no distance at all, and a mark moved a hundredth of a unit is not the same finding as one moved a whole mark-height.</li><li>How many goes it took to settle the rectangles is a finding of its own, and it is about the page rather than about the print. Twenty-six marks over two hundred and five separate nudges and drags says the controls are not letting anybody place a rectangle in one movement. Read it as a complaint about the pad, and fix the pad rather than reading the ratio into the mus'haf.</li><li>One reader is one reader, and four of these five answers are judgements rather than measurements. If a share from here is going to settle whether the mark boxes ship, a second person on the same sixty costs another twenty minutes, and two readers sorting the same marks the same way is a far stronger claim than one.</li></ul> + </section> + + <section class="block record"> + <h3>Record it</h3> + <pre class="cmd">make record CHECK=placement-what-kind-of-wrong RESULT='seed 23 · placed set: <n> looked at, our placement wrong on <p>% (95% <lo>–<hi>) · fallback set: <n> looked at, wrong on <p>% (95% <lo>–<hi>) → <the residual is a placement problem and the match floor is too strict|the residual belongs to the two printings and placement work is finished|the residual is a size fault and needs a re-measurement|no single character, sixty was not enough>, and say which issue rows the print answers became'</pre> + <p class="tunes-lead">Then do what it tunes:</p> + <ul class="tunes"><li>packages/etl/scripts/probe-mark-ink.mjs — the match floor and the search radius are the two numbers that decide which marks land in this set at all. A sitting that comes back mostly moved says the floor is too strict and the radius too small; one that comes back mostly about the print says both are right and the remainder is not ours.</li><li>docs/issues.json — every answer about the printed page is a defect in a vendored corpus and gets a row of its own. It is the only route by which a reader's eye can reach the catalog, and without it a printing defect is silently counted as our placement error.</li><li>docs/decisions.json — the mark-placement row's fallback rule is the thing being examined here. What the residual is made of decides whether that rule is a stopping point or a first attempt.</li><li>docs/design/mark-registration.md §⑦ — it can currently say how big the residual is and not what it is. This sitting is the only source for the second half, and until it is sat the section has to say so.</li><li>packages/etl/scripts/build-mark-report.mjs — the five answers are a guess about what the residual contains. A category nobody uses, or a run of banked marks that all turn out to share a fault with no button, is a measured statement that the vocabulary is wrong, and the vocabulary is what changes.</li></ul> + <p class="expect">Written up in docs/design/mark-registration.md</p> + </section> +</article> </main> <footer class="foot"> diff --git a/docs/validation/ledger.json b/docs/validation/ledger.json index 18fa86aa..1cc0080c 100644 --- a/docs/validation/ledger.json +++ b/docs/validation/ledger.json @@ -229,7 +229,7 @@ "id": "kfgqpc-terms-primary-source", "title": "Read the Complex's published terms at the source", "why": "SOURCES.md currently reads KFGQPC's terms through quran-svg's NOTICE.md, which quotes them verbatim and cites Royal Decrees 136/8 and 9/B/46356 — strong, but secondary. The app states those terms to every reader in the colophon, and an earlier paraphrase of them was wrong.", - "how": "Open qurancomplex.gov.sa in an ordinary browser. The host is unreachable from automation, and this was measured rather than assumed: a GitHub-hosted runner (Azure westus2, run 30285643491, 2026-07-27) resolves the A record to 66.9.131.70 and then times out on TCP 443 — connect rc=124, and curl returns 000 under both a curl and a desktop-Chrome User-Agent. The same failure appears from the maintainer's machine. Two unrelated networks failing at the TCP layer is not a bot filter a header could talk past, and web.archive.org is not fetchable from the tooling either, so no scheduled watcher can do this. It stays a human check permanently. Confirm the wording still says free digital/web/software use with commercial printing reserved.", + "how": "Open qurancomplex.gov.sa in an ordinary browser. The host is unreachable from automation, and this was measured rather than assumed: a GitHub-hosted runner (Azure westus2, run 30285643491, 2026-07-27) resolves the A record to 66.9.131.70 and then times out on TCP 443 — connect rc=124, and curl returns 000 under both a curl and a desktop-Chrome User-Agent. The same failure appears from the maintainer's machine. A third network confirmed it on 2026-08-16: a licensing research pass reached for the primary terms and got connect ECONNREFUSED on qurancomplex.gov.sa and on the dm., fonts. and nashrmaktabi. subdomains, having also found web.archive.org hard-blocked by its tooling. Three unrelated networks failing at the TCP layer is not a bot filter a header could talk past, and no scheduled watcher can do this. It stays a human check permanently. Confirm the wording still says free digital/web/software use with commercial printing reserved.", "owner": "user", "status": "pending", "blocks": [], @@ -526,7 +526,9 @@ "how": "Revise on a phone on two different calendar days, at least one session late enough to be near midnight, swiping the app away immediately after some taps. Then read the record back and check the days, the counts and `since`.", "owner": "user", "status": "pending", - "blocks": ["loop-7"], + "blocks": [ + "loop-7" + ], "runbook": { "needs": [ "A phone — an iPhone if you have one, since the seven-day sweep is Safari's and it is the failure this check is mostly about.", @@ -588,7 +590,7 @@ { "id": "placement-correction-by-eye", "title": "Placement correction by eye — a hundred forced choices between the corrected box and the one we ship", - "why": "The measurement says nearly every mark rectangle sits about a letter away from its own ink, and that one number per page fixes it. Only arithmetic has ever said so, and it marked its own homework: the rasteriser that found the displacement is the rasteriser that scores the correction. Nobody has looked at a single verdict. Until somebody does, the thresholds in the placement record stay unenforced, and nothing that points at a mark can ship — which is the whole reason to hold three hundred thousand rectangles.", + "why": "The measurement says nearly every mark rectangle sits about a letter away from its own ink, and that one number per page fixes it. Only arithmetic has ever said so, and it marked its own homework: the rasteriser that found the displacement is the rasteriser that scores the correction. Nobody has looked at a single verdict. Until somebody does, the thresholds in the placement record stay unenforced, and nothing that points at a mark can ship — which is the whole reason to hold three hundred thousand rectangles. And the hundred trials are drawn from pages the correction was never fitted on, because a correction asked about the pages it was fitted to answers a question nobody needed: it will ship on all six hundred and four. THIS IS AN ACCEPTANCE CHECK, AND THAT IS NOT A DETAIL. It is sat ONCE, on a correction that has already been chosen by measurement, and it is never sat to help choose one. A sitting was opened on 2026-08-12 and stopped after a handful of trials because the reader found the rectangles wrong in four independent ways at once — which is a fact about the model and not something a hundred forced choices could have improved. Buying a reader's half hour to compare two placements neither of which is right is the most expensive way there is to learn something arithmetic already knew. The measurement that followed found the cause, and it is not a per-page displacement at all: the alignment between the two printings is fitted on the ayah-end ornaments and then used to place text the fit has never seen. So the setup command below is still pinned to the per-page correction, and is KNOWN to be building the wrong thing until whatever ships is decided and committed. Rebuild it from the winner before anybody sits this.", "how": "Work a hundred two-panel trials by eye. Each shows the same mark twice with one rectangle drawn on it, and asks which rectangle sits on the mark. Usually one of them is the corrected placement and the other is what the app draws today; nothing on the screen says which, and no answer key is written anywhere — the scorer rebuilds it from the seed afterwards. Save the ruling the page hands back, score it, and bank the verdict.", "owner": "user", "status": "pending", @@ -603,21 +605,23 @@ "Nobody has to be a hafiz for this one. It is a question about where a rectangle sits, not about what the mark says." ], "tool": { - "path": "packages/etl/out/mark-adjudication.html", + "path": "packages/etl/out/mark-adjudication.heldout.html", "label": "The hundred trials", - "note": "Built by the second setup command. Opened from here, every answer you give lands in this session's transcript as you give it; opened as a file on its own, it keeps its own copy and hands you a download at the end. Either way it says nothing about how you are doing, because it does not know." + "note": "Built by the setup command. Opened from here, every answer you give lands in this session's transcript as you give it; opened as a file on its own, it keeps its own copy and hands you a download at the end. Either way it says nothing about how you are doing, because it does not know." }, "setup": [ { - "run": "node packages/etl/scripts/probe-mark-ink.mjs --sample 4000 --seed 7 --pages-n 40", - "expect": "About fifty seconds, ending in a failure code — that is expected, and it is the reason this check exists. It writes packages/etl/out/mark-shift.json: one measured displacement for each of forty pages. That file is the thing on trial." - }, - { - "run": "pnpm adjudicate:marks --seed 11 --count 100", - "expect": "packages/etl/out/mark-adjudication.html in about a second, and a line naming the seed, how many pages the trials spread over, and a short fingerprint of the displacements. Note the fingerprint. Open the file in any browser — it needs nothing running, and it reaches no network." + "run": "pnpm adjudicate:marks --seed 11 --count 100 --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json --pages 40 --out packages/etl/out/mark-adjudication.heldout.html", + "expect": "A line reading 40 of 600 measured, 40 held out · even, then a hundred trials over forty pages in about a second, and two short fingerprints — one for the corrections, one for the pages. Note both. It writes the page and a companion file of the same name ending .pages.json. Open the page in any browser: it needs nothing running and it reaches no network." } ], "steps": [ + { + "do": "If you are also going to sit the placing session, sit this one first.", + "expect": "Nothing to run. It is an order, not a command.", + "why": "The placing session shows the same rectangles and invites you to drag them onto the mark, which is the most effective possible way to learn where our correction tends to sit. Come to these hundred trials afterwards and you are answering from that lesson rather than from the ink, and the count that comes out is a measure of how well you learned it. The reverse order costs nothing: knowing which of two rectangles you preferred does not tell you where to drag anything.", + "id": "this-one-first" + }, { "do": "Read the five short answers the page opens with, then start. Do not read the code that built it first.", "expect": "A first trial: one mark, drawn twice side by side, a rectangle on each, and two keys.", @@ -644,14 +648,14 @@ }, { "do": "At the end, press the button that saves the ruling, and keep the file somewhere you can name on a command line.", - "expect": "mark-ruling-11.json in your downloads. It holds your answers, the seed, and the fingerprint — and no key, because there still isn't one.", + "expect": "mark-ruling-11.28042a4d.json in your downloads. It holds your answers, the seed, and both fingerprints — and no key, because there still isn't one. The second half of the name is the page list: two sittings under this seed that asked about different pages are different sittings, and without it they arrive in the same folder as the same file.", "why": "The ruling is deliberately worthless on its own. That is what makes it evidence: it could not have been edited towards an answer, because at the moment it was written nothing on the machine knew what the answers were.", "id": "save-the-ruling" }, { - "do": "Score it: pnpm adjudicate:score ~/Downloads/mark-ruling-11.json", + "do": "Score it: pnpm adjudicate:score ~/Downloads/mark-ruling-11.28042a4d.json --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json", "expect": "Four lines with intervals — as shipped, the decoy, the catches, twins — then, if you ticked any, how many marks poked out of both rectangles and the headline again without them. One sentence of verdict at the end. It exits 0 only when the session was valid and the corrected rectangle actually won.", - "why": "This is the first moment anyone knows what the right answers were, you included. If it refuses outright, the displacements were re-measured between building the page and scoring it: score against the file it was built from, or build a fresh session and work it again. A ruling scored against displacements it was not worked against comes out looking clean and means nothing.", + "why": "This is the first moment anyone knows what the right answers were, you included. The corrections have to be named because the ones sitting loose in the build directory are whatever was measured last, and this sitting was built from a particular committed file. If it refuses outright, that is the two of them disagreeing: score against the file it was built from, or build a fresh session and work it again. A ruling scored against corrections it was not worked against comes out looking clean and means nothing.", "id": "score-it" }, { @@ -667,9 +671,11 @@ "'cannot see a shift this small' → the session says nothing about the correction either way. Try once more with bigger panels (--panel 640). If the decoys still do not clear half, the difference is below what an eye resolves at this size and no number of trials will change that; the case for the correction then rests entirely on the ink arithmetic and its control, and that is what the record should say rather than pretending this was run.", "'SESSION INVALID' or 'SUSPECT' → discard it and do not argue with it. The catches are rectangles a whole letter off their mark and everyone gets those right while looking; missing them means the session was worked while distracted. Rerun under a different seed, which draws different marks in a different order.", "A run of 'neither box closed on it' is a second finding, and it does not change the verdict above — it starts a different question. The rectangles come from one printing and the ink from another, so a box can arrive in the right place and the wrong size, and no amount of moving it will help. If the count is more than a handful, open the named marks in the encoding inspector and compare the box against the ink before adopting anything; the repair is a re-measurement, not a displacement.", - "One session is one person. If the verdict is going to decide whether marks ship, a second reader under a different seed costs another twenty minutes and turns one person's eyes into a result — and two people disagreeing is itself the finding." + "One session is one person. If the verdict is going to decide whether marks ship, a second reader under a different seed costs another twenty minutes and turns one person's eyes into a result — and two people disagreeing is itself the finding.", + "Whichever way it goes, it goes for the whole print and not for forty pages. Every mark here is on a page the correction was fitted without, so a 'supported' is a statement about pages nobody had looked at, which is the only kind worth having before shipping this on all six hundred and four. Write the held-out pages into the result: a reader a year from now cannot tell from a percentage which question it answered.", + "If the trials look wrong in more ways than the question asks about — off the mark AND the wrong size AND on the neighbouring mark — stop and say so, and do not work the remaining trials. That is not a bad sitting, it is the sitting telling you the correction it was built from is not the one that should ship, and it is worth more than a hundred answers would have been. It has happened once, on 2026-08-12, and finding out cost a handful of trials rather than a half hour because the reader stopped." ], - "record": "make record CHECK=placement-correction-by-eye RESULT='seed 11 · 100 trials · as shipped <pct> [<lo>–<hi>], decoy <pct>, catches <pct>, twins <pct> → <supported|not supported|says nothing>'" + "record": "make record CHECK=placement-correction-by-eye RESULT='seed 11 · 100 trials over 40 held-out pages · as shipped <pct> [<lo>–<hi>], decoy <pct>, catches <pct>, twins <pct> → <supported|not supported|says nothing>'" }, "tunes": [ "packages/etl/data/pages/word-boxes.pin.json — a supported correction is two more numbers per page recorded there. This is the artifact the check exists to release.", @@ -677,7 +683,9 @@ "docs/design/mark-registration.md §⑦ — the recommendation is provisional pending exactly this, and §⑩ ① is this question.", "packages/etl/scripts/lib/adjudication.mjs — an invalid session tunes the mix of trials, not the conclusion about the marks." ], - "record": "docs/design/mark-registration.md" + "record": "docs/design/mark-registration.md", + "verifiedOn": "2026-08-12", + "result": "Not sat, and recorded so the ledger stops reading as untried. Two sittings were opened on 2026-08-12 and both were abandoned — 5 of 100 trials on the first, 9 of 100 on the second — with no verdict written and no answers file handed back, so nothing here measures whether the corrected rectangle beats the shipped one. The fourteen trials do say two things, neither of them a result. On three of the first five the reader ticked that NEITHER rectangle closed around the mark at all: a forced choice between two boxes only informs when at least one is on the thing, and at that rate what is wrong is the mix of trials rather than the correction. The fallback sittings later found the same thing at scale — 100 of 101 gradable marks faulted — so expect it again and deal the trials accordingly. Second, the nine trials of the later sitting were answered in nine seconds, a median of 893 milliseconds on a judgement that needs the eye to travel between two rectangles, which reads as clicking through rather than looking; a sitting that fast is not evidence whichever way it falls. Also on the record as a process failure: two half-transcripts for one check is the outcome the transcript register names as the worst available here, and this is it — the second sitting started fresh on a different commit instead of resuming the first." }, { "id": "placement-residual-by-hand", @@ -685,7 +693,7 @@ "why": "The forced-choice session can settle whether our proposed move is preferred to what ships. It cannot settle whether it is far enough, because the only two answers on offer are the two we thought of: a correction that points the right way and falls a third short wins every trial and is still wrong, and nothing in that session can say so. This asks for a placement instead of a preference, and the gap between where a reader puts a rectangle and where we wanted it is a distance in page units — a number that can be applied to the boxes rather than voted on.", "how": "Place about sixty rectangles by hand. Each trial shows one mark with one rectangle starting plainly displaced, and asks you to drag it onto the mark. Nothing on the screen says where we think it goes, and the correction is not in the page at all. Some marks come round a second time from a different starting point; the spread between your two goes is your own precision, and it is what everything else is read against. Save the file at the end, read it, and bank the number.", "owner": "user", - "status": "pending", + "status": "done", "blocks": [ "mark-C" ], @@ -719,15 +727,15 @@ "id": "keep-them-apart" }, { - "do": "Read the five short answers the page opens with, then start. Do not read the code that built it first.", - "expect": "A first trial: one mark, one red rectangle sitting obviously off it, and a button that will not do anything yet.", + "do": "Read the six short answers the page opens with, then start. Do not read the code that built it first.", + "expect": "A first trial: a line naming one mark, a small piece of print with that mark's letters under a soft blue wash, one red rectangle sitting obviously off it, and a button that will not do anything yet.", "why": "The builder knows where we think each rectangle goes, and this page deliberately does not. Reading it first is the one way to spoil that, and the session would have to be thrown away and rebuilt under a seed you had not seen.", "id": "start-cold-here" }, { - "do": "Drag the rectangle onto the mark nearest to where it started, then press enter. Arrow keys nudge it if you want to be finer; hold shift for a bigger step.", + "do": "Read the line above the picture, find the mark it names, drag the rectangle onto that mark and press enter. Arrow keys nudge it if you want to be finer; hold shift for a bigger step.", "expect": "The button wakes up as soon as you move it, and nothing at all happens when you place it except the next trial. No target, no snapping, no 'close'.", - "why": "The rectangle always starts in the wrong place, so leaving it alone is never an answer — that is why the button stays dead until you move. And the mark nearest where it started is the one being asked about: the starting position is a random direction away from where the app draws the box today, so it points at the right mark without pointing at the right place.", + "why": "The rectangle always starts in the wrong place, so leaving it alone is never an answer — that is why the button stays dead until you move. And a crop this size holds several marks, often two of the same name, so the line names one and the blue wash points at the letters it belongs to: a placement on the neighbouring mark is a whole letter out and would be recorded as though the boxes were a whole letter out. The wash is deliberately too soft to line the rectangle up against, because anything crisp enough to line up against would be showing you where we think the box goes.", "id": "place-them" }, { @@ -749,14 +757,14 @@ "id": "stop-when-tiring" }, { - "do": "At the end, press the button that saves the placements, and keep the file somewhere you can name on a command line.", - "expect": "mark-placements-23.json in your downloads. It holds where you put each rectangle and where each one started, plus the seed and the fingerprint — and nothing about where we thought they went.", - "why": "Same property as the forced-choice ruling, for the same reason: at the moment it was written, nothing on the machine that wrote it knew the answer, so it could not have been edited toward one.", + "do": "At the end, press the button that saves the placements, then move the file into docs/validation/rulings/ named for this sitting and this seed — the transcript already there shows the pattern.", + "expect": "A file named for this seed and whoever placed it arrives in your downloads. It holds where you put each rectangle and where each one started, plus the seed and the fingerprint — and nothing about where we thought they went. In the repo it becomes docs/validation/rulings/<when>-placement-residual-by-hand.seed23.json.", + "why": "Same property as the forced-choice ruling, for the same reason: at the moment it was written, nothing on the machine that wrote it knew the answer, so it could not have been edited toward one. And it does not stay in a downloads folder, because every number banked below is derived from it — a verdict whose working lives on one laptop until the browser clears it is a verdict nobody else can argue with. docs/validation/rulings/README.md says what else belongs there.", "id": "save-the-placements" }, { - "do": "Read it: pnpm nudge:score ~/Downloads/mark-placements-23.json", - "expect": "A headline in the same currency the forced choice uses — how often you landed nearer our correction than the shipped box — then the residual on each axis with an interval, your own precision from the repeated marks, and how much your landings tracked their starting points. It exits 0 only when the placements went the right way.", + "do": "Read it: pnpm nudge:score docs/validation/rulings/<the file you just moved>", + "expect": "It opens with which pages the session can speak for, then a headline in the same currency the forced choice uses — how often you landed nearer our correction than the shipped box — then the residual on each axis with two intervals, one treating every placement as its own fact and one clustered by the page it came from. Read the clustered one. Then your own precision from the repeated marks, whether the proposed move is the right size, and a list of the things that turn out not to explain what is left over. It exits 0 only when the placements went the right way.", "why": "This is the first moment anyone knows what the correction was, you included. If it refuses outright, the displacements were re-measured between building the session and reading it: read it against the file it was built from, or build a fresh session and place them again.", "id": "read-the-residual" }, @@ -769,14 +777,17 @@ ], "reading": [ "'the residual is inside this hand's own precision' → our correction is as close as this session can resolve. Adopt it as proposed and say in the placement record that a reader placing by hand could not distinguish it from their own wobble — which is a result, and is not the same as the correction being exactly right.", - "'the residual is larger than this hand's precision' → the printed pair of numbers is a further move to apply on top of what the measurement proposes. Apply it to the recorded displacements, not to the code that computes them: the arithmetic is not what is wrong, the frame it is measured against is, and burying a hand-measured constant inside a rasteriser hides that distinction from whoever reads it next.", + "'the residual is larger than this hand's precision' → a further move on top of what the measurement proposes, but only if the clustered interval says so. Beating the precision floor says the number is bigger than one hand's wobble; it does not say the number is bigger than nought, and those are different claims. Read the line under the residual: it names which components clear nought once the placements are clustered by the pages they came from, and nothing not named there may be applied. When it does say apply, apply it to the recorded displacements and not to the code that computes them — the arithmetic is not what is wrong, the frame it is measured against is, and burying a hand-measured constant inside a rasteriser hides that distinction from whoever reads it next.", + "'clustered by page, neither component is distinguishable from nought' → adopt the correction and leave the residual alone. Two marks on one page share that page's frame error, so a session of sixty placements over forty pages carries forty pages' worth of information; the interval that counts them as sixty is narrower than the truth. This is not a refutation — most of the interval usually still sits on one side of nought — it is the session declining to settle a distance it never had the sample for. The way to settle it is more pages, not more placements on these ones.", + "'undecidable from this sample' under the gain → the pages in this session all propose nearly the same correction, so nothing here separates a correction that is exactly right from one that is a fifth short. On pages like these a wrong gain and a constant residual are literally the same number. Do not read the gain, and do not buy more placements on the same pages hoping to sharpen it.", + "The coverage line at the top bounds everything under it. A trial cannot exist without a proposed move and a proposed move only exists for a measured page, so a placing session is always an in-sample check: it says the correction holds where it was fitted, and is structurally incapable of saying it holds anywhere else. If that line reads well short of 604, the next thing this project needs is a wider measurement, not a wider session.", "'the correction goes the WRONG way' → stop. Do not adopt anything, and do not reach for a sign flip, which would be fitting the correction to sixty placements by one person. This contradicts the forced-choice session if that one passed, and two instruments disagreeing about the same rectangles is a finding worth more than either of them — write it in the placement record and re-measure before shipping any of it.", "'no direction either way' → the placements did not separate the two positions. Look at the precision line before concluding anything: if it is large, this session measured a tired hand rather than a set of boxes, and rerunning under a different seed on a different day is the cheap answer.", "'the landings track their starting points' → a hand that stopped short of where the eye said. It does not tilt the residual, because the starts are spread evenly around the shipped box, but it widens every interval above and usually means the placements were made faster than they were judged. Above about a quarter, treat the whole session as suggestive and rerun.", "A run of 'the box was the wrong size' is the same second finding the forced-choice session can produce, and it belongs in the same place: the boxes come from one printing and the ink from another, so a rectangle can be in the right place and still not close around its mark. The repair is a re-measurement, not a displacement, and nothing in this check addresses it.", "One session is one hand. If the residual is going to move three hundred thousand rectangles, a second person under a different seed costs another twenty-five minutes — and two hands landing in the same place is a much stronger claim than one hand landing anywhere." ], - "record": "make record CHECK=placement-residual-by-hand RESULT='seed 23 · 60 placements · nearer ours <pct> [<lo>–<hi>] · residual (<x>, <y>) units against a precision of <p> → <adopt as proposed|adopt with the residual applied|contradicts the forced choice|says nothing>'" + "record": "make record CHECK=placement-residual-by-hand RESULT='seed 23 · <n> placements over <pages> pages · nearer ours <pct> [<lo>–<hi>] · residual (<x>, <y>) units against a precision of <p>, clustered by page <clustered intervals> → <adopt as proposed|adopt with the residual applied|contradicts the forced choice|says nothing>, and say which pages it can speak for'" }, "tunes": [ "packages/etl/data/pages/word-boxes.pin.json — the residual is the number that changes what is recorded there, where the forced choice can only say whether to record anything at all.", @@ -784,7 +795,301 @@ "docs/design/mark-registration.md §⑦ and §⑩ ① — §⑦'s recommendation is provisional pending a magnitude, not just a preference.", "packages/etl/scripts/lib/adjudication.mjs — a session whose precision swamps its residual tunes the starting radius and the number of repeated marks, not the conclusion about the boxes." ], + "record": "docs/design/mark-registration.md", + "verifiedOn": "2026-08-12", + "result": "seed 23 · 60 placements over 40 pages · nearer ours 98.3% [91.1–99.7] · residual (-0.073, -0.110) units against a precision of 0.03, but clustered by the pages they came from neither component clears nought (down [-0.230, +0.009], across [-0.223, +0.077]) → adopt the correction as measured, do not apply the residual. Two further limits the sitting could not see past: the 40 pages placed on are the same 40 the correction was fitted to, so nothing here speaks for the other 564; and those pages all propose nearly the same move, so whether the correction is the right size was not measured at all. Ruled out along the way, and worth not re-buying: it is not the mark's name, not a scale error across the page, not the starting point, and not fatigue." + }, + { + "id": "placement-holds-off-its-own-pages", + "title": "Placement on pages the correction has never seen — does it still land, is it the right size, and is the leftover the page or the hand?", + "why": "The last sitting placed sixty rectangles and answered one question decisively and three not at all. The pages placed on were the same forty the correction had been worked out from, so it could only report that the correction holds where it was already fitted — a claim nobody doubted. Those forty pages also all wanted very nearly the same move, and on pages like that a correction that is exactly right and one that is a fifth short are literally the same number, so the size was never measured. And with barely more than one rectangle per page there was no way to tell how much of the leftover distance belongs to the page and how much to the hand holding the mouse. All 604 pages have since been measured, which makes a different sitting possible: rectangles from pages no eye has ever been asked about, chosen so that the move they want is as different from page to page as the mus'haf allows.", + "how": "Two short sittings, built and read separately, and a third that is optional and belongs to somebody else. The first is forty-seven rectangles from forty pages that were not in the original forty, deliberately drawn from the pages that want the largest and the smallest moves rather than at random. The second is twenty-three rectangles over five pages, four or five each, which is the only way to see the page and the hand apart. The third is the first sitting again, unchanged, placed by a second person — the only thing that can tell a print that is off by this much from one reader who places rectangles this way. All of them are placed the same way as before: drag the rectangle onto the mark named above it, press enter, nothing tells you how close you were. None of them has the correction applied to it, because the point is to measure the correction, not to sit on top of it.", + "owner": "user", + "status": "pending", + "blocks": [ + "mark-C" + ], + "runbook": { + "needs": [ + "The same two printings every mark check wants: the pages the app ships, and the gitignored ligature cache the mark boxes are read from. If either mark check has been run on this machine, nothing more is needed.", + "About twenty-five minutes for the first sitting and twelve for the second. They do not have to be the same day, and it is better if they are not.", + "A mouse or a trackpad you are comfortable with, or a touchscreen. Not a phone. The whole measurement is in fractions of a letter.", + "Nobody has to be a hafiz. It is a question about where a rectangle sits.", + "Ideally a second person for the third sitting, which is the only thing that can tell a print that is off by this much from one reader who places rectangles this way. It is built and it is optional: about ten minutes of somebody else's time, and this check can be recorded without it as long as the record says so." + ], + "tool": { + "path": "packages/etl/out/mark-nudge.heldout.html", + "label": "Forty-seven placements on pages nobody has been asked about", + "note": "Built by the first setup command, along with a small companion file listing which pages it drew from. The second setup command reads that companion file so the second sitting cannot land on the same pages; the third builds the first sitting over again for a second person, identical in every way except whose answers it saves. All three open from a file, need nothing running, and reach no network. None of them knows how close you were, because the correction is stripped out before the trials reach the page." + }, + "setup": [ + { + "run": "pnpm nudge:marks --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json --pages 40 --seed 31 --count 47 --out packages/etl/out/mark-nudge.heldout.html", + "expect": "A line reading 40 of 600 measured, 40 held out · extremes, then 47 placements over 40 pages with 7 shown twice and none dropped. It writes the page itself and a companion file of the same name ending .pages.json. Note the two fingerprints it prints — one for the displacements, one for the pages chosen — because both have to match when it is read." + }, + { + "run": "pnpm nudge:marks --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json,packages/etl/out/mark-nudge.heldout.pages.json --pages 5 --seed 37 --count 23 --out packages/etl/out/mark-nudge.structure.html", + "expect": "5 of 600 measured, 80 held out, then 23 placements over 5 pages with 3 shown twice. Those five pages are none of the forty above and none of the original forty, so the two sittings never ask about the same page twice." + }, + { + "run": "pnpm nudge:marks --shift docs/validation/rulings/mark-shift.604pages.c849e72d.json --exclude docs/validation/rulings/mark-shift.40pages.c8528da9.json --pages 40 --seed 31 --count 47 --reader B --out packages/etl/out/mark-nudge.heldout.B.html", + "expect": "Word for word the same summary as the first command except that it says reader B, because it is the same sitting: same seed, same pages, same forty-seven rectangles in the same order from the same starting points. Only for the optional third sitting, and only if somebody else is going to place them." + } + ], + "steps": [ + { + "do": "Before opening anything, read the four predictions in the reading notes below and satisfy yourself they were written down first.", + "expect": "Four statements about numbers that do not exist yet, each one saying what it would mean if it came back wrong.", + "why": "The last sitting was read after the fact, and reading a result after the fact is how a session that measured nothing comes to look like a session that confirmed something. A prediction written before anybody places a rectangle can fail. One written afterwards cannot, whatever it says.", + "id": "read-the-prediction-first" + }, + { + "do": "Do this on a different day from the forced-choice session, or before it — not straight after. The same goes for the previous placing session if you were the one who did it.", + "expect": "Nothing on the screen tells you either way. This one is on you.", + "why": "Placing sixty rectangles teaches a hand which way the correction tends to go, and a hand that has learned the rule answers from the rule rather than from the ink. That is the one fault none of these sessions can detect afterwards, because somebody answering from a learned rule looks exactly like somebody who can see very well. It matters more here than last time: these pages were picked because they want unusual moves, and a hand carrying last time's habit would place them all as though they wanted the usual one.", + "id": "keep-them-apart" + }, + { + "do": "Open the first page. Read the short answers it opens with, then start. Do not read the code that built it first.", + "expect": "A line naming one mark, a small piece of print with that mark's letters under a soft blue wash, one red rectangle sitting obviously off it, and a button that will not do anything yet.", + "why": "The builder knows where we think each rectangle goes and the page deliberately does not. Reading it first is the one way to spoil that, and the sitting would have to be thrown away and rebuilt under a seed you had not seen.", + "id": "start-cold-here" + }, + { + "do": "Read the line above the picture, find the mark it names, drag the rectangle onto that mark and press enter. Arrow keys nudge it; hold shift for a bigger step. Press S if the rectangle is plainly the wrong size for its mark, and place it as well as you can anyway.", + "expect": "The button wakes as soon as you move it, and nothing happens when you place it except the next trial. No target, no snapping, no 'close'. Some rectangles start further out than you may remember from last time.", + "why": "The rectangle always starts wrong, so leaving it alone is never an answer. A crop this size holds several marks and often two of the same name, so the line names one and the wash points at the letters it belongs to — a placement on the neighbour is a whole letter out and would be recorded as though the boxes were a whole letter out. And where a box sits and how big it is are two faults with two repairs; the tick keeps them apart, and never costs the placement.", + "id": "place-them" + }, + { + "do": "Place each one honestly and move on. Do not labour over any single rectangle, and do not try to be consistent with what you did the last time you saw a mark.", + "expect": "Some marks come round a second time, starting somewhere else. You may or may not notice.", + "why": "The pair is the scale for everything else. Two independent goes at the same mark differ by however far your hand wanders, and that spread is what separates a real offset from noise. Reproducing your earlier answer replaces a measurement of your hand with a measurement of your memory, and then nothing below has a scale. The existing mechanism measured a hand to a fiftieth of a unit last time, which is why it is unchanged here.", + "id": "let-the-repeats-be-repeats" + }, + { + "do": "Stop whenever you are tiring. Close the tab and come back to the same file later. Save and move the first sitting before you open the second.", + "expect": "It resumes on the placement you left, with the ones you already made, and nothing reshuffles. The saved file lands in your downloads; it becomes docs/validation/rulings/<when>-placement-holds-off-its-own-pages.seed31.json.", + "why": "A tired hand wanders, and a wandering hand does not read as fatigue in the output — it reads as a wide precision estimate, which quietly makes every real finding undetectable. And the two sittings are stored under different seeds because they answer different questions; scoring one against the other's answers is the mistake the fingerprints exist to catch, but only if the files are named apart in the first place.", + "id": "stop-when-tiring" + }, + { + "do": "Do the second sitting the same way, and save it as docs/validation/rulings/<when>-placement-holds-off-its-own-pages.seed37.json.", + "expect": "Twenty-three placements, four or five on each of five pages. The same five pages come round repeatedly and you will start to recognise them. That is fine and it is the point.", + "why": "Four rectangles on one page is the only arrangement that can say how much of the leftover distance is a property of the page and how much is the hand. The previous sitting had roughly one and a half per page, which is why its clustered interval had to assume the worst and crossed nought. Recognising a page does not spoil this one, because you are still not being told where the box goes.", + "id": "do-the-structure-block" + }, + { + "do": "Optional, and the only part somebody else has to do: sit a second person at the third page, ask them for twenty or so placements, and let them stop whenever they like. Save it as docs/validation/rulings/<when>-placement-holds-off-its-own-pages.seed31.readerB.json. Tell them nothing about the first sitting beyond how to work the page.", + "expect": "The same forty-seven rectangles in the same order from the same starting positions, because it is deliberately the same build. They will not get through all of them and they do not need to — only the ones both of you placed are compared.", + "why": "Everything else here measures one hand against the print and cannot say which of the two it is reading: 'the print is out by a tenth' and 'this reader places rectangles a tenth low' come out as the same number. Two hands separate them, and nothing else does. It has to be the identical build or there is nothing to compare mark for mark, and it has to be somebody who has not seen your answers or you are measuring how persuasive you are.", + "id": "ask-a-second-hand" + }, + { + "do": "Read each one: pnpm nudge:score docs/validation/rulings/<the file you just moved>", + "expect": "It opens by saying how many pages the sitting can speak for and that they were held out of the measurement, which the previous sitting could not say. Then the headline in the same currency as before, the residual with a naive and a clustered interval, your own precision from the repeats, and — this is the new part — whether the pages varied enough for the size of the correction to be readable. It should now say decidable rather than undecidable.", + "why": "This is the first moment anybody knows what the correction was, you included. If it refuses outright, the displacements or the page selection moved between building the sitting and reading it: read it against the files it was built from, or build a fresh sitting and place them again. It refuses on either fingerprint, and it refuses again if the displacements file has no entry for a page the sitting used — because a file can carry the right fingerprint and still be the wrong file for these pages.", + "id": "read-the-residual" + }, + { + "do": "If a second person sat the third page, read the two together by adding --against and the second person's file to the command above.", + "expect": "A new block saying how far the two hands are from each other on each axis, and how that compares with how far each hand is from itself. If it refuses, the two sittings were not built the same way and it says which part differs — that is not something to work around; rebuild and place again.", + "why": "This is the one reading that can say a further correction may be applied to all 604 pages rather than to nobody. Agreement inside the two hands' own wobble means the leftover belongs to the print. A wider gap means it belongs to whoever was sitting there, and the leftover must not be applied to anything — which is a finding, and a more valuable one than the number it would have replaced.", + "id": "read-the-two-hands" + }, + { + "do": "Compare what came back against the four predictions, and write down which of them failed.", + "expect": "Some of them will not hold. That is what they are for.", + "why": "A prediction that survives is worth a sentence; a prediction that fails is worth the whole sitting, because it is the only thing that can tell us we were reasoning about the wrong quantity. The reading notes say what each failure would mean before anyone has seen a number.", + "id": "score-the-predictions" + }, + { + "do": "Bank the numbers with the record command below, whichever way it went.", + "expect": "The ledger gains the headline, the residual, the size estimate, the page-versus-hand split and the date, and this check stops being pending.", + "why": "Everything the mark boxes do next turns on these numbers, and a number nobody wrote down is one somebody has to buy again at half an hour of a person's attention.", + "id": "bank-the-numbers" + } + ], + "reading": [ + "PREDICTION ONE, direction on pages never fitted: nearer ours comes back at 90% or better. This is the load-bearing one. Below about 80% and the correction is a property of the forty pages it was worked out on rather than of the print, which would stop the mark shards where they stand — every page would need its own measurement checked before it could be trusted, and the cheap global story would be dead.", + "PREDICTION TWO, the leftover distance: it comes back either within about six hundredths of the previous sitting's (-0.073, -0.110), or at nought within about the same. Those two outcomes are different findings and this is the first sitting able to tell them apart. Near the old number means the leftover is real and belongs to the print, and the case for applying it gets much stronger. Near nought means the old number was an artefact of the particular forty pages it was measured on, and nothing should be applied at all. Notably larger than the old number in the same direction means it was being applied to the wrong thing.", + "PREDICTION THREE, the size: the estimate comes back at 1.0, and its interval is roughly a fifth of a unit across and an eighth down — where the previous sitting's was two thirds and could not be read at all. This is the whole reason the pages were chosen from the extremes. If the estimate lands reliably below 1, the correction is systematically short and the repair is to scale it, not to add a constant to it — which is a different repair from anything considered so far. If the interval is still too wide to read, the extremes strategy did not buy the leverage it was supposed to and the page-picking is what needs fixing, not the sitting.", + "PREDICTION FOUR, page versus hand, from the second sitting only: the spread of the leftover between the five pages is larger than the spread within any one of them. If it is, the clustered interval is doing the right thing and the previous sitting was honest to widen it. If the within-page spread dominates instead, the clustering was over-conservative, the previous sitting was closer to settled than it looked, and the interval should be read again.", + "PREDICTION FIVE, only if somebody else sat the third page: the two hands come back within about five hundredths of a unit of each other, which is roughly what two hands that each wobble by a fiftieth should manage. Inside that, the leftover distance is a fact about the print and may be applied to all 604 pages. Wider than that, it is a fact about whoever was sitting there and must be applied to nothing — no averaging the two, no splitting the difference. That second outcome is the more valuable one, because it is the only thing that can stop a number from being spread over three hundred thousand rectangles on one person's say-so.", + "If nobody sat the third page, this check cannot tell a print that is off by this much from one reader who places rectangles this way, and any leftover distance banked here is one hand's leftover distance. The record must say so in those words.", + "'the correction goes the WRONG way' on pages held out → stop, and do not reach for a sign flip. It would mean the correction was fitted to its own pages and does not describe the print, which contradicts a forced-choice session that has already passed. Two instruments disagreeing about the same rectangles is worth more than either of them; write it in the placement record and re-measure before shipping anything.", + "'no direction either way' → look at the precision line before concluding anything. If it is large, the sitting measured a tired hand rather than a set of boxes, and running it again under a different seed on a different day is the cheap answer.", + "'the landings track their starting points' → a hand that stopped short of where the eye said. It does not tilt the leftover, because the starts are spread evenly around the shipped box, but it widens every interval and usually means the placements were made faster than they were judged. Above about a quarter, treat the whole sitting as suggestive and run it again.", + "A run of 'the box was the wrong size' is the same second finding both earlier sessions can produce, and it belongs in the same place: the boxes come from one printing and the ink from another, so a rectangle can be in the right place and still not close around its mark. The repair is a re-measurement, not a displacement, and nothing in this check addresses it.", + "If the two sittings disagree with each other — the first says the leftover is real and the second says it is all one page's frame error — the second is the one to believe about structure and the first about size. They were built on pages that do not overlap precisely so that they can disagree." + ], + "record": "make record CHECK=placement-holds-off-its-own-pages RESULT='seed 31 · <n> placements over <pages> held-out pages · nearer ours <pct> [<lo>–<hi>] · leftover (<x>, <y>) against a precision of <p>, clustered <intervals> · size <gain> [<lo>–<hi>], <decidable|undecidable> · seed 37 · <n> placements over 5 pages · between-page spread <a> vs within-page <b> → <the correction travels and may be applied as measured|the leftover is real and should be applied|the leftover was an artefact of the original forty|the correction is short and should be scaled, not shifted|it does not travel — stop>, and then either <two hands agree to <g> against a wobble of <w>, so the leftover is the print> or <two hands differ by <g> against a wobble of <w>, so the leftover is one reader and must not be applied> or <nobody sat the third page, so this is still one hand>'" + }, + "tunes": [ + "packages/etl/data/pages/word-boxes.pin.json — whether the leftover distance is applied to what is recorded there, which the previous sitting was unable to decide and this one is built to decide.", + "packages/etl/scripts/probe-mark-ink.mjs — its page count defaulted to forty and nothing downstream could tell. If the correction travels to pages it never saw, forty was enough and the default stands; if it does not, every page needs its own measurement checked and the default is wrong.", + "packages/etl/scripts/score-mark-nudge.mjs — the clustered interval currently assumes the worst about how much of the spread belongs to the page, because no sitting has ever had enough rectangles on one page to know. The second block measures it, and what it measures is what that interval should assume.", + "packages/etl/scripts/lib/adjudication.mjs — the page-picking strategy that drew these pages from the extremes is justified only if the size estimate comes back readable. If it is still too wide, that strategy is what failed and it is what changes.", + "docs/design/mark-registration.md §⑦ — its recommendation is still provisional pending a magnitude, and this is the sitting that either supplies one or says the magnitude was never there.", + "packages/etl/scripts/score-mark-nudge.mjs — the second reader's block prints a comparison and no threshold. What two hands on this screen actually manage is the number that belongs there, and until this sitting happens nobody has one." + ], "record": "docs/design/mark-registration.md" + }, + { + "id": "placement-what-kind-of-wrong", + "title": "What kind of wrong — the marks nothing can place, named one at a time instead of counted", + "why": "Every instrument this project has built for the mark boxes answers in one currency. The forced choice answers which of two rectangles is better; the placing sittings answer how far a rectangle should move. Both are distances, and a distance cannot say the three things now left over. When the correction places each mark from its own printed ink and falls back for the ones it cannot match, about six marks in a thousand still land badly out — and of those, a third have almost no ink underneath them at all: the median share of the rectangle covered by ink is a ninth for the weak matches and a twentieth for the ones whose search ran to the edge of its window, against a fifth for every mark it did place, and between a quarter and a third of them are effectively on blank paper. A rectangle sitting on blank paper is not in the wrong place; there is nothing in that place. No distance can say so, and neither can it say that a rectangle is in the right place and the wrong size, or that what is odd is the printed page rather than anything we did. So this sitting asks for a kind of wrong rather than a number, and the reader may give more than one. It grew a second half for a plainer reason: everything above is about the six in a thousand, and nobody has ever looked at the other 99.4% at all. Those were graded by holding half the marks back from the fit and scoring on the other half, which is the right defence against a correction that fits noise and is no defence whatever against a correction that is confidently wrong in the same way everywhere — the ink says it landed on ink, and the ink is the only thing that has ever been asked. A rate of a few per cent there is thousands of marks and a fact about the whole book; the same rate in the tail is a rounding error. The expensive population is the one nobody was looking at, so it gets its own sixty and its own rate.", + "how": "Two sittings, drawn reproducibly, and they are two rather than one long one because they are about two different things. One is sixty crops from the marks the correction places from their own printed ink — the 99.4% nobody had ever looked at, where a fault would be a fault in almost the whole book; that one has now been sat, and what it bounds and what it cannot is under Reading the result. The other is not a sample at all. It is every one of the 1,877 marks the correction refuses to place and falls back for — the six in a thousand this check was originally written about — dealt into sixteen parts of roughly an hour each, so the tail is seen rather than estimated. Sixty would have bought an interval on a population small enough to look at in full, which is a poor trade when the whole question is what the tail is made of. Each crop shows the printed page around one mark with our rectangle on it, at whatever size the screen gives. Six answers, and the first of them is the one most marks deserve: say nothing is wrong and it is on the right mark; drag the rectangle where it belongs; say it is the wrong shape without saying where it should go; draw a rectangle around the ink that should have been boxed; say something is odd in the print itself; or bank it as one you cannot classify. Say as many of the faults as are true, take any of them back, and stop whenever you like — it resumes where you left it. For a long time there was deliberately no answer meaning *this one looks right*, because passing a mark was that answer and it made the common case cost nothing. Somebody who sat sixty marks and found nothing wrong with a single one of them showed why that is not enough: silence and *I have not got there yet* are the same bytes in the file, and a reader with no way to say a box is fine starts to wonder whether they are meant to be finding faults. So both are kept. The page still counts how far you got, and what you explicitly vouched for is reported against that count rather than instead of it. Unlike the placing sittings this one is fine on a phone: it is a question about what you are looking at, not a measurement of your hand.", + "owner": "user", + "status": "pending", + "blocks": [ + "mark-D", + "mark-C" + ], + "runbook": { + "needs": [ + "The same two printings every mark check wants: the pages the app ships, and the gitignored ligature cache the mark boxes are read from. If any mark check has been run on this machine, nothing more is needed.", + "About half an hour for sixty marks, and there are two sittings of sixty. They do not have to be done on the same day, or in either order. It is slower than choosing and faster than placing, because most of the time goes on recognising what you are looking at rather than on aiming.", + "A phone is fine, and is the device this page was drawn for. Nothing here is measured in fractions of a letter.", + "Nobody has to be a hafiz to say a rectangle is on blank paper. It helps for the answer about the print, and that answer may always be left unsaid." + ], + "tool": { + "path": "packages/etl/out/mark-report.placed.html", + "label": "Sixty marks placed from their own ink, and sixty nothing could place", + "note": "Two files, built by the second setup command — this one and mark-report.fallback.html beside it. Sit them separately and save each on its own: they are two different questions, and one pile of answers over both cannot be split apart afterwards into a rate for either. Opened from a session each posts every answer into this sitting's transcript as you give it, and the save button banks a file beside them; opened as a file on its own it keeps its own copy in the browser and offers a download at the end. It counts how many are left and nothing else — no score, no running tally, no verdict, because a reader who can see the score answers the score." + }, + "setup": [ + { + "run": "node packages/etl/scripts/probe-mark-ink.mjs --pages-n 604 --sample 400000 --grain line-tilt --rows-out packages/etl/out/mark-rows.line-tilt.json --out packages/etl/out/mark-ink.line-tilt.html --shift-out packages/etl/out/mark-shift.line-tilt.json", + "expect": "Upwards of an hour: it rasterises every mark in the book, 326,515 of them over 604 pages, and the row dump is about 73 MB. Skip it entirely if you already have that file from the placement options work and have not touched it since — re-measuring between building a sitting and reading it makes both meaningless. Note the two output paths: never let this write over an existing mark-shift.json, because the two by-eye scorers pin their rulings to the displacements they were built from and would refuse a ruling made against a different measurement." + }, + { + "run": "pnpm report:marks --rows packages/etl/out/mark-rows.line-tilt.json --set placed --count 60 --out packages/etl/out/mark-report.placed.html && pnpm report:marks --rows packages/etl/out/mark-rows.line-tilt.json --set fallback --count 60 --out packages/etl/out/mark-report.fallback.html", + "expect": "About a minute each, ending in a line reading how many marks are in that set out of how many in the book, how many were drawn, and a short fingerprint of the displacements — 324638 of 326515 for the placed set and 1877 of 326515 for the fallback, 60 drawn each, at the time of writing. The two fingerprints must be the same string; if they are not, the measurement moved between the two builds and both sittings are void. Note it. Each writes a page that needs nothing running and reaches no network. Both are named explicitly here rather than left to the default, because the second would otherwise silently overwrite the first and there would be no sign of it except a sitting that asked about the wrong population." + }, + { + "run": "make session CHECK=placement-what-kind-of-wrong", + "expect": "A URL for this machine and a second one for the phone, both carrying a token. Open the tool through it and every answer lands in docs/validation/sessions/ as you give it, so closing the tab loses nothing. Optional: the page works opened straight off the filesystem and hands you a download instead, and the only thing lost that way is the running transcript." + } + ], + "steps": [ + { + "do": "Read the explanation at the top once, then press the button under it to shorten it. It stays short for the rest of the sitting, and the long version is one press away whenever you want it back.", + "expect": "A single line in its place, and a button offering the long one back. The answer buttons sit in a strip pinned to the bottom of the screen and stay there while the picture scrolls behind them, so they are in the same place on every card whatever size that card's picture is.", + "why": "The explanation is about 275 px of text and it sat above every card, every time, which put the button meaning nothing-is-wrong below the fold on a phone — so the commonest answer was the one you had to scroll to reach. Shortening it after the first read takes some 230 px off that. What was deliberately not done is putting the fault buttons behind a disclosure to save the same space: that would add a tap to reporting a fault while saying nothing-is-wrong stayed free, and the ratio between those two is the entire thing this sitting measures. The weight of the controls changed; what any answer costs did not.", + "id": "read-it-once-then-shorten-it" + }, + { + "do": "Before answering anything, read the line above the picture, then look at the whole word. Press the button under the picture only once you know which letter the rectangle is sitting on.", + "expect": "Above the picture: the mark's name, the word it sits in, and — when the crop holds more than one mark of that name — a sentence saying which of them is meant, counted from the right. The picture itself opens wide enough to show the neighbouring letters, and the button swaps between that and a close view of the rectangle alone. The answers you have already given stay put either way.", + "why": "The first sitting of this page was stopped after one screen because the crop was tight enough to judge a placement and too tight to say which letter was being talked about — and a reader who cannot name the letter cannot tell a rectangle on the wrong mark from a rectangle on no mark. The wide view is for identifying, the close one for judging, and doing them in that order is the whole reason there are two. The line above them took two further goes. A reader looking at a crop with several of the same mark in it asked, reasonably, which one — so the sentence that picks one out was added. Then it turned out the card already knew the containing word on ninety-one cards in a hundred and fifteen and was throwing it away, that the mark's own name reached only one of the three ways that sentence can be written, and that the whole line was set in the dimmest type on the screen. It is the question, not a footnote, and it is set like one now.", + "id": "know-what-you-are-looking-at" + }, + { + "do": "When nothing is wrong with a mark, say so. It is a real answer, and it is the one you should expect to give most often.", + "expect": "The button fills in solid with a tick, and the sitting moves on. It joins the list under the picture like any other answer and can be taken back the same way.", + "why": "The marks are drawn at random, so most of them are fine and most answers here are this one. For a long time it could not be given at all: passing a mark silently was the verdict, which made the common case free but left the file unable to tell somebody who looked and was satisfied from somebody who had not got there yet. Sixty marks were sat that way, every one of them fine, and the transcript could only report that nobody had complained. Both are kept now — how far you got is still the denominator, and what you explicitly vouched for is reported against it.", + "id": "nothing-wrong-is-an-answer" + }, + { + "do": "Say every answer that is true about the mark, not the truest one. A mark can be in the wrong place and the wrong size and sitting on a page that is itself odd.", + "expect": "Each answer appears in a short list under the buttons as you give it. Nothing is exclusive and nothing advances the page except moving on yourself.", + "why": "The single-verdict shape is what every earlier sitting had, and it is what made the residual unreadable: forced to pick one, a reader picks the most obvious, and the second fault is never recorded by anybody. These five are not a ranking, they are five different repairs in five different files, and one mark can need two of them.", + "id": "say-everything-true" + }, + { + "do": "When the rectangle is the right size and simply on the wrong mark, tap the mark it should be on. Tap it again to take that back, and tap the other half as well when the mark is printed in two pieces. Dragging it there by hand still works and so does the pad of arrows — grab the rectangle itself, because a drag that starts anywhere else scrolls the page.", + "expect": "A tap lights the ink you chose and puts the rectangle round it, at the size of that ink. A tap that lands on blank paper moves nothing and says so in the line under the picture — the marks are small, so aim at the stroke itself or look closer first. Resting a finger on the rectangle without moving it is a tap however long you rest there, and never a nudge. A drag follows your finger and stops where you let go. Either way the answer records where the rectangle ended up relative to the box that ships, there is a button that puts it back where it started if you would rather begin again, and it does not matter how many times you change your mind: only where it finally rests is read.", + "why": "This is the one answer that is a distance, and it is the only one that can be applied to anything arithmetically — so it has to mean exactly one thing. A rectangle dragged because it was too small records a displacement that is really a size complaint, and that number would then be averaged in with real ones. It grabs only on the rectangle for a plainer reason: on a phone the picture is most of the screen, and a page where scrolling past it silently records a placement nobody made is worse than a page that records nothing. Tapping was added after the first sitting measured what the by-hand loop costs: 127 of the 128 rectangles were moved, over 226 separate goes, a median of two units of travel each. Choosing which mark is meant is the judgement only you can give; how far across and how far down is something the print already knows, so pointing hands that half back. What is recorded is which way you answered — and a press of the arrows on top of a tap makes it a hand-placed answer again — because the two are read apart and never averaged together.", + "id": "move-it-means-move-it" + }, + { + "do": "Press the wrong-shape answer when the rectangle is the wrong size or the wrong proportions for its mark, and do not drag it as well unless it is also in the wrong place.", + "expect": "It records that and nothing else. No distance is attached to it and none is asked for.", + "why": "Where a box sits and how big it is are two faults with two repairs, and the whole line of work behind this sitting turns on keeping them apart — a size fault expressed as a distance is a wrong number that looks like a right one. The boxes come from one printing and the ink from another, so a rectangle can be exactly where it belongs and still not close around its mark; that is a re-measurement, not a shift.", + "id": "wrong-shape-is-never-a-distance" + }, + { + "do": "When you can see the ink this mark should have been boxing, press the draw answer and drag a rectangle around it from scratch.", + "expect": "The picture switches to a crosshair and takes the whole gesture, and the rectangle you draw is recorded with its own position and its own size. The mode ends when you move on.", + "why": "This is the only positive answer on the page. Everything else says what is wrong; this one says where the right answer is, and it is the only thing that can settle whether a mark that matched nothing was in the wrong neighbourhood or was in the right neighbourhood with nothing there. A handful of these is worth more than any number of complaints, because they can be measured against what the search found.", + "id": "point-at-the-right-ink" + }, + { + "do": "Use the answer about the print when what is strange is the mus'haf page rather than our rectangle — a letter that looks doubled, a mark that is not where that word should carry one, ink that does not belong. Write a sentence about what you are seeing.", + "expect": "A box asking what looks wrong, and the sentence is kept with the answer.", + "why": "This is the only answer on the page that is not about us, and mixing it in with the others would corrupt both: a defect in somebody else's file counted as a placement failure makes our correction look worse than it is, and a placement failure filed as a printing defect makes it look better. It goes to the issue catalog rather than into any ruling that grades a correction, which is why it is asked for in words — a count of odd pages is not something anybody can act on, and a sentence naming what was odd is.", + "id": "the-print-is-not-us" + }, + { + "do": "Use the bank-it answer whenever none of the four fits, and do not force one that nearly does. A sentence is welcome and is not required.", + "expect": "It records and moves you on to the next mark.", + "why": "A vocabulary is a guess about what the residual is made of, and this is the one answer that can prove the guess wrong. If a reader has to force a fifth of these marks into a category that nearly fits, the categories are what needs fixing, and there is no way to learn that from a page whose only options are the four we thought of.", + "id": "banking-it-is-an-answer" + }, + { + "do": "Take back anything you change your mind about, using the button beside it in the list.", + "expect": "It leaves the list and the ones after it renumber. If you are working through a session, the transcript keeps a line saying you took it back.", + "why": "A reader who says a thing and then unsays it has told us the mark is a hard one, and that is a fact worth more than the tidiness of pretending the first answer never happened. Erasing it would also make the transcript disagree with the saved file, and then neither could be trusted.", + "id": "changing-your-mind-is-data" + }, + { + "do": "Stop whenever you are tiring and come back to the same file later.", + "expect": "It opens on the mark you left, with everything you have already said. Nothing reshuffles.", + "why": "Unlike the placing sittings, tiredness here does not widen an error bar — it collapses the vocabulary. A tired reader stops distinguishing and reaches for whichever answer is nearest the thumb, and sixty marks answered that way look exactly like forty marks that genuinely all had the same fault.", + "id": "stop-when-tiring" + }, + { + "do": "At the end of each sitting press the save button, and if you were not working through a session move the downloaded file into docs/validation/rulings/ named for this sitting and the set it was drawn from. Two sittings, two files, and never one file for both.", + "expect": "Through a session it says where it was banked and needs no download. On its own it lands in your downloads, and in the repo it becomes docs/validation/rulings/<when>-placement-what-kind-of-wrong.placed.json and a second one ending .fallback.json. Each carries the seed, the fingerprint, which set it drew from, how far you got, and every answer with the mark it was about.", + "why": "Same property as the other rulings: nothing on the machine that wrote it knows which answer was wanted, so it could not have been edited toward one. And it does not stay in a downloads folder, because everything read below is derived from it. The two files stay two because the whole reason for sitting both is to get a rate for each: the marks placed from their own ink are almost the entire book and the ones that fell back are six in a thousand, so a single rate over the pile would be a fact about how many of each the sampler happened to draw. docs/validation/rulings/README.md says what else belongs there.", + "id": "save-what-you-said" + }, + { + "do": "Read each file with the scoring command, one at a time, and read the interval rather than the percentage.", + "expect": "Per sitting: how many marks you looked at, what was said by word with an interval on each, and one figure for how often our placement was wrong — moved, wrong shape, or boxed elsewhere. The marks you called odd in the print are named and taken out of the denominator, with the rate printed both ways underneath so you can see what that choice was worth. How much of what you looked at you explicitly vouched for is printed beside the fault rate, never in place of it. And where you corrected a rectangle it prints two distances under two separately worded sentences — how far your own hand moved the rectangle we drew, and where the rectangles ended up measured from the box before any correction — with one row per mark however many goes it took to settle. It refuses to read a file whose displacements have moved, and it refuses to read one whose answers disagree with the measurement about which set a mark came from.", + "why": "Every share in the list below is a proportion out of sixty, and sixty cannot tell one in fifty from one in seven. Doing this by hand invites reading the point estimate, which is the number that is almost certainly wrong; the interval is the finding. The scorer also does the two pieces of bookkeeping nobody does reliably by eye — it never mixes the two sittings, and it never lets a fault in somebody else's printing be counted as a fault in our placement.", + "id": "read-it-with-the-scorer" + }, + { + "do": "Open a row in the issue catalog for each mark the scorer named as odd in the print, quoting the sentence you wrote about it.", + "expect": "A handful at most, each printed with its page, its line and the mark's name. They are the only answers here that are about a file we did not write.", + "why": "They are a different repair on a different timescale — a vendored corpus is re-fetched, not shifted — and they are already out of the rate you just read, which means that if they never become rows they simply disappear. That is the failure worth guarding against: an answer that was counted as not-our-fault and then filed nowhere has been used once to make our number look better and never again for anything.", + "id": "route-the-print-defects" + }, + { + "do": "Bank the shares with the record command below, whichever way they went.", + "expect": "The ledger gains what the residual is actually made of and the date, and this check stops being pending.", + "why": "The share of the residual that is a placement problem is the number that decides whether the mark boxes are finished. Six in a thousand landing badly out is a shipping decision if most of them are marks with no ink under them, and is not one if most of them are rectangles that could have been dragged onto their marks by anybody looking.", + "id": "bank-the-shares" + } + ], + "reading": [ + "Read the two sittings apart, always, and never average them. They come from populations 173 times different in size, so any figure over the pile is a fact about how many of each the sampler drew rather than about the mus'haf. The scorer will not produce one; do not produce one by hand either.", + "The placed sitting is the one that matters most, and it was sat on 2026-08-14: sixty marks, every one explicitly vouched for, no fault of any kind and no printing oddity. Read that as it stands. A clean sixty is not zero — it bounds the failure rate at roughly one in twenty, which is the honest ceiling sixty marks can buy, and a sitting that had come back with three or four faults would have been a rate of a few per cent over 324,638 marks, thousands of them, a finding about the whole book rather than about its tail. But the caveat travels with the number wherever it goes: that population is defined by a match of 0.55 or better and a displacement under three units on a mark roughly 5.6 by 3.6, so a gross error was structurally impossible on those cards before anybody looked. Sixty clean answers from a population that cannot contain the fault being looked for is weak evidence about the fault and strong evidence about something else — that the instrument works and a reader can sit it. The thresholds in mark-registration §⑦ stay untested, and what tests them is the fallback sitting.", + "The headline of each is a split, not a score: of the sixty, how many were moved, how many called the wrong shape, how many had their real ink pointed at, how many were about the print, and how many nobody could classify. There is no pass mark here and the page deliberately offers none. What the shares mean is below, and they are written about the fallback sitting unless they say otherwise.", + "Mostly moved, with the drawn rectangles landing near where the reader dragged to → these marks are a placement problem after all, and the rule that refused to place them is refusing too much. That is a statement about the match floor and the search radius rather than about the model: the search was not allowed to look far enough, or was told a match this weak could not be trusted when it could. Widening either is cheap and is measurable against these very answers.", + "Mostly the print, or mostly banked → the residual is not ours, and no further work on placement will move it. This is the outcome the ink measurements point at, since between a quarter and a third of this set has essentially nothing under the rectangle to be placed onto. If it comes back this way, the mark boxes are done at the accuracy they have, the remaining six in a thousand get written down as a property of the two printings, and the shipping decision stops waiting on them.", + "A run of wrong-shape answers → the size fault the earlier sittings kept flagging and nothing has ever measured. It belongs to the extraction rather than to the correction: the rectangle is the bounding box of a path in one printing, drawn over ink from another, so a systematic size complaint means the two printings set that mark differently and the repair is a re-measurement. Nothing in this sitting or the correction addresses it, and the count is what decides whether it is worth a piece of work of its own.", + "Drawn rectangles that land far from anything the search considered → the most valuable answers here, and the ones to look at individually rather than as a share. They say the search was looking in the wrong neighbourhood, which is a different failure from looking too timidly in the right one, and a single clear example is enough to justify examining the window the search is given.", + "An even spread across all five → the sample is heterogeneous and sixty is not enough to say what any category is made of. Do not read the shares; draw a second sixty under a different seed, and if it spreads the same way, the honest conclusion is that this residual has no single character and is best described as what is left over.", + "No share from the fallback sitting says how often the mark boxes are right. It draws only from the marks the correction already refused to place, which is six in a thousand chosen precisely because it is the worst of the book. Quoting it as a property of the mus'haf would be quoting the tail as the distribution — the placed sitting is the one that speaks for the book, and it speaks for it only as wide as its interval.", + "The two distances are never differenced, and the scorer says so in its own output because somebody will otherwise try. One is measured from the rectangle you were shown and answers how wrong our correction still looks to a person sitting in front of it. The other is measured from the box before any correction, so it holds ours and yours together, and it is the one to set beside a measurement taken off the ink because that is measured from the same starting point. The gap between them is only the correction already applied — subtract one from the other and you learn nothing while believing you have found a disagreement. And a distance and a count answer different questions: a mark called the wrong shape has no distance at all, and a mark moved a hundredth of a unit is not the same finding as one moved a whole mark-height.", + "How many goes it took to settle the rectangles is a finding of its own, and it is about the page rather than about the print. Twenty-six marks over two hundred and five separate nudges and drags says the controls are not letting anybody place a rectangle in one movement. Read it as a complaint about the pad, and fix the pad rather than reading the ratio into the mus'haf.", + "One reader is one reader, and four of these five answers are judgements rather than measurements. If a share from here is going to settle whether the mark boxes ship, a second person on the same sixty costs another twenty minutes, and two readers sorting the same marks the same way is a far stronger claim than one." + ], + "record": "make record CHECK=placement-what-kind-of-wrong RESULT='seed 23 · placed set: <n> looked at, our placement wrong on <p>% (95% <lo>–<hi>) · fallback set: <n> looked at, wrong on <p>% (95% <lo>–<hi>) → <the residual is a placement problem and the match floor is too strict|the residual belongs to the two printings and placement work is finished|the residual is a size fault and needs a re-measurement|no single character, sixty was not enough>, and say which issue rows the print answers became'" + }, + "tunes": [ + "packages/etl/scripts/probe-mark-ink.mjs — the match floor and the search radius are the two numbers that decide which marks land in this set at all. A sitting that comes back mostly moved says the floor is too strict and the radius too small; one that comes back mostly about the print says both are right and the remainder is not ours.", + "docs/issues.json — every answer about the printed page is a defect in a vendored corpus and gets a row of its own. It is the only route by which a reader's eye can reach the catalog, and without it a printing defect is silently counted as our placement error.", + "docs/decisions.json — the mark-placement row's fallback rule is the thing being examined here. What the residual is made of decides whether that rule is a stopping point or a first attempt.", + "docs/design/mark-registration.md §⑦ — it can currently say how big the residual is and not what it is. This sitting is the only source for the second half, and until it is sat the section has to say so.", + "packages/etl/scripts/build-mark-report.mjs — the five answers are a guess about what the residual contains. A category nobody uses, or a run of banked marks that all turn out to share a fault with no button, is a measured statement that the vocabulary is wrong, and the vocabulary is what changes." + ], + "record": "docs/design/mark-registration.md", + "verifiedOn": "2026-08-17", + "result": "Two populations, read apart, and the check stays open because only 190 of 1,877 fallback marks have been sat. THE MARKS PLACED FROM THEIR OWN INK: sixty of them, every one explicitly affirmed, no faults. That bounds visible placement error at about 5 percent rather than at zero — sixty clean answers cannot say less than that — and the caveat has to travel with the number: that population is defined by a match of 0.55 or better and a displacement under 3 units on a mark 5.6 by 3.6, so a gross error was structurally impossible on those cards. It says the rule does not fail loudly where it fits; it does not say the rule is right. THE MARKS THAT FELL BACK TO THE PRINTED LINE: 160 answered so far across five sittings, 158 of them carrying a complaint. Scored on the largest single sitting, 100 of 101 gradable marks are faulted — 99.0 percent, 95 percent interval 94.6 to 99.8 — with fourteen marks held out of that denominator because the print itself was called into question. The reader moved 158 rectangles over 350 separate goes, a median of 2.105 units by hand and a worst of 12.229; reshaped 37 over 197 goes. Where those rectangles ended up, measured from the box before any correction, runs across -3.3 and down -2.6 in medians — the same direction and roughly the same size as the ink measurement, which is the corroboration worth keeping. READ IT AS A MEASUREMENT OF THE POPULATION, NOT OF THE PRINT: this set is by construction the marks the machine could not place from ink, so a near-total fault rate is what it was selected to produce. The finding is not the rate, it is that the fallback rule places almost nothing acceptably and the residual is ours rather than the printers: only 15 of 160 were called odd in the print, and those are routed to the issue catalog as a defect in vendored data. TWO DEFECTS IN THE INSTRUMENT WERE FOUND AND FIXED WHILE READING THIS: a sitting that reported how many marks were left rather than how many had been seen, and a settler that read only handed-over transcripts and so lost 25 marks that were answered on the served page and banked but never handed over. THE BOTTOM OF THE PLACED SET, SAT ON 2026-08-17 IN TWO SITTINGS OF THIRTY: drawn not at random from the placed population but from the worst of it — the band where our rectangle and the ink it settled on overlap between 0.55 and 0.65, which begins exactly at the floor below which the search gives up, and the band immediately above it, 0.65 to 0.75. All sixty faulted, sixty of sixty seen. Set beside the sixty random ones above, that is the first finding: the placed population is not uniform, the earlier sixty were drawn across the whole of it, and the bound of about 5 percent was a bound over a mix. Sampling the bottom of the same population finds everything wrong with it. BEING TWO BANDS RATHER THAN ONE IS WHAT MAKES THE REST WORTH ANYTHING, because every finding can be read twice on marks differing only in how sure the search was. EVERY HAMZA IN BOTH WAS MOVED SIDEWAYS IN THE SAME DIRECTION, eleven of eleven, width left exactly as drawn: median 1.077 page units in the weaker band, 0.573 in the better one, against 0.002 for every other mark in the sitting. Fatha and kasra came back too short in both, grown 0.600 and 0.500 in the weaker band and 0.065 and 0.215 in the better, on boxes about 3.9 tall with width untouched. And the whole sitting moves the same way: median hand distance 0.363 falling to 0.276, worst 5.877 falling to 2.053, that worst being a kasra that ended with essentially no overlap with where we drew it. SO THE OVERLAP SCORE IS DOING ITS JOB. It does not predict whether a reader complains — pointing at ink files a complaint either way, see below — but it predicts how far out we were, and the halving of the hamza error across one band step is the strongest evidence here, because it ties the error to the search rather than to a fixed per-class offset. TWO MARKS WERE CALLED ODD IN THE PRINT, 86:23 superscript alef and 463:130 wasla, and they matter out of proportion to two: the fifteen before them all came from marks the correction could not place at all, which left standing the cheap explanation that oddness and unplaceability are the same thing seen twice. These two were placed from their own ink and matched well enough to clear the bar, so whatever the reader is seeing survives a successful match. A THIRD INSTRUMENT DEFECT FOUND WHILE READING THIS, and it bounds what the rate can be read as: nearly every answer was given by pointing at ink, which is one gesture setting position and size together, and the page files a complaint for each whenever the change is anything other than exactly zero — so a point always files both words, and sixty of sixty carry both with none carrying one alone. The mark-level rate stands and is confirmed by the reader as corrections they meant. The split between the two words does not: in the weaker band four of thirty were filed the wrong size while their size changed by under 0.05 units, and four were filed misplaced while moving under 0.05. Word-level counts from a sitting worked by pointing are not quoted; the mark-level rate is. Deliberately not repaired with a threshold, because a reader who points has made one statement and the vocabulary has no word for it — which is the fifth tune on this check coming true as written. THE FIRST FULL PART, SAT 2026-08-17 ON THE REBUILT DEAL: 106 marks, one whole part of sixteen, every one from the fallback population — 274 of 1,877 now sat, 1,603 left. 91 of 91 gradable marks faulted, 100 percent, 95 percent interval 95.9 to 100, with 15 held out because the print itself was called into question. That rate is again what the population was selected to produce and is again not the finding. THE FINDING IS THAT THE TWO KINDS OF WRONG ARE NOT ON THE SAME MARKS. Median hand movement 1.479 units against a median size change of 0.130, on a box about 6.3 by 3.4 — the line-inherited rule gets the size nearly right and the position badly wrong. Sorted by the name of the mark it splits three ways: the single marks (fatha, kasra, damma, shadda, hamza — 78 of 106) moved about a unit sideways with size untouched; the marks that sit above the letter moved downward as well and far more, sukun 2.151 down and the rounded zeros 3.043 against under half a unit for every other class, so the printed line says nothing about how high above a letter a non-vowel sits; and the ten doubled marks barely moved but were grown 0.5 to 1.4 units in both directions, a box drawn for one of something that is two. That third one is the most actionable and needs no reader, because extent is something the print can be asked about directly. THE INSTRUMENT IS NOW MEASURABLY BETTER AND IT MOVED THE NUMBERS. 98 of 106 marks settled in one go, 145 goes for 106 moves against 350 goes for 158 in the sitting before it; median hand distance fell from 2.105 to 1.479 and the worst from 12.229 to 6.619. THE WHOLE CORRECTION THE ANSWERS IMPLY IS THE CORROBORATION WORTH KEEPING: across -3.170 and down -2.255, against -3.3 and -2.6 from the earlier sitting — two independent draws from the same population agreeing on direction and size, which is what a real displacement looks like and what reader noise does not. AND THE BOTH-WORDS ARTEFACT TURNS OUT TO BE A PROPERTY OF HOW CLOSE THE BOX ALREADY WAS: 1 of 106 filed misplaced while moving under 0.05 units and 7 of 104 filed the wrong size while their size changed by under 0.05, against 4 of 30 for each in the bands, with two marks carrying moved and not wrong-size — the first anywhere to carry one word without the other. So word-level counts are least trustworthy exactly where the correction was already working, which is the opposite of the guess and matters because that is most of the book. TWO THINGS DID NOT MOVE. The affirm button is still never pressed: 0 said nothing was wrong and 0 passed in silence, across every sitting so far. And the odd-in-the-print rate rose rather than fell as the page improved — 15 in 106 here against 15 in 160 before, 30 across the fallback population and 32 in all — so whatever a reader is seeing there is not something the page was doing to them, and at 14 in a hundred a full pass would set aside some 240 marks nobody can say anything about. THREE MORE BANDS OF THE PLACED SET, SAT 2026-08-19 AND 2026-08-20, CARRYING THE OVERLAP SCALE TO ITS TOP: 0.75 to 0.85 (a band of 13,935), 0.85 to 0.95 (300,028, the bulk of the whole placed population), and 0.95 to 1.01 (8,637, as tight a match as this rule ever reports), thirty marks apiece. Ninety of ninety faulted — the placed set has now failed on every mark in five bands running from the search floor to a near-exact match, so overlap with the ink is not predicting whether a reader complains, at any point on the scale tried so far. WHAT IT DOES PREDICT IS SIZE. Median hand distance across the five bands, floor to ceiling: 0.363, 0.276, 0.092, 0.081, 0.06; worst case 5.877, 2.053, 0.626, 0.441, 0.154. The steepest drop is between the second and third band, not spread evenly across all five, so whatever separates a rectangle that is merely near the ink from one that is close to identical to it happens somewhere around 0.75, not gradually. ONE MARK WAS CALLED ODD IN THE PRINT, in the 0.75-0.85 band only — 128:85, a shadda; the bands above and below it carried none between them, so the odd-in-the-print rate still tracks nothing about how well the search matched. Measured in docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.75_0.85.seed23.settled.json, .b0.85_0.95.seed23.settled.json, and .b0.95_1.01.seed23.settled.json. THE THIRD OF THE THREE WRONG-SIZE PARTS, SAT 2026-08-19 AND 2026-08-20, closing the deck part one was dealt from: 91 marks of the 329 with a weak ink match, 301 answers over 147 goes, ninety-one of ninety-one faulted. Median hand distance 0.633, worst 5.482; 89 reshaped over 129 goes — the same rough size as part one, so unlike the placed bands this population does not get better-behaved the more of it is read, which is consistent with a weak match being close to the definition of a bad correction rather than a graded one. 17 more were called odd in the print, on 17 pages between 76 and 598; see the print-oddities-called-out-by-eye issue row for the running total and the pages. Part two of three, the remaining roughly 90 marks of the 271-mark deck, is not yet sat; 58 marks of the 329 were answered before the three-way split and are not part of any numbered part. Measured in docs/validation/rulings/2026-08-20-placement-weak-size-part3.seed23.settled.json." } ] } diff --git a/docs/validation/rulings/2026-08-12T1650-placement-residual-by-hand.seed23.json b/docs/validation/rulings/2026-08-12T1650-placement-residual-by-hand.seed23.json new file mode 100644 index 00000000..166fa80d --- /dev/null +++ b/docs/validation/rulings/2026-08-12T1650-placement-residual-by-hand.seed23.json @@ -0,0 +1,862 @@ +{ + "built": "2026-08-12T17:26:53.766Z", + "kind": "nudge", + "seed": 23, + "count": 60, + "repeats": 9, + "pages": 40, + "shiftRan": "2026-08-12", + "shiftFingerprint": "c8528da9", + "asks": [ + "place", + "size" + ], + "shows": [ + "ink", + "box", + "which-mark" + ], + "finished": "2026-08-12T18:17:03.654Z", + "answers": [ + { + "i": 0, + "id": "p340k216", + "u": [ + -0.464, + -1.577 + ], + "from": [ + -2.75, + -1.2 + ], + "moves": 63, + "ms": 8063 + }, + { + "i": 1, + "id": "p336k300", + "u": [ + -1.081, + -1.046 + ], + "from": [ + -2.65, + -1.41 + ], + "moves": 70, + "ms": 12305 + }, + { + "i": 2, + "id": "p463k471", + "u": [ + -0.632, + -0.882 + ], + "from": [ + 3, + 0.17 + ], + "moves": 172, + "ms": 8241 + }, + { + "i": 3, + "id": "p115k199", + "u": [ + -0.372, + -0.894 + ], + "from": [ + 1.28, + -2.71 + ], + "moves": 150, + "ms": 9434 + }, + { + "i": 4, + "id": "p557k129", + "u": [ + -0.697, + -0.899 + ], + "from": [ + 0.3, + -2.98 + ], + "moves": 66, + "ms": 6810 + }, + { + "i": 5, + "id": "p315k187", + "u": [ + -1.49, + -1.22 + ], + "from": [ + -2.64, + -1.42 + ], + "moves": 35, + "ms": 7451 + }, + { + "i": 6, + "id": "p180k285", + "u": [ + -0.52, + -0.89 + ], + "from": [ + 1.18, + 2.76 + ], + "moves": 115, + "ms": 17714 + }, + { + "i": 7, + "id": "p314k75", + "u": [ + -0.31, + -1.03 + ], + "from": [ + -2.96, + -0.48 + ], + "moves": 68, + "ms": 8131 + }, + { + "i": 8, + "id": "p337k432", + "u": [ + -0.31, + -1.35 + ], + "from": [ + 0.09, + -3 + ], + "moves": 45, + "ms": 9019 + }, + { + "i": 9, + "id": "p326k205", + "u": [ + -0.691, + -1.911 + ], + "from": [ + 1.57, + 2.56 + ], + "moves": 57, + "ms": 48120 + }, + { + "i": 10, + "id": "p583k243", + "u": [ + -0.488, + -0.864 + ], + "from": [ + 1.74, + 2.44 + ], + "moves": 107, + "ms": 6969 + }, + { + "i": 11, + "id": "p591k154", + "u": [ + -2, + -2.32 + ], + "from": [ + -1.95, + 2.28 + ], + "moves": 93, + "ms": 7127 + }, + { + "i": 12, + "id": "p95k361", + "u": [ + -1.88, + -1.26 + ], + "from": [ + -1.23, + 2.74 + ], + "moves": 97, + "ms": 12467 + }, + { + "i": 13, + "id": "p326k358", + "u": [ + -0.34, + -1.1 + ], + "from": [ + -2.89, + -0.8 + ], + "moves": 61, + "ms": 5647 + }, + { + "i": 14, + "id": "p181k58", + "u": [ + -0.79, + -0.59 + ], + "from": [ + -0.79, + -2.89 + ], + "moves": 50, + "ms": 7171 + }, + { + "i": 15, + "id": "p150k428", + "u": [ + -0.66, + -0.98 + ], + "from": [ + 2.99, + -0.18 + ], + "moves": 89, + "ms": 9253 + }, + { + "i": 16, + "id": "p86k237", + "u": [ + -0.65, + -0.74 + ], + "from": [ + 0.6, + -2.94 + ], + "moves": 75, + "ms": 11989 + }, + { + "i": 17, + "id": "p39k402", + "u": [ + -1.19, + -1.38 + ], + "from": [ + -0.69, + 2.92 + ], + "moves": 100, + "ms": 10095 + }, + { + "i": 18, + "id": "p592k178", + "u": [ + -0.37, + -0.83 + ], + "from": [ + 2.88, + -0.83 + ], + "moves": 65, + "ms": 5112 + }, + { + "i": 19, + "id": "p501k474", + "u": [ + -1.04, + -1.104 + ], + "from": [ + 1.78, + 2.41 + ], + "moves": 142, + "ms": 37977 + }, + { + "i": 20, + "id": "p443k324", + "u": [ + -0.39, + -1.12 + ], + "from": [ + 2.66, + 1.38 + ], + "moves": 111, + "ms": 9162 + }, + { + "i": 21, + "id": "p340k216", + "u": [ + -0.475, + -1.573 + ], + "from": [ + -1.8, + 2.4 + ], + "moves": 97, + "ms": 5578 + }, + { + "i": 22, + "id": "p414k133", + "u": [ + -0.38, + -0.967 + ], + "from": [ + 3, + -0.05 + ], + "moves": 108, + "ms": 5145 + }, + { + "i": 23, + "id": "p45k14", + "u": [ + -0.537, + -1.058 + ], + "from": [ + 0.54, + 2.95 + ], + "moves": 81, + "ms": 7224 + }, + { + "i": 24, + "id": "p531k428", + "u": [ + -0.298, + -0.737 + ], + "from": [ + -2.69, + -1.33 + ], + "moves": 117, + "ms": 6831 + }, + { + "i": 25, + "id": "p93k377", + "u": [ + -0.452, + -0.785 + ], + "from": [ + 2.35, + 1.87 + ], + "moves": 114, + "ms": 5868 + }, + { + "i": 26, + "id": "p180k303", + "u": [ + -0.589, + -1.233 + ], + "from": [ + 1.3, + -2.71 + ], + "moves": 109, + "ms": 7265 + }, + { + "i": 27, + "id": "p336k355", + "u": [ + -0.483, + -1.029 + ], + "from": [ + 1.75, + -2.44 + ], + "moves": 90, + "ms": 5963 + }, + { + "i": 28, + "id": "p146k477", + "u": [ + -0.568, + -0.476 + ], + "from": [ + -2.44, + -1.75 + ], + "moves": 93, + "ms": 4797 + }, + { + "i": 29, + "id": "p24k85", + "u": [ + -0.554, + -1.042 + ], + "from": [ + 0.05, + 3 + ], + "moves": 114, + "ms": 14655 + }, + { + "i": 30, + "id": "p380k492", + "u": [ + -2.197, + -1.81 + ], + "from": [ + -0.72, + 2.91 + ], + "moves": 117, + "ms": 7422 + }, + { + "i": 31, + "id": "p115k25", + "u": [ + -0.443, + -0.895 + ], + "from": [ + -2.8, + 1.07 + ], + "moves": 103, + "ms": 5446 + }, + { + "i": 32, + "id": "p245k425", + "u": [ + -0.416, + -0.944 + ], + "from": [ + 2.73, + 1.24 + ], + "moves": 57, + "ms": 5189 + }, + { + "i": 33, + "id": "p531k141", + "u": [ + -0.617, + -1.386 + ], + "from": [ + -2.85, + -0.93 + ], + "moves": 69, + "ms": 13504 + }, + { + "i": 34, + "id": "p179k373", + "u": [ + -0.73, + -0.714 + ], + "from": [ + -2.99, + -0.28 + ], + "moves": 50, + "ms": 7295 + }, + { + "i": 35, + "id": "p115k199", + "u": [ + -0.372, + -0.937 + ], + "from": [ + -1.04, + 2.81 + ], + "moves": 97, + "ms": 11850 + }, + { + "i": 36, + "id": "p120k101", + "u": [ + -1.25, + -1.23 + ], + "from": [ + -3, + 0.12 + ], + "moves": 64, + "ms": 5691 + }, + { + "i": 37, + "id": "p218k0", + "u": [ + -0.276, + -0.395 + ], + "from": [ + 1.81, + 2.39 + ], + "moves": 124, + "ms": 4757 + }, + { + "i": 38, + "id": "p107k399", + "u": [ + -0.081, + -0.765 + ], + "from": [ + -2.59, + 1.51 + ], + "moves": 113, + "ms": 7499 + }, + { + "i": 39, + "id": "p314k75", + "u": [ + -0.321, + -1.066 + ], + "from": [ + -2.9, + -0.77 + ], + "moves": 91, + "ms": 5860 + }, + { + "i": 40, + "id": "p423k272", + "u": [ + -0.397, + -0.984 + ], + "from": [ + -0.24, + 2.99 + ], + "moves": 136, + "ms": 11968 + }, + { + "i": 41, + "id": "p552k313", + "u": [ + -1.517, + -1.295 + ], + "from": [ + 0.57, + -2.94 + ], + "moves": 91, + "ms": 6616 + }, + { + "i": 42, + "id": "p443k449", + "u": [ + -0.357, + -0.949 + ], + "from": [ + 2.57, + -1.55 + ], + "moves": 97, + "ms": 8025 + }, + { + "i": 43, + "id": "p8k501", + "u": [ + -0.794, + -0.909 + ], + "from": [ + 2.86, + 0.92 + ], + "moves": 100, + "ms": 6743 + }, + { + "i": 44, + "id": "p531k141", + "u": [ + -0.583, + -1.417 + ], + "from": [ + -2.87, + -0.88 + ], + "moves": 54, + "ms": 5459 + }, + { + "i": 45, + "id": "p464k477", + "u": [ + -2.122, + -1.109 + ], + "from": [ + 1.01, + -2.82 + ], + "moves": 65, + "ms": 8494 + }, + { + "i": 46, + "id": "p179k373", + "u": [ + -0.768, + -0.646 + ], + "from": [ + 2.95, + -0.57 + ], + "moves": 90, + "ms": 4253 + }, + { + "i": 47, + "id": "p591k154", + "u": [ + -2.007, + -2.269 + ], + "from": [ + 1.75, + 2.44 + ], + "moves": 69, + "ms": 475341 + }, + { + "i": 48, + "id": "p379k356", + "u": [ + -1.417, + -1.609 + ], + "from": [ + 2.97, + -0.43 + ], + "moves": 74, + "ms": 7214 + }, + { + "i": 49, + "id": "p146k420", + "u": [ + -1.235, + -1.274 + ], + "from": [ + 2.18, + -2.06 + ], + "moves": 150, + "ms": 6068 + }, + { + "i": 50, + "id": "p414k457", + "u": [ + -1.726, + -2.606 + ], + "from": [ + -3, + -0.11 + ], + "moves": 78, + "ms": 5631 + }, + { + "i": 51, + "id": "p157k39", + "u": [ + -1.256, + -1.131 + ], + "from": [ + 2.77, + 1.15 + ], + "moves": 97, + "ms": 14627 + }, + { + "i": 52, + "id": "p282k145", + "u": [ + -0.831, + -1.266 + ], + "from": [ + 0.7, + -2.92 + ], + "moves": 71, + "ms": 6492 + }, + { + "i": 53, + "id": "p443k449", + "u": [ + -0.381, + -0.88 + ], + "from": [ + -1.22, + 2.74 + ], + "moves": 56, + "ms": 14214 + }, + { + "i": 54, + "id": "p336k355", + "u": [ + -0.489, + -1.036 + ], + "from": [ + 0.85, + -2.88 + ], + "moves": 121, + "ms": 5945 + }, + { + "i": 55, + "id": "p93k154", + "u": [ + -0.674, + -0.661 + ], + "from": [ + 1.43, + -2.64 + ], + "moves": 98, + "ms": 4004 + }, + { + "i": 56, + "id": "p423k272", + "u": [ + -0.413, + -0.991 + ], + "from": [ + 2.79, + 1.09 + ], + "moves": 203, + "ms": 8529 + }, + { + "i": 57, + "id": "p314k410", + "u": [ + -0.46, + -0.846 + ], + "from": [ + -2.94, + -0.57 + ], + "moves": 129, + "ms": 7117 + }, + { + "i": 58, + "id": "p223k145", + "u": [ + -0.429, + -0.805 + ], + "from": [ + 2.79, + -1.1 + ], + "moves": 130, + "ms": 6404 + }, + { + "i": 59, + "id": "p157k443", + "u": [ + -0.248, + -0.844 + ], + "from": [ + 3, + 0.1 + ], + "moves": 87, + "ms": 5935 + } + ] +} \ No newline at end of file diff --git a/docs/validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json b/docs/validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json new file mode 100644 index 00000000..0220063f --- /dev/null +++ b/docs/validation/rulings/2026-08-14-placement-what-kind-of-wrong-fallback.seed23.settled.json @@ -0,0 +1,12687 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-17T18:41:28.188Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "1870ee91", + "set": "fallback", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-fallback-p1of16.23.json", + "slice": "-p1of16", + "finished": "2026-08-14T11:31:34.032Z", + "shown": 117, + "seen": 27, + "looked": 27, + "whole": false, + "answers": 281, + "spoke": 26 + }, + { + "file": "mark-report-fallback-p1of16-af801ae52.23.json", + "slice": "-p1of16-af801ae52", + "finished": "2026-08-14T14:12:09.774Z", + "shown": 115, + "seen": 5, + "looked": 5, + "whole": false, + "answers": 39, + "spoke": 5 + }, + { + "file": "mark-report-fallback-p1of16-a5ed4530c.23.json", + "slice": "-p1of16-a5ed4530c", + "finished": "2026-08-14T16:18:27.949Z", + "shown": 115, + "seen": 1, + "looked": 1, + "whole": false, + "answers": 21, + "spoke": 1 + }, + { + "file": "mark-report-fallback-p1of16-adc15c685.23.json", + "slice": "-p1of16-adc15c685", + "finished": "2026-08-14T16:30:16.170Z", + "shown": 115, + "seen": 1, + "looked": 1, + "whole": false, + "answers": 13, + "spoke": 1 + }, + { + "file": "mark-report-fallback-p1of16-a2329f82d.23.json", + "slice": "-p1of16-a2329f82d", + "finished": "2026-08-15T12:47:08.769Z", + "shown": 115, + "seen": 19, + "looked": 115, + "whole": true, + "answers": 351, + "spoke": 115 + }, + { + "file": "mark-report-fallback-p1of16-ad535a944.23.json", + "slice": "-p1of16-ad535a944", + "finished": "2026-08-15T16:42:04.525Z", + "shown": 108, + "seen": 20, + "looked": 20, + "whole": false, + "answers": 39, + "spoke": 20 + }, + { + "file": "mark-report-fallback-p1of16-a4f5ab34a.23.json", + "slice": "-p1of16-a4f5ab34a", + "finished": "2026-08-17T18:36:41.269Z", + "shown": 106, + "seen": 106, + "looked": 106, + "whole": true, + "answers": 285, + "spoke": 106 + } + ], + "banked": [], + "marks": 274, + "faulted": 272, + "defects": 30, + "answers": 1029, + "settledMarks": [ + { + "id": "13:35", + "page": 13, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 323.8, + 46.2, + 6.3, + 3.4 + ], + "drawn": [ + 322.786, + 44.781, + 6.3, + 3.4 + ], + "settled": [ + 323.186, + 45.181, + 5.9, + 4.2 + ], + "hand": [ + 0.4, + 0.4 + ], + "to": [ + -0.614, + -1.019 + ], + "size": [ + 5.9, + 4.2 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 44, + "reshapes": 41, + "notes": [] + }, + { + "id": "14:171", + "page": 14, + "line": 5, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "looks-right" + ], + "fault": false, + "box": [ + 14.6, + 175, + 6.3, + 3.4 + ], + "drawn": [ + 13.495, + 172.993, + 6.3, + 3.4 + ], + "settled": null, + "hand": null, + "to": null, + "size": null, + "was": null, + "goes": 0, + "reshapes": 0, + "notes": [] + }, + { + "id": "16:266", + "page": 16, + "line": 8, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 127.9, + 287.1, + 8.4, + 6.3 + ], + "drawn": [ + 127.129, + 285.454, + 8.4, + 6.3 + ], + "settled": [ + 127.036, + 285.261, + 7.6, + 8.1 + ], + "hand": [ + -0.093, + -0.193 + ], + "to": [ + -0.864, + -1.839 + ], + "size": [ + 7.6, + 8.1 + ], + "was": [ + 8.4, + 6.3 + ], + "goes": 7, + "reshapes": 14, + "notes": [] + }, + { + "id": "18:25", + "page": 18, + "line": 1, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 110.1, + 33.9, + 7.7, + 5.8 + ], + "drawn": [ + 108.326, + 32.53, + 7.7, + 5.8 + ], + "settled": [ + 109.114, + 32.33, + 8.8, + 5.8 + ], + "hand": [ + 0.788, + -0.2 + ], + "to": [ + -0.986, + -1.57 + ], + "size": [ + 8.8, + 5.8 + ], + "was": [ + 7.7, + 5.8 + ], + "goes": 7, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "19:377", + "page": 19, + "line": 10, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 313, + 341.3, + 5.6, + 3.1 + ], + "drawn": [ + 312.438, + 340.175, + 5.6, + 3.1 + ], + "settled": [ + 312.638, + 339.325, + 5.75, + 5.65 + ], + "hand": [ + 0.201, + -0.85 + ], + "to": [ + -0.362, + -1.975 + ], + "size": [ + 5.75, + 5.65 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 11, + "reshapes": 10, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "20:130", + "page": 20, + "line": 4, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 324, + 123.6, + 4.3, + 3.3 + ], + "drawn": [ + 321.235, + 121.729, + 4.3, + 3.3 + ], + "settled": [ + 320.943, + 121.862, + 4.3, + 3.3 + ], + "hand": [ + -0.292, + 0.133 + ], + "to": [ + -3.057, + -1.738 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "20:132", + "page": 20, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 313.5, + 124.2, + 6.3, + 3.4 + ], + "drawn": [ + 310.754, + 122.357, + 6.3, + 3.4 + ], + "settled": [ + 310.505, + 122.375, + 6.13, + 3.33 + ], + "hand": [ + -0.249, + 0.018 + ], + "to": [ + -2.995, + -1.825 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "32:173", + "page": 32, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 311.8, + 191, + 5.6, + 3.1 + ], + "drawn": [ + 311.416, + 190.385, + 5.6, + 3.1 + ], + "settled": [ + 308.695, + 186.666, + 5.7, + 3.2 + ], + "hand": [ + -2.721, + -3.719 + ], + "to": [ + -3.105, + -4.334 + ], + "size": [ + 5.7, + 3.2 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 10, + "reshapes": 2, + "notes": [] + }, + { + "id": "32:175", + "page": 32, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 294.4, + 196.4, + 5.6, + 3.1 + ], + "drawn": [ + 293.886, + 195.67, + 5.6, + 3.1 + ], + "settled": [ + 291.197, + 192.249, + 5.6, + 3.1 + ], + "hand": [ + -2.689, + -3.421 + ], + "to": [ + -3.203, + -4.151 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [] + }, + { + "id": "32:176", + "page": 32, + "line": 6, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 288.8, + 187.5, + 4.3, + 3.3 + ], + "drawn": [ + 288.24, + 186.728, + 4.3, + 3.3 + ], + "settled": [ + 285.544, + 183.446, + 4.2, + 3.3 + ], + "hand": [ + -2.696, + -3.282 + ], + "to": [ + -3.256, + -4.054 + ], + "size": [ + 4.2, + 3.3 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 12, + "reshapes": 5, + "notes": [] + }, + { + "id": "32:180", + "page": 32, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 256.1, + 195.4, + 7.8, + 3.7 + ], + "drawn": [ + 255.309, + 194.423, + 7.8, + 3.7 + ], + "settled": [ + 252.83, + 191.665, + 7.9, + 3.8 + ], + "hand": [ + -2.479, + -2.758 + ], + "to": [ + -3.27, + -3.735 + ], + "size": [ + 7.9, + 3.8 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 4, + "reshapes": 2, + "notes": [] + }, + { + "id": "32:184", + "page": 32, + "line": 6, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 205.6, + 189.1, + 4.4, + 3 + ], + "drawn": [ + 204.42, + 187.777, + 4.4, + 3 + ], + "settled": [ + 202.488, + 185.545, + 4.4, + 3 + ], + "hand": [ + -1.932, + -2.232 + ], + "to": [ + -3.112, + -3.555 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [] + }, + { + "id": "32:185", + "page": 32, + "line": 6, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 196.1, + 191.2, + 4, + 3.9 + ], + "drawn": [ + 194.848, + 189.813, + 4, + 3.9 + ], + "settled": [ + 192.927, + 187.866, + 4, + 3.9 + ], + "hand": [ + -1.921, + -1.947 + ], + "to": [ + -3.173, + -3.334 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "32:187", + "page": 32, + "line": 6, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 191.4, + 197, + 4.4, + 3 + ], + "drawn": [ + 190.114, + 195.583, + 4.4, + 3 + ], + "settled": [ + 188.169, + 193.531, + 4.4, + 3 + ], + "hand": [ + -1.945, + -2.052 + ], + "to": [ + -3.231, + -3.469 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "32:188", + "page": 32, + "line": 6, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 183.5, + 208.7, + 7, + 3.6 + ], + "drawn": [ + 182.165, + 207.239, + 7, + 3.6 + ], + "settled": [ + 180.274, + 205.158, + 7, + 3.6 + ], + "hand": [ + -1.891, + -2.081 + ], + "to": [ + -3.226, + -3.542 + ], + "size": null, + "was": null, + "goes": 4, + "reshapes": 0, + "notes": [] + }, + { + "id": "32:189", + "page": 32, + "line": 6, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 165.2, + 213.4, + 6.3, + 3.4 + ], + "drawn": [ + 163.726, + 211.815, + 6.3, + 3.4 + ], + "settled": [ + 161.893, + 209.925, + 6.4, + 3.15 + ], + "hand": [ + -1.833, + -1.89 + ], + "to": [ + -3.307, + -3.475 + ], + "size": [ + 6.4, + 3.15 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 2, + "reshapes": 3, + "notes": [] + }, + { + "id": "36:36", + "page": 36, + "line": 1, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 102.6, + 35.5, + 6.3, + 3.4 + ], + "drawn": [ + 100.7, + 34.115, + 6.3, + 3.4 + ], + "settled": [ + 99.515, + 33.585, + 6.27, + 3.33 + ], + "hand": [ + -1.185, + -0.53 + ], + "to": [ + -3.085, + -1.915 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "36:45", + "page": 36, + "line": 2, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 327.3, + 47.8, + 5.3, + 6.6 + ], + "drawn": [ + 323.982, + 45.922, + 5.3, + 6.6 + ], + "settled": [ + 324.338, + 46.064, + 5.4, + 6.9 + ], + "hand": [ + 0.356, + 0.142 + ], + "to": [ + -2.962, + -1.736 + ], + "size": [ + 5.4, + 6.9 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 3, + "reshapes": 5, + "notes": [] + }, + { + "id": "36:48", + "page": 36, + "line": 2, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 300.3, + 44.1, + 5.3, + 6.6 + ], + "drawn": [ + 297.183, + 42.311, + 5.3, + 6.6 + ], + "settled": [ + 297.183, + 42.411, + 5.3, + 6.75 + ], + "hand": [ + 0, + 0.1 + ], + "to": [ + -3.117, + -1.689 + ], + "size": [ + 5.3, + 6.75 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "36:521", + "page": 36, + "line": 13, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 286, + 448.6, + 5.3, + 6.6 + ], + "drawn": [ + 283.985, + 446.649, + 5.3, + 6.6 + ], + "settled": [ + 283, + 446.125, + 5.34, + 6.67 + ], + "hand": [ + -0.985, + -0.524 + ], + "to": [ + -3, + -2.475 + ], + "size": [ + 5.34, + 6.67 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "40:91", + "page": 40, + "line": 3, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 161.4, + 92.7, + 4.4, + 3 + ], + "drawn": [ + 159.898, + 90.976, + 4.4, + 3 + ], + "settled": [ + 157.234, + 89.857, + 4.4, + 3 + ], + "hand": [ + -2.664, + -1.119 + ], + "to": [ + -4.166, + -2.843 + ], + "size": null, + "was": null, + "goes": 2, + "reshapes": 0, + "notes": [] + }, + { + "id": "40:101", + "page": 40, + "line": 3, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 61.1, + 81, + 4.3, + 3.3 + ], + "drawn": [ + 58.726, + 78.643, + 4.3, + 3.3 + ], + "settled": [ + 57.555, + 78.695, + 4.19, + 3.07 + ], + "hand": [ + -1.171, + 0.052 + ], + "to": [ + -3.545, + -2.305 + ], + "size": [ + 4.19, + 3.07 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "50:30", + "page": 50, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 107.8, + 89.2, + 5.6, + 3.1 + ], + "drawn": [ + 106.334, + 87.064, + 5.6, + 3.1 + ], + "settled": [ + 107.222, + 87.654, + 5.6, + 5 + ], + "hand": [ + 0.888, + 0.59 + ], + "to": [ + -0.578, + -1.546 + ], + "size": [ + 5.6, + 5 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 9, + "reshapes": 14, + "notes": [] + }, + { + "id": "52:12", + "page": 52, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 207.5, + 15.5, + 7, + 3.6 + ], + "drawn": [ + 207.187, + 14.896, + 7, + 3.6 + ], + "settled": [ + 202.189, + 11.173, + 7, + 3.6 + ], + "hand": [ + -4.998, + -3.723 + ], + "to": [ + -5.311, + -4.327 + ], + "size": null, + "was": null, + "goes": 6, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:14", + "page": 52, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 198, + 34, + 4, + 3.9 + ], + "drawn": [ + 197.721, + 33.43, + 4, + 3.9 + ], + "settled": [ + 192.905, + 29.693, + 4, + 3.9 + ], + "hand": [ + -4.816, + -3.737 + ], + "to": [ + -5.095, + -4.307 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:19", + "page": 52, + "line": 1, + "name": "maddah", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 170.1, + 13.4, + 6.6, + 3.1 + ], + "drawn": [ + 169.903, + 12.912, + 6.6, + 3.1 + ], + "settled": [ + 165.123, + 9.498, + 6.75, + 3.15 + ], + "hand": [ + -4.78, + -3.414 + ], + "to": [ + -4.977, + -3.902 + ], + "size": [ + 6.75, + 3.15 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 2, + "reshapes": 3, + "notes": [] + }, + { + "id": "52:21", + "page": 52, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 147.8, + 18.1, + 7.8, + 3.7 + ], + "drawn": [ + 147.67, + 17.679, + 7.8, + 3.7 + ], + "settled": [ + 142.656, + 14.38, + 7.8, + 3.7 + ], + "hand": [ + -5.014, + -3.299 + ], + "to": [ + -5.144, + -3.72 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:25", + "page": 52, + "line": 1, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 113, + 10.6, + 5.3, + 4.3 + ], + "drawn": [ + 112.98, + 10.29, + 5.3, + 4.3 + ], + "settled": [ + 108.297, + 6.785, + 5.3, + 4.3 + ], + "hand": [ + -4.683, + -3.505 + ], + "to": [ + -4.703, + -3.815 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:26", + "page": 52, + "line": 1, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 105, + 14.1, + 4.4, + 3 + ], + "drawn": [ + 105.006, + 13.816, + 4.4, + 3 + ], + "settled": [ + 100.395, + 10.32, + 4.27, + 2.8 + ], + "hand": [ + -4.611, + -3.496 + ], + "to": [ + -4.605, + -3.78 + ], + "size": [ + 4.27, + 2.8 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "52:28", + "page": 52, + "line": 1, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 83, + 21.1, + 4.4, + 3 + ], + "drawn": [ + 83.074, + 20.884, + 4.4, + 3 + ], + "settled": [ + 78.217, + 17.356, + 4.4, + 3 + ], + "hand": [ + -4.857, + -3.528 + ], + "to": [ + -4.783, + -3.744 + ], + "size": null, + "was": null, + "goes": 6, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:32", + "page": 52, + "line": 1, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 35.8, + 11.8, + 5.3, + 6.6 + ], + "drawn": [ + 36.018, + 11.728, + 5.3, + 6.6 + ], + "settled": [ + 31.472, + 8.113, + 5.3, + 6.6 + ], + "hand": [ + -4.546, + -3.615 + ], + "to": [ + -4.328, + -3.687 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:36", + "page": 52, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 319.8, + 68.4, + 7, + 3.6 + ], + "drawn": [ + 319.05, + 67.275, + 7, + 3.6 + ], + "settled": [ + 315.654, + 64.771, + 7, + 3.6 + ], + "hand": [ + -3.396, + -2.504 + ], + "to": [ + -4.146, + -3.629 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:41", + "page": 52, + "line": 2, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 251.8, + 48.5, + 5.3, + 4.3 + ], + "drawn": [ + 251.05, + 47.375, + 5.3, + 4.3 + ], + "settled": [ + 248.015, + 45.45, + 5.21, + 4.28 + ], + "hand": [ + -3.035, + -1.925 + ], + "to": [ + -3.785, + -3.05 + ], + "size": [ + 5.21, + 4.28 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "52:48", + "page": 52, + "line": 2, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 163.8, + 58.3, + 2.1, + 6 + ], + "drawn": [ + 163.05, + 57.175, + 2.1, + 6 + ], + "settled": [ + 160.154, + 55.418, + 2.1, + 6 + ], + "hand": [ + -2.896, + -1.757 + ], + "to": [ + -3.646, + -2.882 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "52:49", + "page": 52, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 156.9, + 72.8, + 6.3, + 3.4 + ], + "drawn": [ + 156.15, + 71.675, + 6.3, + 3.4 + ], + "settled": [ + 153.019, + 69.954, + 6.3, + 3.4 + ], + "hand": [ + -3.131, + -1.721 + ], + "to": [ + -3.881, + -2.846 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "56:114", + "page": 56, + "line": 4, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 280.3, + 117.7, + 5.3, + 4.3 + ], + "drawn": [ + 279.982, + 116.534, + 5.3, + 4.3 + ], + "settled": [ + 277.654, + 113.932, + 5.3, + 4.3 + ], + "hand": [ + -2.328, + -2.602 + ], + "to": [ + -2.646, + -3.768 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "56:115", + "page": 56, + "line": 4, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 275.1, + 118.3, + 4.4, + 3 + ], + "drawn": [ + 274.747, + 117.151, + 4.4, + 3 + ], + "settled": [ + 272.525, + 114.515, + 4.27, + 2.93 + ], + "hand": [ + -2.222, + -2.636 + ], + "to": [ + -2.575, + -3.785 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "56:124", + "page": 56, + "line": 4, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 174.3, + 124, + 4.4, + 3 + ], + "drawn": [ + 173.322, + 123.153, + 4.4, + 3 + ], + "settled": [ + 171.66, + 120.245, + 4.4, + 2.93 + ], + "hand": [ + -1.662, + -2.908 + ], + "to": [ + -2.64, + -3.755 + ], + "size": [ + 4.4, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "56:127", + "page": 56, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 142, + 126.1, + 6.3, + 3.4 + ], + "drawn": [ + 140.827, + 125.347, + 6.3, + 3.4 + ], + "settled": [ + 139.419, + 122.031, + 6.3, + 3.4 + ], + "hand": [ + -1.408, + -3.316 + ], + "to": [ + -2.581, + -4.069 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:14", + "page": 60, + "line": 1, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 226.3, + 14.9, + 4.4, + 3 + ], + "drawn": [ + 225.747, + 13.992, + 4.4, + 3 + ], + "settled": [ + 220.557, + 12.396, + 4.4, + 3 + ], + "hand": [ + -5.19, + -1.596 + ], + "to": [ + -5.743, + -2.504 + ], + "size": null, + "was": null, + "goes": 7, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:15", + "page": 60, + "line": 1, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 221.6, + 19.5, + 5.3, + 6.6 + ], + "drawn": [ + 221.057, + 18.575, + 5.3, + 6.6 + ], + "settled": [ + 215.836, + 16.787, + 5.3, + 6.6 + ], + "hand": [ + -5.221, + -1.788 + ], + "to": [ + -5.764, + -2.713 + ], + "size": null, + "was": null, + "goes": 8, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:24", + "page": 60, + "line": 1, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 156.1, + 13.7, + 5.3, + 6.6 + ], + "drawn": [ + 155.706, + 12.519, + 5.3, + 6.6 + ], + "settled": [ + 150.667, + 11.095, + 5.3, + 6.6 + ], + "hand": [ + -5.039, + -1.424 + ], + "to": [ + -5.433, + -2.605 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:36", + "page": 60, + "line": 1, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 22, + 18, + 5.3, + 6.6 + ], + "drawn": [ + 21.912, + 16.295, + 5.3, + 6.6 + ], + "settled": [ + 16.95, + 15.721, + 5.3, + 6.6 + ], + "hand": [ + -4.962, + -0.574 + ], + "to": [ + -5.05, + -2.279 + ], + "size": null, + "was": null, + "goes": 7, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:37", + "page": 60, + "line": 1, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 8.3, + 17.9, + 5.3, + 6.6 + ], + "drawn": [ + 8.243, + 16.142, + 5.3, + 6.6 + ], + "settled": [ + 3.149, + 15.594, + 5.3, + 6.6 + ], + "hand": [ + -5.094, + -0.548 + ], + "to": [ + -5.151, + -2.306 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:40", + "page": 60, + "line": 2, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 308.8, + 48.4, + 5.3, + 4.3 + ], + "drawn": [ + 307.8, + 47.525, + 5.3, + 4.3 + ], + "settled": [ + 303.254, + 46.375, + 5.3, + 4.3 + ], + "hand": [ + -4.546, + -1.15 + ], + "to": [ + -5.546, + -2.025 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:42", + "page": 60, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 283.2, + 70.4, + 7, + 3.6 + ], + "drawn": [ + 282.2, + 69.525, + 7, + 3.6 + ], + "settled": [ + 277.48, + 68.23, + 7, + 3.6 + ], + "hand": [ + -4.72, + -1.295 + ], + "to": [ + -5.72, + -2.17 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:57", + "page": 60, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 124, + 74.7, + 7, + 3.6 + ], + "drawn": [ + 123, + 73.825, + 7, + 3.6 + ], + "settled": [ + 118.826, + 72.846, + 7, + 3.6 + ], + "hand": [ + -4.174, + -0.979 + ], + "to": [ + -5.174, + -1.854 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:60", + "page": 60, + "line": 2, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 90.9, + 49.8, + 5.3, + 6.6 + ], + "drawn": [ + 89.9, + 48.925, + 5.3, + 6.6 + ], + "settled": [ + 86.258, + 47.83, + 5.3, + 6.6 + ], + "hand": [ + -3.642, + -1.095 + ], + "to": [ + -4.642, + -1.97 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:63", + "page": 60, + "line": 2, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 35, + 51.4, + 5.3, + 6.6 + ], + "drawn": [ + 34, + 50.525, + 5.3, + 6.6 + ], + "settled": [ + 30.525, + 49.285, + 5.23, + 6.75 + ], + "hand": [ + -3.475, + -1.24 + ], + "to": [ + -4.475, + -2.115 + ], + "size": [ + 5.23, + 6.75 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "60:68", + "page": 60, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 308.6, + 105.9, + 7, + 3.6 + ], + "drawn": [ + 306.693, + 104.424, + 7, + 3.6 + ], + "settled": [ + 304.189, + 103.848, + 7, + 3.6 + ], + "hand": [ + -2.504, + -0.576 + ], + "to": [ + -4.411, + -2.052 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:79", + "page": 60, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 217.1, + 106, + 7, + 3.6 + ], + "drawn": [ + 215.039, + 104.575, + 7, + 3.6 + ], + "settled": [ + 213.355, + 104.28, + 6.93, + 3.6 + ], + "hand": [ + -1.684, + -0.295 + ], + "to": [ + -3.745, + -1.72 + ], + "size": [ + 6.93, + 3.6 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "60:82", + "page": 60, + "line": 3, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 183, + 90.8, + 4.3, + 3.3 + ], + "drawn": [ + 180.879, + 89.395, + 4.3, + 3.3 + ], + "settled": [ + 179.32, + 88.98, + 4.27, + 3.14 + ], + "hand": [ + -1.559, + -0.415 + ], + "to": [ + -3.68, + -1.82 + ], + "size": [ + 4.27, + 3.14 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 13, + "reshapes": 8, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "60:84", + "page": 60, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 175.4, + 107, + 6.3, + 3.4 + ], + "drawn": [ + 173.268, + 105.599, + 6.3, + 3.4 + ], + "settled": [ + 171.712, + 105.221, + 6.3, + 3.4 + ], + "hand": [ + -1.556, + -0.378 + ], + "to": [ + -3.688, + -1.779 + ], + "size": null, + "was": null, + "goes": 7, + "reshapes": 0, + "notes": [] + }, + { + "id": "60:110", + "page": 60, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 190.7, + 124.3, + 8.5, + 3.9 + ], + "drawn": [ + 188.025, + 122.655, + 8.5, + 3.9 + ], + "settled": [ + 187.665, + 122.475, + 8.5, + 3.9 + ], + "hand": [ + -0.36, + -0.18 + ], + "to": [ + -3.035, + -1.825 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "64:376", + "page": 64, + "line": 11, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 174.9, + 377.5, + 4.3, + 3.3 + ], + "drawn": [ + 172.228, + 375.349, + 4.3, + 3.3 + ], + "settled": [ + 171.374, + 375.083, + 4.3, + 3.3 + ], + "hand": [ + -0.854, + -0.266 + ], + "to": [ + -3.526, + -2.417 + ], + "size": null, + "was": null, + "goes": 6, + "reshapes": 0, + "notes": [] + }, + { + "id": "64:378", + "page": 64, + "line": 11, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 160.7, + 369.2, + 8.2, + 6.4 + ], + "drawn": [ + 158.04, + 367.089, + 8.2, + 6.4 + ], + "settled": [ + 157.09, + 366.91, + 8.14, + 6.36 + ], + "hand": [ + -0.95, + -0.179 + ], + "to": [ + -3.61, + -2.29 + ], + "size": [ + 8.14, + 6.36 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "68:251", + "page": 68, + "line": 8, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 308.7, + 286.4, + 6.1, + 5.2 + ], + "drawn": [ + 307.421, + 285.197, + 6.1, + 5.2 + ], + "settled": [ + 307.281, + 285.029, + 6.1, + 5.2 + ], + "hand": [ + -0.14, + -0.168 + ], + "to": [ + -1.419, + -1.371 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "70:272", + "page": 70, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 295.8, + 266.6, + 7, + 3.6 + ], + "drawn": [ + 292.804, + 264.314, + 7, + 3.6 + ], + "settled": [ + 292.604, + 264.214, + 7, + 3.6 + ], + "hand": [ + -0.2, + -0.1 + ], + "to": [ + -3.196, + -2.386 + ], + "size": null, + "was": null, + "goes": 3, + "reshapes": 0, + "notes": [] + }, + { + "id": "70:300", + "page": 70, + "line": 8, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 15.3, + 269.8, + 4.4, + 3 + ], + "drawn": [ + 12.527, + 268.158, + 4.4, + 3 + ], + "settled": [ + 12.227, + 267.858, + 4.4, + 3 + ], + "hand": [ + -0.3, + -0.3 + ], + "to": [ + -3.073, + -1.942 + ], + "size": null, + "was": null, + "goes": 16, + "reshapes": 0, + "notes": [] + }, + { + "id": "78:419", + "page": 78, + "line": 12, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 177.1, + 406.4, + 5.6, + 3.1 + ], + "drawn": [ + 175.34, + 405.236, + 5.6, + 3.1 + ], + "settled": [ + 173.099, + 404.891, + 5.6, + 3.1 + ], + "hand": [ + -2.241, + -0.345 + ], + "to": [ + -4.001, + -1.509 + ], + "size": null, + "was": null, + "goes": 4, + "reshapes": 0, + "notes": [] + }, + { + "id": "78:420", + "page": 78, + "line": 12, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 169.5, + 410.8, + 8.2, + 6.4 + ], + "drawn": [ + 167.718, + 409.64, + 8.2, + 6.4 + ], + "settled": [ + 165.753, + 409.223, + 8.2, + 6.4 + ], + "hand": [ + -1.965, + -0.417 + ], + "to": [ + -3.747, + -1.577 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "82:331", + "page": 82, + "line": 10, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 100, + 357.4, + 8.6, + 6.7 + ], + "drawn": [ + 98.739, + 356.728, + 8.6, + 6.7 + ], + "settled": [ + 98.839, + 356.328, + 7.4, + 6.8 + ], + "hand": [ + 0.1, + -0.4 + ], + "to": [ + -1.161, + -1.072 + ], + "size": [ + 7.4, + 6.8 + ], + "was": [ + 8.6, + 6.7 + ], + "goes": 16, + "reshapes": 27, + "notes": [] + }, + { + "id": "88:0", + "page": 88, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 329.3, + 13.6, + 4, + 3.9 + ], + "drawn": [ + 329.553, + 13.965, + 4, + 3.9 + ], + "settled": [ + 324.527, + 9.018, + 4, + 3.9 + ], + "hand": [ + -5.026, + -4.947 + ], + "to": [ + -4.773, + -4.582 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:2", + "page": 88, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 320.9, + 10.9, + 6.3, + 3.4 + ], + "drawn": [ + 321.082, + 11.203, + 6.3, + 3.4 + ], + "settled": [ + 316.005, + 6.436, + 6.3, + 3.4 + ], + "hand": [ + -5.077, + -4.767 + ], + "to": [ + -4.895, + -4.464 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:4", + "page": 88, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 307.5, + 17.4, + 5.6, + 3.1 + ], + "drawn": [ + 307.547, + 17.584, + 5.6, + 3.1 + ], + "settled": [ + 295.329, + 18.101, + 5.6, + 3.1 + ], + "hand": [ + -12.218, + 0.517 + ], + "to": [ + -12.171, + 0.701 + ], + "size": null, + "was": null, + "goes": 4, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:5", + "page": 88, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 300.1, + 22.4, + 5.6, + 3.1 + ], + "drawn": [ + 300.074, + 22.52, + 5.6, + 3.1 + ], + "settled": [ + 295.321, + 18.127, + 5.6, + 3.1 + ], + "hand": [ + -4.753, + -4.393 + ], + "to": [ + -4.779, + -4.273 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:9", + "page": 88, + "line": 1, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 271.3, + 11.7, + 5.3, + 4.3 + ], + "drawn": [ + 270.99, + 11.57, + 5.3, + 4.3 + ], + "settled": [ + 266.991, + 7.667, + 5.3, + 4.3 + ], + "hand": [ + -3.999, + -3.903 + ], + "to": [ + -4.309, + -4.033 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:13", + "page": 88, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 239.5, + 21.4, + 8.5, + 3.9 + ], + "drawn": [ + 238.894, + 21.009, + 8.5, + 3.9 + ], + "settled": [ + 235.202, + 17.527, + 8.5, + 3.9 + ], + "hand": [ + -3.692, + -3.482 + ], + "to": [ + -4.298, + -3.873 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:19", + "page": 88, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 172.7, + 13.3, + 4, + 3.9 + ], + "drawn": [ + 171.417, + 12.313, + 4, + 3.9 + ], + "settled": [ + 168.522, + 9.629, + 4, + 3.9 + ], + "hand": [ + -2.895, + -2.684 + ], + "to": [ + -4.178, + -3.671 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:20", + "page": 88, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 170.8, + 9.8, + 6.3, + 3.4 + ], + "drawn": [ + 169.51, + 8.807, + 6.3, + 3.4 + ], + "settled": [ + 166.777, + 6.218, + 6.3, + 3.4 + ], + "hand": [ + -2.733, + -2.589 + ], + "to": [ + -4.023, + -3.582 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:22", + "page": 88, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 163.4, + 10.5, + 7, + 3.6 + ], + "drawn": [ + 162.041, + 9.446, + 7, + 3.6 + ], + "settled": [ + 159.393, + 6.913, + 7, + 3.6 + ], + "hand": [ + -2.648, + -2.533 + ], + "to": [ + -4.007, + -3.587 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:24", + "page": 88, + "line": 1, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 151.5, + 20.8, + 4.4, + 3 + ], + "drawn": [ + 150.011, + 19.632, + 4.4, + 3 + ], + "settled": [ + 147.26, + 17.12, + 4.4, + 2.8 + ], + "hand": [ + -2.751, + -2.512 + ], + "to": [ + -4.24, + -3.68 + ], + "size": [ + 4.4, + 2.8 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "88:25", + "page": 88, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 140.4, + 19.5, + 6.3, + 3.4 + ], + "drawn": [ + 138.812, + 18.244, + 6.3, + 3.4 + ], + "settled": [ + 137.74, + 16.749, + 6.2, + 5.15 + ], + "hand": [ + -1.072, + -1.495 + ], + "to": [ + -2.66, + -2.751 + ], + "size": [ + 6.2, + 5.15 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 3, + "reshapes": 4, + "notes": [] + }, + { + "id": "88:296", + "page": 88, + "line": 9, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 122.2, + 302.2, + 4.4, + 3 + ], + "drawn": [ + 119.934, + 301.212, + 4.4, + 3 + ], + "settled": [ + 118.442, + 301.212, + 4.4, + 3 + ], + "hand": [ + -1.492, + 0 + ], + "to": [ + -3.758, + -0.988 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:297", + "page": 88, + "line": 9, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 106.6, + 303.5, + 5.3, + 6.6 + ], + "drawn": [ + 104.172, + 302.484, + 5.3, + 6.6 + ], + "settled": [ + 102.712, + 302.592, + 5.3, + 6.6 + ], + "hand": [ + -1.46, + 0.108 + ], + "to": [ + -3.888, + -0.908 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:298", + "page": 88, + "line": 9, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 97.7, + 306.7, + 4.4, + 3 + ], + "drawn": [ + 95.173, + 305.666, + 4.4, + 3 + ], + "settled": [ + 93.894, + 305.719, + 4.4, + 3 + ], + "hand": [ + -1.279, + 0.053 + ], + "to": [ + -3.806, + -0.981 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:303", + "page": 88, + "line": 9, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 47.5, + 303.5, + 5.3, + 6.7 + ], + "drawn": [ + 44.444, + 302.373, + 5.3, + 6.7 + ], + "settled": [ + 43.759, + 302.592, + 5.3, + 6.7 + ], + "hand": [ + -0.685, + 0.219 + ], + "to": [ + -3.741, + -0.908 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "88:311", + "page": 88, + "line": 10, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 293.6, + 360.8, + 6.3, + 3.4 + ], + "drawn": [ + 290.389, + 359.957, + 6.3, + 3.4 + ], + "settled": [ + 289.989, + 359.957, + 6.4, + 3.4 + ], + "hand": [ + -0.4, + 0 + ], + "to": [ + -3.611, + -0.843 + ], + "size": [ + 6.4, + 3.4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 8, + "reshapes": 1, + "notes": [] + }, + { + "id": "88:322", + "page": 88, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 155.4, + 334.7, + 4, + 3.9 + ], + "drawn": [ + 152.616, + 334.027, + 4, + 3.9 + ], + "settled": [ + 152.395, + 334.125, + 3.87, + 3.87 + ], + "hand": [ + -0.221, + 0.098 + ], + "to": [ + -3.005, + -0.575 + ], + "size": [ + 3.87, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "88:326", + "page": 88, + "line": 10, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 119.8, + 335.5, + 4.4, + 3 + ], + "drawn": [ + 117.125, + 334.87, + 4.4, + 3 + ], + "settled": [ + 116.805, + 335.003, + 4.4, + 3 + ], + "hand": [ + -0.32, + 0.133 + ], + "to": [ + -2.995, + -0.497 + ], + "size": null, + "was": null, + "goes": 2, + "reshapes": 0, + "notes": [] + }, + { + "id": "92:91", + "page": 92, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "print-defect" + ], + "fault": true, + "box": [ + 245.7, + 83.4, + 7, + 3.5 + ], + "drawn": [ + 244.788, + 82.398, + 7, + 3.5 + ], + "settled": [ + 245.308, + 82.638, + 7, + 3.5 + ], + "hand": [ + 0.52, + 0.24 + ], + "to": [ + -0.392, + -0.762 + ], + "size": null, + "was": null, + "goes": 5, + "reshapes": 0, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "94:196", + "page": 94, + "line": 5, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 182.6, + 160.3, + 6.9, + 5.3 + ], + "drawn": [ + 182.032, + 159.236, + 6.9, + 5.3 + ], + "settled": [ + 181.91, + 159.086, + 7.6, + 6.05 + ], + "hand": [ + -0.122, + -0.15 + ], + "to": [ + -0.69, + -1.214 + ], + "size": [ + 7.6, + 6.05 + ], + "was": [ + 6.9, + 5.3 + ], + "goes": 9, + "reshapes": 11, + "notes": [] + }, + { + "id": "98:222", + "page": 98, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 77.7, + 229, + 6.3, + 3.4 + ], + "drawn": [ + 76.503, + 227.621, + 6.3, + 3.4 + ], + "settled": [ + 74.585, + 226.352, + 6.3, + 3.4 + ], + "hand": [ + -1.918, + -1.269 + ], + "to": [ + -3.115, + -2.648 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "99:22", + "page": 99, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 133.6, + 12.8, + 4, + 3.9 + ], + "drawn": [ + 132.558, + 11.193, + 4, + 3.9 + ], + "settled": [ + 131.93, + 11.135, + 4.8, + 3.73 + ], + "hand": [ + -0.628, + -0.058 + ], + "to": [ + -1.67, + -1.665 + ], + "size": [ + 4.8, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "100:5", + "page": 100, + "line": 1, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 303.7, + 17.3, + 4.3, + 3.3 + ], + "drawn": [ + 303.076, + 15.477, + 4.3, + 3.3 + ], + "settled": [ + 303.182, + 13.724, + 4.3, + 3.3 + ], + "hand": [ + 0.106, + -1.753 + ], + "to": [ + -0.518, + -3.576 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:20", + "page": 100, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 189.8, + 11.9, + 5.6, + 3.1 + ], + "drawn": [ + 188.719, + 9.897, + 5.6, + 3.1 + ], + "settled": [ + 187.724, + 8.073, + 5.6, + 3.1 + ], + "hand": [ + -0.995, + -1.824 + ], + "to": [ + -2.076, + -3.827 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:35", + "page": 100, + "line": 1, + "name": "maddah", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 55.7, + 11.8, + 6.6, + 3.1 + ], + "drawn": [ + 54.081, + 9.584, + 6.6, + 3.1 + ], + "settled": [ + 54.295, + 8.33, + 6.53, + 3.2 + ], + "hand": [ + 0.214, + -1.254 + ], + "to": [ + -1.405, + -3.47 + ], + "size": [ + 6.53, + 3.2 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "100:46", + "page": 100, + "line": 2, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 317.1, + 61.1, + 2.1, + 6.1 + ], + "drawn": [ + 317.897, + 60.754, + 2.1, + 6.1 + ], + "settled": [ + 315.298, + 56.496, + 2.1, + 6.1 + ], + "hand": [ + -2.599, + -4.258 + ], + "to": [ + -1.802, + -4.604 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:48", + "page": 100, + "line": 2, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 306.8, + 47.9, + 4, + 3.9 + ], + "drawn": [ + 307.519, + 47.518, + 4, + 3.9 + ], + "settled": [ + 305.545, + 43.202, + 4, + 3.9 + ], + "hand": [ + -1.974, + -4.316 + ], + "to": [ + -1.255, + -4.698 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:51", + "page": 100, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 287, + 69.7, + 7.8, + 3.7 + ], + "drawn": [ + 287.569, + 69.249, + 7.8, + 3.7 + ], + "settled": [ + 285.655, + 65.148, + 7.8, + 3.7 + ], + "hand": [ + -1.914, + -4.101 + ], + "to": [ + -1.345, + -4.552 + ], + "size": null, + "was": null, + "goes": 3, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:55", + "page": 100, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 254.1, + 43.6, + 6.3, + 3.4 + ], + "drawn": [ + 254.387, + 43.02, + 6.3, + 3.4 + ], + "settled": [ + 250.235, + 39.045, + 6.13, + 3.33 + ], + "hand": [ + -4.152, + -3.975 + ], + "to": [ + -3.865, + -4.555 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 3, + "reshapes": 3, + "notes": [] + }, + { + "id": "100:56", + "page": 100, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 252, + 72, + 7, + 3.6 + ], + "drawn": [ + 252.272, + 71.413, + 7, + 3.6 + ], + "settled": [ + 250.656, + 67.275, + 7, + 3.6 + ], + "hand": [ + -1.616, + -4.138 + ], + "to": [ + -1.344, + -4.725 + ], + "size": null, + "was": null, + "goes": 6, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:60", + "page": 100, + "line": 2, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 234.4, + 57.1, + 2.1, + 6 + ], + "drawn": [ + 234.504, + 56.437, + 2.1, + 6 + ], + "settled": [ + 230.921, + 52.756, + 2.1, + 6 + ], + "hand": [ + -3.583, + -3.681 + ], + "to": [ + -3.479, + -4.344 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:62", + "page": 100, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 220.5, + 50.4, + 7, + 3.6 + ], + "drawn": [ + 220.508, + 49.693, + 7, + 3.6 + ], + "settled": [ + 217.095, + 46.215, + 6.93, + 3.47 + ], + "hand": [ + -3.413, + -3.478 + ], + "to": [ + -3.405, + -4.185 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "100:65", + "page": 100, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 197.9, + 53.4, + 7, + 3.5 + ], + "drawn": [ + 197.719, + 52.606, + 7, + 3.5 + ], + "settled": [ + 194.409, + 49.267, + 7, + 3.5 + ], + "hand": [ + -3.31, + -3.339 + ], + "to": [ + -3.491, + -4.133 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:74", + "page": 100, + "line": 2, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 139.5, + 68, + 4, + 3.9 + ], + "drawn": [ + 138.817, + 66.976, + 4, + 3.9 + ], + "settled": [ + 136.33, + 63.995, + 4, + 3.87 + ], + "hand": [ + -2.487, + -2.981 + ], + "to": [ + -3.17, + -4.005 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "100:85", + "page": 100, + "line": 2, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 38.1, + 54.7, + 4.4, + 3 + ], + "drawn": [ + 36.569, + 53.288, + 4.4, + 3 + ], + "settled": [ + 35.343, + 50.783, + 4.4, + 3 + ], + "hand": [ + -1.226, + -2.505 + ], + "to": [ + -2.757, + -3.917 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "100:393", + "page": 100, + "line": 10, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 111, + 339.2, + 6.9, + 5.5 + ], + "drawn": [ + 110.358, + 337.825, + 6.9, + 5.5 + ], + "settled": [ + 110.21, + 337.725, + 7.74, + 6.53 + ], + "hand": [ + -0.148, + -0.1 + ], + "to": [ + -0.79, + -1.475 + ], + "size": [ + 7.74, + 6.53 + ], + "was": [ + 6.9, + 5.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "102:48", + "page": 102, + "line": 2, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 232.5, + 52.2, + 6.7, + 5.5 + ], + "drawn": [ + 231.492, + 51.377, + 6.7, + 5.5 + ], + "settled": [ + 227.269, + 49.037, + 6.7, + 5.5 + ], + "hand": [ + -4.223, + -2.34 + ], + "to": [ + -5.231, + -3.163 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "102:52", + "page": 102, + "line": 2, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 176.3, + 53.5, + 4.4, + 3 + ], + "drawn": [ + 175.294, + 52.96, + 4.4, + 3 + ], + "settled": [ + 171.191, + 50.136, + 4.4, + 3 + ], + "hand": [ + -4.103, + -2.824 + ], + "to": [ + -5.109, + -3.364 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "102:55", + "page": 102, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 143.6, + 53.6, + 6.3, + 3.4 + ], + "drawn": [ + 142.595, + 53.217, + 6.3, + 3.4 + ], + "settled": [ + 138.505, + 50.375, + 6.13, + 3.33 + ], + "hand": [ + -4.09, + -2.842 + ], + "to": [ + -5.095, + -3.225 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "102:56", + "page": 102, + "line": 2, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 137.8, + 54.9, + 4.4, + 3 + ], + "drawn": [ + 136.796, + 54.55, + 4.4, + 3 + ], + "settled": [ + 132.773, + 51.539, + 4.4, + 3 + ], + "hand": [ + -4.023, + -3.011 + ], + "to": [ + -5.027, + -3.361 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "102:67", + "page": 102, + "line": 2, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 66.6, + 56.5, + 4.4, + 3 + ], + "drawn": [ + 65.598, + 56.501, + 4.4, + 3 + ], + "settled": [ + 61.814, + 53.304, + 4.4, + 3 + ], + "hand": [ + -3.784, + -3.197 + ], + "to": [ + -4.786, + -3.196 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "107:260", + "page": 107, + "line": 7, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 229.3, + 248.4, + 7.2, + 5.7 + ], + "drawn": [ + 228.21, + 247.18, + 7.2, + 5.7 + ], + "settled": [ + 228.325, + 247.225, + 7.73, + 5.47 + ], + "hand": [ + 0.115, + 0.045 + ], + "to": [ + -0.975, + -1.175 + ], + "size": [ + 7.73, + 5.47 + ], + "was": [ + 7.2, + 5.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "110:70", + "page": 110, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 327.2, + 91.2, + 6.3, + 3.4 + ], + "drawn": [ + 324.36, + 89.344, + 6.3, + 3.4 + ], + "settled": [ + 324.175, + 89.715, + 6.27, + 3.33 + ], + "hand": [ + -0.185, + 0.371 + ], + "to": [ + -3.025, + -1.485 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "110:275", + "page": 110, + "line": 10, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 246.1, + 334.4, + 7.8, + 3.7 + ], + "drawn": [ + 244.339, + 332.774, + 7.8, + 3.7 + ], + "settled": [ + 242.69, + 331.979, + 7.8, + 3.7 + ], + "hand": [ + -1.649, + -0.795 + ], + "to": [ + -3.41, + -2.421 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "113:29", + "page": 113, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 80.4, + 4, + 6.2, + 3.4 + ], + "drawn": [ + 77.717, + 4.54, + 6.2, + 3.4 + ], + "settled": [ + 76.895, + 4.785, + 6.27, + 3.33 + ], + "hand": [ + -0.822, + 0.245 + ], + "to": [ + -3.505, + 0.785 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.2, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "113:36", + "page": 113, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 22.2, + 11.2, + 7.7, + 3.7 + ], + "drawn": [ + 19.154, + 11.759, + 7.7, + 3.7 + ], + "settled": [ + 18.075, + 12.1, + 7.73, + 3.6 + ], + "hand": [ + -1.079, + 0.341 + ], + "to": [ + -4.125, + 0.9 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.7, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "113:200", + "page": 113, + "line": 6, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 17.7, + 212.6, + 6.2, + 3.4 + ], + "drawn": [ + 14.76, + 215.022, + 6.2, + 3.4 + ], + "settled": [ + 14.422, + 214.769, + 6.2, + 3.4 + ], + "hand": [ + -0.338, + -0.253 + ], + "to": [ + -3.278, + 2.169 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "113:229", + "page": 113, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 97.6, + 224.3, + 3.9, + 3.9 + ], + "drawn": [ + 94.58, + 225.782, + 3.9, + 3.9 + ], + "settled": [ + 94.65, + 225.795, + 4, + 3.73 + ], + "hand": [ + 0.07, + 0.013 + ], + "to": [ + -2.95, + 1.495 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 3.9, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "116:262", + "page": 116, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 43.7, + 222.4, + 5.6, + 3.1 + ], + "drawn": [ + 42.913, + 221.874, + 5.6, + 3.1 + ], + "settled": [ + 38.691, + 218.696, + 5.8, + 3.3 + ], + "hand": [ + -4.222, + -3.178 + ], + "to": [ + -5.009, + -3.704 + ], + "size": [ + 5.8, + 3.3 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 6, + "reshapes": 5, + "notes": [] + }, + { + "id": "116:263", + "page": 116, + "line": 7, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 39.9, + 232, + 4.4, + 3 + ], + "drawn": [ + 39.111, + 231.504, + 4.4, + 3 + ], + "settled": [ + 34.768, + 228.44, + 4.4, + 3 + ], + "hand": [ + -4.343, + -3.064 + ], + "to": [ + -5.132, + -3.56 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "116:288", + "page": 116, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 153, + 266.8, + 8.4, + 3.9 + ], + "drawn": [ + 152.188, + 265.613, + 8.4, + 3.9 + ], + "settled": [ + 148.665, + 263.795, + 8.27, + 3.73 + ], + "hand": [ + -3.522, + -1.817 + ], + "to": [ + -4.335, + -3.005 + ], + "size": [ + 8.27, + 3.73 + ], + "was": [ + 8.4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "120:342", + "page": 120, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 89.2, + 302.2, + 7, + 3.6 + ], + "drawn": [ + 87.054, + 301.22, + 7, + 3.6 + ], + "settled": [ + 86.248, + 301.162, + 7, + 3.6 + ], + "hand": [ + -0.806, + -0.058 + ], + "to": [ + -2.952, + -1.038 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "124:145", + "page": 124, + "line": 4, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 93.5, + 142.6, + 4, + 3.9 + ], + "drawn": [ + 92.055, + 140.199, + 4, + 3.9 + ], + "settled": [ + 91.77, + 140.195, + 4.84, + 3.73 + ], + "hand": [ + -0.285, + -0.004 + ], + "to": [ + -1.73, + -2.405 + ], + "size": [ + 4.84, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "130:90", + "page": 130, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 64.8, + 54.3, + 7, + 3.6 + ], + "drawn": [ + 62.765, + 52.455, + 7, + 3.6 + ], + "settled": [ + 64.492, + 52.916, + 6.8, + 4.5 + ], + "hand": [ + 1.727, + 0.461 + ], + "to": [ + -0.308, + -1.384 + ], + "size": [ + 6.8, + 4.5 + ], + "was": [ + 7, + 3.6 + ], + "goes": 4, + "reshapes": 5, + "notes": [] + }, + { + "id": "130:148", + "page": 130, + "line": 4, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 306.8, + 118.2, + 5.3, + 6.6 + ], + "drawn": [ + 303.852, + 116.714, + 5.3, + 6.6 + ], + "settled": [ + 303.855, + 116.69, + 5.23, + 6.62 + ], + "hand": [ + 0.003, + -0.024 + ], + "to": [ + -2.945, + -1.51 + ], + "size": [ + 5.23, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "130:153", + "page": 130, + "line": 4, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 260.3, + 145.5, + 6.3, + 3.4 + ], + "drawn": [ + 257.427, + 144.109, + 6.3, + 3.4 + ], + "settled": [ + 257.305, + 144.115, + 6.13, + 3.33 + ], + "hand": [ + -0.122, + 0.006 + ], + "to": [ + -2.995, + -1.385 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "132:86", + "page": 132, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 327.8, + 86.7, + 5.6, + 3.1 + ], + "drawn": [ + 325.988, + 84.942, + 5.6, + 3.1 + ], + "settled": [ + 325.214, + 83.035, + 5.6, + 3.1 + ], + "hand": [ + -0.774, + -1.907 + ], + "to": [ + -2.586, + -3.665 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "134:387", + "page": 134, + "line": 11, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 204.5, + 378.6, + 4.4, + 3 + ], + "drawn": [ + 202.701, + 376.176, + 4.4, + 3 + ], + "settled": [ + 201.595, + 375.035, + 4.27, + 2.93 + ], + "hand": [ + -1.106, + -1.141 + ], + "to": [ + -2.905, + -3.565 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "137:73", + "page": 137, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 38.8, + 52.8, + 7, + 3.6 + ], + "drawn": [ + 38.359, + 52.36, + 7, + 3.6 + ], + "settled": [ + 35.049, + 49.223, + 7, + 3.6 + ], + "hand": [ + -3.31, + -3.137 + ], + "to": [ + -3.751, + -3.577 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "137:88", + "page": 137, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 227.7, + 106.2, + 7, + 3.6 + ], + "drawn": [ + 227.013, + 105.263, + 7, + 3.6 + ], + "settled": [ + 224.02, + 102.845, + 7, + 3.6 + ], + "hand": [ + -2.992, + -2.417 + ], + "to": [ + -3.68, + -3.355 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "137:91", + "page": 137, + "line": 3, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 202.3, + 83.1, + 4.4, + 3 + ], + "drawn": [ + 201.613, + 82.163, + 4.4, + 3 + ], + "settled": [ + 198.785, + 80.011, + 4.4, + 3 + ], + "hand": [ + -2.827, + -2.151 + ], + "to": [ + -3.515, + -3.089 + ], + "size": null, + "was": null, + "goes": 6, + "reshapes": 0, + "notes": [] + }, + { + "id": "138:242", + "page": 138, + "line": 7, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 45.5, + 251.2, + 7, + 3.6 + ], + "drawn": [ + 42.84, + 248.877, + 7, + 3.6 + ], + "settled": [ + 42.092, + 249.395, + 7.15, + 3.7 + ], + "hand": [ + -0.748, + 0.518 + ], + "to": [ + -3.408, + -1.805 + ], + "size": [ + 7.15, + 3.7 + ], + "was": [ + 7, + 3.6 + ], + "goes": 2, + "reshapes": 3, + "notes": [] + }, + { + "id": "138:251", + "page": 138, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 264.7, + 267, + 7.1, + 3.6 + ], + "drawn": [ + 261.839, + 265.556, + 7.1, + 3.6 + ], + "settled": [ + 261.233, + 265.181, + 7.2, + 3.7 + ], + "hand": [ + -0.606, + -0.375 + ], + "to": [ + -3.467, + -1.819 + ], + "size": [ + 7.2, + 3.7 + ], + "was": [ + 7.1, + 3.6 + ], + "goes": 2, + "reshapes": 1, + "notes": [] + }, + { + "id": "142:336", + "page": 142, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 104.9, + 299.7, + 7, + 3.6 + ], + "drawn": [ + 103.154, + 297.812, + 7, + 3.6 + ], + "settled": [ + 104.708, + 297.708, + 6.9, + 4.75 + ], + "hand": [ + 1.554, + -0.104 + ], + "to": [ + -0.192, + -1.992 + ], + "size": [ + 6.9, + 4.75 + ], + "was": [ + 7, + 3.6 + ], + "goes": 3, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "152:490", + "page": 152, + "line": 13, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 278.2, + 445.8, + 5.3, + 6.6 + ], + "drawn": [ + 275.998, + 442.891, + 5.3, + 6.6 + ], + "settled": [ + 275.77, + 442.81, + 5.4, + 6.62 + ], + "hand": [ + -0.228, + -0.081 + ], + "to": [ + -2.43, + -2.99 + ], + "size": [ + 5.4, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "154:261", + "page": 154, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 238.7, + 228.6, + 7, + 3.6 + ], + "drawn": [ + 237.673, + 228.564, + 7, + 3.6 + ], + "settled": [ + 233.625, + 225.675, + 6.93, + 3.47 + ], + "hand": [ + -4.048, + -2.889 + ], + "to": [ + -5.075, + -2.925 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "154:278", + "page": 154, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 71.2, + 233.2, + 6.3, + 3.4 + ], + "drawn": [ + 69.179, + 231.649, + 6.3, + 3.4 + ], + "settled": [ + 67.245, + 230.505, + 6.27, + 3.33 + ], + "hand": [ + -1.934, + -1.144 + ], + "to": [ + -3.955, + -2.695 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "158:58", + "page": 158, + "line": 2, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 197.5, + 55.6, + 4.3, + 3.3 + ], + "drawn": [ + 194.707, + 53.755, + 4.3, + 3.3 + ], + "settled": [ + 193.585, + 53.7, + 4.25, + 3.2 + ], + "hand": [ + -1.122, + -0.055 + ], + "to": [ + -3.915, + -1.9 + ], + "size": [ + 4.25, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "158:60", + "page": 158, + "line": 2, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 188.1, + 49.3, + 5.3, + 6.6 + ], + "drawn": [ + 185.268, + 47.464, + 5.3, + 6.6 + ], + "settled": [ + 184.173, + 47.464, + 5.3, + 6.6 + ], + "hand": [ + -1.095, + 0 + ], + "to": [ + -3.927, + -1.836 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "158:67", + "page": 158, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 140.3, + 69.4, + 7, + 3.6 + ], + "drawn": [ + 137.264, + 67.612, + 7, + 3.6 + ], + "settled": [ + 136.425, + 67.545, + 6.93, + 3.47 + ], + "hand": [ + -0.839, + -0.067 + ], + "to": [ + -3.875, + -1.855 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "158:78", + "page": 158, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 321.6, + 84.5, + 7.1, + 3.6 + ], + "drawn": [ + 318.733, + 83.196, + 7.1, + 3.6 + ], + "settled": [ + 318.635, + 83.015, + 6.93, + 3.47 + ], + "hand": [ + -0.098, + -0.181 + ], + "to": [ + -2.965, + -1.485 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7.1, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "168:160", + "page": 168, + "line": 5, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 120.3, + 182.5, + 7, + 3.6 + ], + "drawn": [ + 117.867, + 180.914, + 7, + 3.6 + ], + "settled": [ + 116.744, + 180.31, + 7, + 3.6 + ], + "hand": [ + -1.123, + -0.604 + ], + "to": [ + -3.556, + -2.19 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "176:15", + "page": 176, + "line": 1, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 252.1, + 12.2, + 4.3, + 3.3 + ], + "drawn": [ + 251.034, + 10.763, + 4.3, + 3.3 + ], + "settled": [ + 249.706, + 9.355, + 4.3, + 3.3 + ], + "hand": [ + -1.328, + -1.408 + ], + "to": [ + -2.394, + -2.845 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "192:278", + "page": 192, + "line": 9, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 234.5, + 299.3, + 5.3, + 6.6 + ], + "drawn": [ + 233.813, + 298.363, + 5.3, + 6.6 + ], + "settled": [ + 230.609, + 296.829, + 5.3, + 6.6 + ], + "hand": [ + -3.203, + -1.533 + ], + "to": [ + -3.891, + -2.471 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "192:298", + "page": 192, + "line": 10, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 292.3, + 337, + 5.3, + 6.6 + ], + "drawn": [ + 291.613, + 336.063, + 5.3, + 6.6 + ], + "settled": [ + 289.203, + 334.009, + 5.3, + 6.6 + ], + "hand": [ + -2.409, + -2.053 + ], + "to": [ + -3.097, + -2.991 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "206:24", + "page": 206, + "line": 1, + "name": "maddah", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 153.4, + 22.5, + 6.6, + 3.1 + ], + "drawn": [ + 151.996, + 20.771, + 6.6, + 3.1 + ], + "settled": [ + 150.495, + 19.065, + 6.67, + 3.07 + ], + "hand": [ + -1.501, + -1.706 + ], + "to": [ + -2.905, + -3.435 + ], + "size": [ + 6.67, + 3.07 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "206:280", + "page": 206, + "line": 8, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 108.1, + 286.6, + 6.3, + 3.4 + ], + "drawn": [ + 106.364, + 285.265, + 6.3, + 3.4 + ], + "settled": [ + 104.079, + 285.66, + 6.3, + 3.4 + ], + "hand": [ + -2.285, + 0.395 + ], + "to": [ + -4.021, + -0.94 + ], + "size": null, + "was": null, + "goes": 2, + "reshapes": 0, + "notes": [] + }, + { + "id": "206:296", + "page": 206, + "line": 9, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 285.7, + 298.7, + 5.3, + 4.3 + ], + "drawn": [ + 282.605, + 296.509, + 5.3, + 4.3 + ], + "settled": [ + 282.715, + 296.509, + 5.3, + 4.3 + ], + "hand": [ + 0.11, + 0 + ], + "to": [ + -2.985, + -2.191 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "214:122", + "page": 214, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 303.3, + 123.7, + 7, + 3.6 + ], + "drawn": [ + 300.211, + 121.059, + 7, + 3.6 + ], + "settled": [ + 300.285, + 120.745, + 6.93, + 3.47 + ], + "hand": [ + 0.074, + -0.314 + ], + "to": [ + -3.015, + -2.955 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "219:179", + "page": 219, + "line": 5, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 182.9, + 168.9, + 4, + 3.9 + ], + "drawn": [ + 181.838, + 167.767, + 4, + 3.9 + ], + "settled": [ + 181.125, + 167.395, + 4.27, + 3.73 + ], + "hand": [ + -0.713, + -0.372 + ], + "to": [ + -1.775, + -1.505 + ], + "size": [ + 4.27, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "220:48", + "page": 220, + "line": 2, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 229.8, + 53.6, + 4.4, + 3 + ], + "drawn": [ + 227.93, + 51.438, + 4.4, + 3 + ], + "settled": [ + 226.917, + 50.079, + 4.4, + 3 + ], + "hand": [ + -1.013, + -1.359 + ], + "to": [ + -2.883, + -3.521 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "220:410", + "page": 220, + "line": 12, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 200.1, + 407.5, + 7, + 3.5 + ], + "drawn": [ + 197.952, + 406.421, + 7, + 3.5 + ], + "settled": [ + 196.895, + 406.46, + 7.07, + 3.6 + ], + "hand": [ + -1.057, + 0.039 + ], + "to": [ + -3.205, + -1.04 + ], + "size": [ + 7.07, + 3.6 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "226:195", + "page": 226, + "line": 6, + "name": "small waw", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 183.4, + 207.3, + 5.5, + 5.7 + ], + "drawn": [ + 180.823, + 205.303, + 5.5, + 5.7 + ], + "settled": [ + 179.63, + 205.095, + 5.5, + 5.73 + ], + "hand": [ + -1.193, + -0.208 + ], + "to": [ + -3.77, + -2.205 + ], + "size": [ + 5.5, + 5.73 + ], + "was": [ + 5.5, + 5.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "230:249", + "page": 230, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 312.9, + 232, + 5.6, + 3.1 + ], + "drawn": [ + 310.684, + 230.464, + 5.6, + 3.1 + ], + "settled": [ + 312.701, + 230.822, + 5.7, + 3.8 + ], + "hand": [ + 2.017, + 0.358 + ], + "to": [ + -0.199, + -1.178 + ], + "size": [ + 5.7, + 3.8 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 3, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "232:0", + "page": 232, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 327.8, + 20.5, + 6.3, + 3.4 + ], + "drawn": [ + 325.924, + 18.391, + 6.3, + 3.4 + ], + "settled": [ + 324.091, + 17.658, + 6.3, + 3.4 + ], + "hand": [ + -1.833, + -0.733 + ], + "to": [ + -3.709, + -2.842 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "233:332", + "page": 233, + "line": 9, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 30.5, + 302.5, + 8.2, + 6.4 + ], + "drawn": [ + 29.922, + 301.28, + 8.2, + 6.4 + ], + "settled": [ + 29.501, + 300.893, + 7.05, + 6.4 + ], + "hand": [ + -0.421, + -0.387 + ], + "to": [ + -0.999, + -1.607 + ], + "size": [ + 7.05, + 6.4 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 4, + "reshapes": 2, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "242:429", + "page": 242, + "line": 12, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 218.3, + 407.7, + 6.3, + 3.4 + ], + "drawn": [ + 216.499, + 405.928, + 6.3, + 3.4 + ], + "settled": [ + 217.759, + 406.415, + 6.1, + 4.4 + ], + "hand": [ + 1.26, + 0.487 + ], + "to": [ + -0.541, + -1.285 + ], + "size": [ + 6.1, + 4.4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 7, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "244:128", + "page": 244, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 134.7, + 125, + 7, + 3.6 + ], + "drawn": [ + 134.411, + 124.304, + 7, + 3.6 + ], + "settled": [ + 129.895, + 120.085, + 6.93, + 3.47 + ], + "hand": [ + -4.516, + -4.219 + ], + "to": [ + -4.805, + -4.915 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "244:133", + "page": 244, + "line": 4, + "name": "rounded zero", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 45.5, + 119.1, + 3.2, + 4.1 + ], + "drawn": [ + 45.802, + 118.651, + 3.2, + 4.1 + ], + "settled": [ + 40.93, + 114.17, + 3.2, + 4.14 + ], + "hand": [ + -4.872, + -4.481 + ], + "to": [ + -4.57, + -4.93 + ], + "size": [ + 3.2, + 4.14 + ], + "was": [ + 3.2, + 4.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "244:153", + "page": 244, + "line": 5, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 211.5, + 157.9, + 5.3, + 6.6 + ], + "drawn": [ + 210.191, + 157.067, + 5.3, + 6.6 + ], + "settled": [ + 208.775, + 154.16, + 5.27, + 6.62 + ], + "hand": [ + -1.416, + -2.907 + ], + "to": [ + -2.725, + -3.74 + ], + "size": [ + 5.27, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "244:155", + "page": 244, + "line": 5, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 196.8, + 178.9, + 7, + 3.6 + ], + "drawn": [ + 195.488, + 177.897, + 7, + 3.6 + ], + "settled": [ + 193.934, + 174.817, + 7, + 3.6 + ], + "hand": [ + -1.554, + -3.08 + ], + "to": [ + -2.866, + -4.083 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "250:35", + "page": 250, + "line": 2, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 321.2, + 56.2, + 4.4, + 3 + ], + "drawn": [ + 319.862, + 53.36, + 4.4, + 3 + ], + "settled": [ + 319.409, + 53.12, + 4.4, + 3 + ], + "hand": [ + -0.453, + -0.24 + ], + "to": [ + -1.791, + -3.08 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "250:291", + "page": 250, + "line": 9, + "name": "damma", + "rule": "line-tilt", + "words": [ + "looks-right" + ], + "fault": false, + "box": [ + 236.5, + 300.4, + 5.3, + 6.6 + ], + "drawn": [ + 233.56, + 298.594, + 5.3, + 6.6 + ], + "settled": null, + "hand": null, + "to": null, + "size": null, + "was": null, + "goes": 0, + "reshapes": 0, + "notes": [] + }, + { + "id": "256:267", + "page": 256, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 213.1, + 297.4, + 7, + 3.6 + ], + "drawn": [ + 211.447, + 295.578, + 7, + 3.6 + ], + "settled": [ + 210.209, + 294.599, + 7, + 3.6 + ], + "hand": [ + -1.238, + -0.979 + ], + "to": [ + -2.891, + -2.801 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "260:212", + "page": 260, + "line": 6, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 39.5, + 196, + 4.4, + 3 + ], + "drawn": [ + 38.623, + 194.949, + 4.4, + 3 + ], + "settled": [ + 36.358, + 192.578, + 4.4, + 3 + ], + "hand": [ + -2.265, + -2.371 + ], + "to": [ + -3.142, + -3.422 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "260:221", + "page": 260, + "line": 7, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 284.5, + 245.6, + 7, + 3.6 + ], + "drawn": [ + 282.475, + 243.208, + 7, + 3.6 + ], + "settled": [ + 281.425, + 242.475, + 7.07, + 3.47 + ], + "hand": [ + -1.05, + -0.733 + ], + "to": [ + -3.075, + -3.125 + ], + "size": [ + 7.07, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "260:230", + "page": 260, + "line": 7, + "name": "rounded zero", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 196.6, + 226, + 3.2, + 4.1 + ], + "drawn": [ + 195.269, + 224.608, + 3.2, + 4.1 + ], + "settled": [ + 193.19, + 221.565, + 3.2, + 4.27 + ], + "hand": [ + -2.079, + -3.043 + ], + "to": [ + -3.41, + -4.435 + ], + "size": [ + 3.2, + 4.27 + ], + "was": [ + 3.2, + 4.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "260:231", + "page": 260, + "line": 7, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 188.5, + 226.4, + 5.3, + 4.3 + ], + "drawn": [ + 187.223, + 225.086, + 5.3, + 4.3 + ], + "settled": [ + 185.498, + 221.855, + 5.3, + 4.3 + ], + "hand": [ + -1.725, + -3.231 + ], + "to": [ + -3.002, + -4.545 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "260:237", + "page": 260, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 138.5, + 229, + 6.3, + 3.4 + ], + "drawn": [ + 137.606, + 228.237, + 6.3, + 3.4 + ], + "settled": [ + 135.775, + 224.775, + 6.27, + 3.33 + ], + "hand": [ + -1.831, + -3.462 + ], + "to": [ + -2.725, + -4.225 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "260:244", + "page": 260, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 74.2, + 232.3, + 6.3, + 3.4 + ], + "drawn": [ + 73.802, + 232.253, + 6.3, + 3.4 + ], + "settled": [ + 71.32, + 228.36, + 6.3, + 3.4 + ], + "hand": [ + -2.482, + -3.893 + ], + "to": [ + -2.88, + -3.94 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "260:247", + "page": 260, + "line": 7, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 86.5, + 230.7, + 4.4, + 3 + ], + "drawn": [ + 86.015, + 230.527, + 4.4, + 3 + ], + "settled": [ + 83.735, + 226.645, + 4.27, + 2.93 + ], + "hand": [ + -2.28, + -3.882 + ], + "to": [ + -2.765, + -4.055 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "260:257", + "page": 260, + "line": 8, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 322.3, + 269.3, + 4.4, + 3 + ], + "drawn": [ + 321.26, + 266.773, + 4.4, + 3 + ], + "settled": [ + 320.061, + 265.627, + 4.4, + 3 + ], + "hand": [ + -1.199, + -1.146 + ], + "to": [ + -2.239, + -3.673 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "263:76", + "page": 263, + "line": 3, + "name": "small waw", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 287.9, + 97.3, + 5.6, + 5.7 + ], + "drawn": [ + 285.983, + 94.589, + 5.6, + 5.7 + ], + "settled": [ + 286.011, + 94.285, + 5.6, + 5.7 + ], + "hand": [ + 0.028, + -0.304 + ], + "to": [ + -1.889, + -3.015 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "273:229", + "page": 273, + "line": 6, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 36.2, + 198.1, + 4.4, + 3 + ], + "drawn": [ + 35.829, + 197.246, + 4.4, + 3 + ], + "settled": [ + 32.795, + 193.975, + 4.27, + 2.93 + ], + "hand": [ + -3.034, + -3.271 + ], + "to": [ + -3.405, + -4.125 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "273:239", + "page": 273, + "line": 7, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 302.2, + 250.1, + 7, + 3.6 + ], + "drawn": [ + 302.055, + 249.485, + 7, + 3.6 + ], + "settled": [ + 298.955, + 246.215, + 6.93, + 3.47 + ], + "hand": [ + -3.1, + -3.27 + ], + "to": [ + -3.245, + -3.885 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "273:250", + "page": 273, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 216.3, + 235.4, + 5.6, + 3.1 + ], + "drawn": [ + 215.205, + 233.954, + 5.6, + 3.1 + ], + "settled": [ + 213.13, + 231.865, + 5.6, + 3.07 + ], + "hand": [ + -2.075, + -2.089 + ], + "to": [ + -3.17, + -3.535 + ], + "size": [ + 5.6, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "284:413", + "page": 284, + "line": 12, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 234.1, + 403.3, + 6.3, + 3.4 + ], + "drawn": [ + 233.021, + 400.462, + 6.3, + 3.4 + ], + "settled": [ + 231.035, + 400.31, + 6.13, + 3.2 + ], + "hand": [ + -1.986, + -0.152 + ], + "to": [ + -3.065, + -2.99 + ], + "size": [ + 6.13, + 3.2 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "285:453", + "page": 285, + "line": 12, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 202.9, + 405.7, + 9.1, + 4 + ], + "drawn": [ + 201.489, + 404.225, + 9.1, + 4 + ], + "settled": [ + 201.84, + 404.54, + 10, + 4.8 + ], + "hand": [ + 0.351, + 0.315 + ], + "to": [ + -1.06, + -1.16 + ], + "size": [ + 10, + 4.8 + ], + "was": [ + 9.1, + 4 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "288:188", + "page": 288, + "line": 5, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 121.9, + 176, + 4, + 3.9 + ], + "drawn": [ + 120.074, + 174.805, + 4, + 3.9 + ], + "settled": [ + 118.105, + 173.995, + 3.91, + 3.87 + ], + "hand": [ + -1.969, + -0.81 + ], + "to": [ + -3.795, + -2.005 + ], + "size": [ + 3.91, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "294:138", + "page": 294, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 121, + 112, + 5.6, + 3.1 + ], + "drawn": [ + 119.771, + 110.536, + 5.6, + 3.1 + ], + "settled": [ + 120.471, + 110.636, + 5.7, + 4.3 + ], + "hand": [ + 0.7, + 0.1 + ], + "to": [ + -0.529, + -1.364 + ], + "size": [ + 5.7, + 4.3 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 6, + "reshapes": 9, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "298:91", + "page": 298, + "line": 3, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 308.7, + 83.4, + 7.8, + 3.8 + ], + "drawn": [ + 307.204, + 81.319, + 7.8, + 3.8 + ], + "settled": [ + 307.485, + 81.645, + 9.33, + 5.47 + ], + "hand": [ + 0.281, + 0.326 + ], + "to": [ + -1.215, + -1.755 + ], + "size": [ + 9.33, + 5.47 + ], + "was": [ + 7.8, + 3.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "298:422", + "page": 298, + "line": 11, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 54.3, + 366.8, + 6.3, + 3.4 + ], + "drawn": [ + 51.406, + 365.2, + 6.3, + 3.4 + ], + "settled": [ + 50.983, + 365.341, + 6.3, + 3.4 + ], + "hand": [ + -0.423, + 0.141 + ], + "to": [ + -3.317, + -1.459 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "302:128", + "page": 302, + "line": 4, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 203.5, + 144.5, + 6.3, + 3.4 + ], + "drawn": [ + 202.5, + 143.149, + 6.3, + 3.4 + ], + "settled": [ + 201.845, + 141.575, + 6.13, + 3.33 + ], + "hand": [ + -0.655, + -1.574 + ], + "to": [ + -1.655, + -2.925 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "304:143", + "page": 304, + "line": 4, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 171.5, + 146.6, + 6.3, + 3.4 + ], + "drawn": [ + 169.048, + 144.421, + 6.3, + 3.4 + ], + "settled": [ + 171.11, + 144.915, + 6, + 4.67 + ], + "hand": [ + 2.062, + 0.494 + ], + "to": [ + -0.39, + -1.685 + ], + "size": [ + 6, + 4.67 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "308:90", + "page": 308, + "line": 3, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 257.7, + 94.3, + 2.1, + 6.1 + ], + "drawn": [ + 254.99, + 91.965, + 2.1, + 6.1 + ], + "settled": [ + 254.148, + 91.42, + 2.1, + 6.1 + ], + "hand": [ + -0.842, + -0.545 + ], + "to": [ + -3.552, + -2.88 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "314:249", + "page": 314, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 107.1, + 195.8, + 5.6, + 3.1 + ], + "drawn": [ + 104.956, + 193.931, + 5.6, + 3.1 + ], + "settled": [ + 103.409, + 193.738, + 5.6, + 3.1 + ], + "hand": [ + -1.547, + -0.193 + ], + "to": [ + -3.691, + -2.062 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "328:72", + "page": 328, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 89.8, + 55.8, + 8.5, + 3.9 + ], + "drawn": [ + 88.677, + 52.841, + 8.5, + 3.9 + ], + "settled": [ + 88.577, + 52.841, + 8.5, + 3.9 + ], + "hand": [ + -0.1, + 0 + ], + "to": [ + -1.223, + -2.959 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "338:354", + "page": 338, + "line": 10, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 183.4, + 337.8, + 4.3, + 3.3 + ], + "drawn": [ + 181.693, + 336.912, + 4.3, + 3.3 + ], + "settled": [ + 180.365, + 336.62, + 4.3, + 3.3 + ], + "hand": [ + -1.328, + -0.292 + ], + "to": [ + -3.035, + -1.18 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "343:116", + "page": 343, + "line": 3, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 6.8, + 88, + 9.4, + 4.1 + ], + "drawn": [ + 6.017, + 87.288, + 9.4, + 4.1 + ], + "settled": [ + 6.267, + 87.138, + 9.2, + 3.9 + ], + "hand": [ + 0.25, + -0.15 + ], + "to": [ + -0.533, + -0.862 + ], + "size": [ + 9.2, + 3.9 + ], + "was": [ + 9.4, + 4.1 + ], + "goes": 4, + "reshapes": 4, + "notes": [] + }, + { + "id": "343:420", + "page": 343, + "line": 11, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 143.5, + 370.1, + 8.2, + 6.4 + ], + "drawn": [ + 142.861, + 368.993, + 8.2, + 6.4 + ], + "settled": [ + 142.75, + 368.585, + 7.24, + 7.29 + ], + "hand": [ + -0.111, + -0.408 + ], + "to": [ + -0.75, + -1.515 + ], + "size": [ + 7.24, + 7.29 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "344:270", + "page": 344, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 281, + 224.5, + 7, + 3.5 + ], + "drawn": [ + 280.099, + 223.636, + 7, + 3.5 + ], + "settled": [ + 276.825, + 222.73, + 6.93, + 3.6 + ], + "hand": [ + -3.274, + -0.906 + ], + "to": [ + -4.175, + -1.77 + ], + "size": [ + 6.93, + 3.6 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "346:275", + "page": 346, + "line": 8, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 282.2, + 288.5, + 7, + 3.5 + ], + "drawn": [ + 280.814, + 285.959, + 7, + 3.5 + ], + "settled": [ + 280.495, + 285.465, + 7.07, + 3.47 + ], + "hand": [ + -0.319, + -0.494 + ], + "to": [ + -1.705, + -3.035 + ], + "size": [ + 7.07, + 3.47 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "355:48", + "page": 355, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 270.3, + 46.4, + 6.3, + 3.4 + ], + "drawn": [ + 269.222, + 45.087, + 6.3, + 3.4 + ], + "settled": [ + 269.912, + 45.207, + 6.41, + 4.14 + ], + "hand": [ + 0.69, + 0.12 + ], + "to": [ + -0.388, + -1.193 + ], + "size": [ + 6.41, + 4.14 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 7, + "reshapes": 7, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "364:242", + "page": 364, + "line": 7, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 201, + 250.1, + 7, + 3.6 + ], + "drawn": [ + 199.609, + 248.649, + 7, + 3.6 + ], + "settled": [ + 196.961, + 246.893, + 7, + 3.6 + ], + "hand": [ + -2.648, + -1.756 + ], + "to": [ + -4.039, + -3.207 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "370:5", + "page": 370, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 295.1, + 23.7, + 5.6, + 3.1 + ], + "drawn": [ + 292.275, + 22.263, + 5.6, + 3.1 + ], + "settled": [ + 292.125, + 22.135, + 5.47, + 3.07 + ], + "hand": [ + -0.15, + -0.128 + ], + "to": [ + -2.975, + -1.565 + ], + "size": [ + 5.47, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "375:18", + "page": 375, + "line": 1, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 206.5, + 37.1, + 6.3, + 3.4 + ], + "drawn": [ + 205.194, + 34.829, + 6.3, + 3.4 + ], + "settled": [ + 205.78, + 35.185, + 6, + 5.47 + ], + "hand": [ + 0.586, + 0.356 + ], + "to": [ + -0.72, + -1.915 + ], + "size": [ + 6, + 5.47 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "376:117", + "page": 376, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 276.7, + 157.2, + 8.5, + 3.9 + ], + "drawn": [ + 275.737, + 156.587, + 8.5, + 3.9 + ], + "settled": [ + 273.47, + 153.335, + 8.4, + 3.87 + ], + "hand": [ + -2.267, + -3.252 + ], + "to": [ + -3.23, + -3.865 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "376:118", + "page": 376, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 266.1, + 157.2, + 7.8, + 3.7 + ], + "drawn": [ + 265.055, + 156.498, + 7.8, + 3.7 + ], + "settled": [ + 262.847, + 153.318, + 7.8, + 3.7 + ], + "hand": [ + -2.208, + -3.18 + ], + "to": [ + -3.253, + -3.882 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "380:442", + "page": 380, + "line": 11, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 154.3, + 370.3, + 4, + 3.9 + ], + "drawn": [ + 153.157, + 368.651, + 4, + 3.9 + ], + "settled": [ + 150.21, + 367.335, + 4, + 3.9 + ], + "hand": [ + -2.947, + -1.316 + ], + "to": [ + -4.09, + -2.965 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "388:7", + "page": 388, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 281.3, + 15.1, + 7.8, + 3.7 + ], + "drawn": [ + 280.363, + 14.038, + 7.8, + 3.7 + ], + "settled": [ + 277.085, + 11.235, + 7.73, + 3.73 + ], + "hand": [ + -3.277, + -2.802 + ], + "to": [ + -4.215, + -3.865 + ], + "size": [ + 7.73, + 3.73 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "390:39", + "page": 390, + "line": 1, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 10.6, + 21.7, + 8.2, + 6.4 + ], + "drawn": [ + 9.785, + 20.764, + 8.2, + 6.4 + ], + "settled": [ + 9.683, + 20.535, + 7.3, + 6.65 + ], + "hand": [ + -0.102, + -0.229 + ], + "to": [ + -0.917, + -1.165 + ], + "size": [ + 7.3, + 6.65 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 4, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "396:97", + "page": 396, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 139.4, + 108, + 4.9, + 2.6 + ], + "drawn": [ + 137.452, + 106.676, + 4.9, + 2.6 + ], + "settled": [ + 136.18, + 106.216, + 4.9, + 2.6 + ], + "hand": [ + -1.272, + -0.46 + ], + "to": [ + -3.22, + -1.784 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "400:235", + "page": 400, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 32.9, + 226.7, + 4, + 3.9 + ], + "drawn": [ + 30.322, + 224.168, + 4, + 3.9 + ], + "settled": [ + 28.217, + 224.194, + 4, + 3.9 + ], + "hand": [ + -2.105, + 0.026 + ], + "to": [ + -4.683, + -2.506 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "400:241", + "page": 400, + "line": 8, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 311.7, + 282.8, + 6.3, + 3.4 + ], + "drawn": [ + 310.592, + 281.383, + 6.3, + 3.4 + ], + "settled": [ + 307.175, + 280.505, + 6.13, + 3.33 + ], + "hand": [ + -3.417, + -0.878 + ], + "to": [ + -4.525, + -2.295 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "408:365", + "page": 408, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 276.5, + 333.6, + 4, + 3.9 + ], + "drawn": [ + 273.825, + 331.144, + 4, + 3.9 + ], + "settled": [ + 273.009, + 330.618, + 4, + 3.9 + ], + "hand": [ + -0.816, + -0.526 + ], + "to": [ + -3.491, + -2.982 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "408:385", + "page": 408, + "line": 10, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 103.6, + 335.3, + 7.8, + 3.7 + ], + "drawn": [ + 100.542, + 333.377, + 7.8, + 3.7 + ], + "settled": [ + 100.295, + 333.355, + 7.73, + 3.73 + ], + "hand": [ + -0.247, + -0.022 + ], + "to": [ + -3.305, + -1.945 + ], + "size": [ + 7.73, + 3.73 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "410:227", + "page": 410, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 295.9, + 232.3, + 7, + 3.6 + ], + "drawn": [ + 292.669, + 229.31, + 7, + 3.6 + ], + "settled": [ + 292.381, + 229.281, + 7, + 3.6 + ], + "hand": [ + -0.288, + -0.029 + ], + "to": [ + -3.519, + -3.019 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "410:236", + "page": 410, + "line": 7, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 203.3, + 231.8, + 4.4, + 3 + ], + "drawn": [ + 200.324, + 229.076, + 4.4, + 3 + ], + "settled": [ + 200.324, + 228.926, + 4.4, + 3 + ], + "hand": [ + 0, + -0.15 + ], + "to": [ + -2.976, + -2.874 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "414:217", + "page": 414, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 295.4, + 229.4, + 6.3, + 3.4 + ], + "drawn": [ + 294.362, + 226.704, + 6.3, + 3.4 + ], + "settled": [ + 294.108, + 226.45, + 6.3, + 3.4 + ], + "hand": [ + -0.254, + -0.254 + ], + "to": [ + -1.292, + -2.95 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "414:249", + "page": 414, + "line": 8, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 318, + 261.6, + 5.3, + 4.3 + ], + "drawn": [ + 316.107, + 260.689, + 5.3, + 4.3 + ], + "settled": [ + 314.135, + 260.278, + 5.3, + 4.3 + ], + "hand": [ + -1.972, + -0.411 + ], + "to": [ + -3.865, + -1.322 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "416:298", + "page": 416, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 83.9, + 303.3, + 6.3, + 3.4 + ], + "drawn": [ + 81.665, + 301.406, + 6.3, + 3.4 + ], + "settled": [ + 80.845, + 300.905, + 6.27, + 3.33 + ], + "hand": [ + -0.82, + -0.501 + ], + "to": [ + -3.055, + -2.395 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "418:243", + "page": 418, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 9.4, + 265, + 6.3, + 3.4 + ], + "drawn": [ + 8.29, + 264.259, + 6.3, + 3.4 + ], + "settled": [ + 5.045, + 263.705, + 6.13, + 3.33 + ], + "hand": [ + -3.245, + -0.554 + ], + "to": [ + -4.355, + -1.295 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "418:258", + "page": 418, + "line": 9, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 164.6, + 302.4, + 5.3, + 6.6 + ], + "drawn": [ + 161.94, + 301.529, + 5.3, + 6.6 + ], + "settled": [ + 160.97, + 301.62, + 5.4, + 6.62 + ], + "hand": [ + -0.97, + 0.091 + ], + "to": [ + -3.63, + -0.78 + ], + "size": [ + 5.4, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "423:299", + "page": 423, + "line": 8, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 175.4, + 281.9, + 7.7, + 5.8 + ], + "drawn": [ + 174.851, + 280.602, + 7.7, + 5.8 + ], + "settled": [ + 175.001, + 280.702, + 8.05, + 5.7 + ], + "hand": [ + 0.15, + 0.1 + ], + "to": [ + -0.399, + -1.198 + ], + "size": [ + 8.05, + 5.7 + ], + "was": [ + 7.7, + 5.8 + ], + "goes": 4, + "reshapes": 4, + "notes": [] + }, + { + "id": "424:82", + "page": 424, + "line": 3, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 330.1, + 103.6, + 4, + 3.9 + ], + "drawn": [ + 326.805, + 101.369, + 4, + 3.9 + ], + "settled": [ + 325.963, + 100.869, + 4, + 3.9 + ], + "hand": [ + -0.842, + -0.5 + ], + "to": [ + -4.137, + -2.731 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "424:99", + "page": 424, + "line": 3, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 202.5, + 87.8, + 8.9, + 4.3 + ], + "drawn": [ + 199.655, + 85.645, + 8.9, + 4.3 + ], + "settled": [ + 199.48, + 85.46, + 8.8, + 4.26 + ], + "hand": [ + -0.175, + -0.185 + ], + "to": [ + -3.02, + -2.34 + ], + "size": [ + 8.8, + 4.26 + ], + "was": [ + 8.9, + 4.3 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "428:109", + "page": 428, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 30.3, + 160.6, + 7, + 3.6 + ], + "drawn": [ + 27.934, + 158.962, + 7, + 3.6 + ], + "settled": [ + 26.783, + 159.048, + 7.2, + 3.7 + ], + "hand": [ + -1.151, + 0.086 + ], + "to": [ + -3.517, + -1.552 + ], + "size": [ + 7.2, + 3.7 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 4, + "notes": [] + }, + { + "id": "428:111", + "page": 428, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 12.9, + 161.1, + 7, + 3.6 + ], + "drawn": [ + 10.385, + 159.395, + 7, + 3.6 + ], + "settled": [ + 9.378, + 159.568, + 7, + 3.6 + ], + "hand": [ + -1.007, + 0.173 + ], + "to": [ + -3.522, + -1.532 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "430:92", + "page": 430, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 154.2, + 90.2, + 7, + 3.6 + ], + "drawn": [ + 152.62, + 88.605, + 7, + 3.6 + ], + "settled": [ + 153.92, + 88.855, + 7.35, + 4.75 + ], + "hand": [ + 1.3, + 0.25 + ], + "to": [ + -0.28, + -1.345 + ], + "size": [ + 7.35, + 4.75 + ], + "was": [ + 7, + 3.6 + ], + "goes": 9, + "reshapes": 6, + "notes": [] + }, + { + "id": "437:117", + "page": 437, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 215.5, + 117.1, + 5.6, + 3.1 + ], + "drawn": [ + 213.512, + 115.149, + 5.6, + 3.1 + ], + "settled": [ + 215.273, + 115.725, + 5.8, + 4.75 + ], + "hand": [ + 1.761, + 0.576 + ], + "to": [ + -0.227, + -1.375 + ], + "size": [ + 5.8, + 4.75 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 8, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "438:168", + "page": 438, + "line": 5, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 175.3, + 159.4, + 4.3, + 3.3 + ], + "drawn": [ + 173.507, + 157.619, + 4.3, + 3.3 + ], + "settled": [ + 171.45, + 156.9, + 4.12, + 3.2 + ], + "hand": [ + -2.057, + -0.719 + ], + "to": [ + -3.85, + -2.5 + ], + "size": [ + 4.12, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "438:179", + "page": 438, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 68.3, + 156.5, + 7, + 3.5 + ], + "drawn": [ + 65.9, + 154.671, + 7, + 3.5 + ], + "settled": [ + 65.225, + 154.425, + 6.93, + 3.53 + ], + "hand": [ + -0.675, + -0.246 + ], + "to": [ + -3.075, + -2.075 + ], + "size": [ + 6.93, + 3.53 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "440:66", + "page": 440, + "line": 2, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 24.1, + 67.3, + 4, + 3.9 + ], + "drawn": [ + 23.455, + 65.812, + 4, + 3.9 + ], + "settled": [ + 23.087, + 63.812, + 4, + 3.9 + ], + "hand": [ + -0.368, + -2 + ], + "to": [ + -1.013, + -3.488 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "440:72", + "page": 440, + "line": 3, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 294.9, + 81.6, + 4, + 3.9 + ], + "drawn": [ + 294.025, + 80.413, + 4, + 3.9 + ], + "settled": [ + 292.165, + 78.595, + 5.91, + 3.73 + ], + "hand": [ + -1.86, + -1.817 + ], + "to": [ + -2.735, + -3.005 + ], + "size": [ + 5.91, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "440:75", + "page": 440, + "line": 3, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "print-defect" + ], + "fault": true, + "box": [ + 268, + 78.8, + 5.3, + 6.6 + ], + "drawn": [ + 267.125, + 77.613, + 5.3, + 6.6 + ], + "settled": [ + 264.14, + 75.833, + 5.3, + 6.6 + ], + "hand": [ + -2.985, + -1.779 + ], + "to": [ + -3.86, + -2.967 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "440:81", + "page": 440, + "line": 3, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 219.9, + 90.7, + 4.3, + 3.3 + ], + "drawn": [ + 219.025, + 89.513, + 4.3, + 3.3 + ], + "settled": [ + 216.215, + 87.915, + 4.19, + 3.11 + ], + "hand": [ + -2.81, + -1.598 + ], + "to": [ + -3.685, + -2.785 + ], + "size": [ + 4.19, + 3.11 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "441:470", + "page": 441, + "line": 13, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 135.6, + 467, + 8.7, + 6.8 + ], + "drawn": [ + 134.621, + 465.716, + 8.7, + 6.8 + ], + "settled": [ + 134.688, + 465.825, + 7.55, + 7.5 + ], + "hand": [ + 0.067, + 0.109 + ], + "to": [ + -0.912, + -1.175 + ], + "size": [ + 7.55, + 7.5 + ], + "was": [ + 8.7, + 6.8 + ], + "goes": 7, + "reshapes": 6, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "456:22", + "page": 456, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 197.4, + 16.1, + 6.3, + 3.4 + ], + "drawn": [ + 195.827, + 14.83, + 6.3, + 3.4 + ], + "settled": [ + 193.965, + 13.532, + 6.3, + 3.4 + ], + "hand": [ + -1.862, + -1.298 + ], + "to": [ + -3.435, + -2.568 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "456:32", + "page": 456, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 140.8, + 14.4, + 5.6, + 3.1 + ], + "drawn": [ + 138.732, + 12.914, + 5.6, + 3.1 + ], + "settled": [ + 137.737, + 12.251, + 5.6, + 3.1 + ], + "hand": [ + -0.995, + -0.663 + ], + "to": [ + -3.063, + -2.149 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "461:417", + "page": 461, + "line": 12, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 261, + 423.1, + 8, + 6.3 + ], + "drawn": [ + 259.867, + 421.892, + 8, + 6.3 + ], + "settled": [ + 260.059, + 421.686, + 7.1, + 7.5 + ], + "hand": [ + 0.192, + -0.206 + ], + "to": [ + -0.941, + -1.414 + ], + "size": [ + 7.1, + 7.5 + ], + "was": [ + 8, + 6.3 + ], + "goes": 4, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "462:386", + "page": 462, + "line": 12, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 271.9, + 402.5, + 6.3, + 3.4 + ], + "drawn": [ + 270.298, + 400.612, + 6.3, + 3.4 + ], + "settled": [ + 271.088, + 401.063, + 6.3, + 4.45 + ], + "hand": [ + 0.79, + 0.451 + ], + "to": [ + -0.812, + -1.437 + ], + "size": [ + 6.3, + 4.45 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 4, + "reshapes": 3, + "notes": [] + }, + { + "id": "466:408", + "page": 466, + "line": 12, + "name": "maddah", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 235.6, + 407.7, + 6.6, + 3.1 + ], + "drawn": [ + 235.328, + 406.727, + 6.6, + 3.1 + ], + "settled": [ + 230.861, + 404.877, + 6.9, + 3.3 + ], + "hand": [ + -4.467, + -1.85 + ], + "to": [ + -4.739, + -2.823 + ], + "size": [ + 6.9, + 3.3 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 3, + "reshapes": 3, + "notes": [] + }, + { + "id": "466:412", + "page": 466, + "line": 12, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 185, + 406, + 5.3, + 6.7 + ], + "drawn": [ + 184.33, + 404.658, + 5.3, + 6.7 + ], + "settled": [ + 180.36, + 403.3, + 5.3, + 6.62 + ], + "hand": [ + -3.97, + -1.358 + ], + "to": [ + -4.64, + -2.7 + ], + "size": [ + 5.3, + 6.62 + ], + "was": [ + 5.3, + 6.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "466:413", + "page": 466, + "line": 12, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 181.4, + 428.7, + 6.3, + 3.4 + ], + "drawn": [ + 180.706, + 427.335, + 6.3, + 3.4 + ], + "settled": [ + 176.701, + 425.868, + 6.45, + 3.5 + ], + "hand": [ + -4.005, + -1.467 + ], + "to": [ + -4.699, + -2.832 + ], + "size": [ + 6.45, + 3.5 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 2, + "notes": [] + }, + { + "id": "466:415", + "page": 466, + "line": 12, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 154.5, + 412.7, + 4.4, + 3 + ], + "drawn": [ + 153.591, + 411.135, + 4.4, + 3 + ], + "settled": [ + 149.861, + 409.803, + 4.4, + 3 + ], + "hand": [ + -3.73, + -1.332 + ], + "to": [ + -4.639, + -2.897 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "466:432", + "page": 466, + "line": 12, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 17.1, + 407.8, + 5.3, + 6.7 + ], + "drawn": [ + 15.129, + 405.248, + 5.3, + 6.7 + ], + "settled": [ + 13.376, + 405.33, + 5.3, + 6.7 + ], + "hand": [ + -1.753, + 0.082 + ], + "to": [ + -3.724, + -2.47 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "470:69", + "page": 470, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 53.7, + 51.7, + 7.8, + 3.7 + ], + "drawn": [ + 50.513, + 49.009, + 7.8, + 3.7 + ], + "settled": [ + 50.425, + 49.16, + 7.73, + 3.6 + ], + "hand": [ + -0.088, + 0.151 + ], + "to": [ + -3.275, + -2.54 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "472:400", + "page": 472, + "line": 12, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 233, + 414.3, + 4.3, + 3.3 + ], + "drawn": [ + 231.783, + 412.926, + 4.3, + 3.3 + ], + "settled": [ + 229.25, + 412.36, + 4.26, + 3.2 + ], + "hand": [ + -2.533, + -0.566 + ], + "to": [ + -3.75, + -1.94 + ], + "size": [ + 4.26, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "474:272", + "page": 474, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 250.5, + 303.6, + 8.5, + 3.9 + ], + "drawn": [ + 248.34, + 302.216, + 8.5, + 3.9 + ], + "settled": [ + 247.34, + 301.995, + 8.4, + 3.87 + ], + "hand": [ + -1, + -0.221 + ], + "to": [ + -3.16, + -1.605 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "480:293", + "page": 480, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 240.9, + 304.8, + 7.8, + 3.7 + ], + "drawn": [ + 238.072, + 304.01, + 7.8, + 3.7 + ], + "settled": [ + 237.885, + 304.025, + 7.73, + 3.73 + ], + "hand": [ + -0.187, + 0.015 + ], + "to": [ + -3.015, + -0.775 + ], + "size": [ + 7.73, + 3.73 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "490:157", + "page": 490, + "line": 5, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 322.9, + 155.3, + 4.3, + 3.3 + ], + "drawn": [ + 321.05, + 152.719, + 4.3, + 3.3 + ], + "settled": [ + 318.367, + 152.746, + 4.3, + 3.3 + ], + "hand": [ + -2.683, + 0.027 + ], + "to": [ + -4.533, + -2.554 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "490:171", + "page": 490, + "line": 5, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 172.8, + 158, + 5.3, + 6.6 + ], + "drawn": [ + 170.947, + 155.959, + 5.3, + 6.6 + ], + "settled": [ + 168.675, + 155.89, + 5.21, + 6.62 + ], + "hand": [ + -2.272, + -0.069 + ], + "to": [ + -4.125, + -2.11 + ], + "size": [ + 5.21, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "490:180", + "page": 490, + "line": 5, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 112.3, + 176, + 4.9, + 2.6 + ], + "drawn": [ + 110.446, + 174.179, + 4.9, + 2.6 + ], + "settled": [ + 108.28, + 173.705, + 4.8, + 2.53 + ], + "hand": [ + -2.166, + -0.474 + ], + "to": [ + -4.02, + -2.295 + ], + "size": [ + 4.8, + 2.53 + ], + "was": [ + 4.9, + 2.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "490:219", + "page": 490, + "line": 6, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 75.9, + 196.1, + 6.1, + 5.3 + ], + "drawn": [ + 72.999, + 194.806, + 6.1, + 5.3 + ], + "settled": [ + 73.719, + 194.636, + 7.5, + 5.43 + ], + "hand": [ + 0.72, + -0.17 + ], + "to": [ + -2.181, + -1.464 + ], + "size": [ + 7.5, + 5.43 + ], + "was": [ + 6.1, + 5.3 + ], + "goes": 4, + "reshapes": 4, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "490:223", + "page": 490, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 56.3, + 193.7, + 7, + 3.6 + ], + "drawn": [ + 53.412, + 192.457, + 7, + 3.6 + ], + "settled": [ + 53.225, + 192.345, + 6.93, + 3.47 + ], + "hand": [ + -0.187, + -0.112 + ], + "to": [ + -3.075, + -1.355 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "492:188", + "page": 492, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 309.5, + 198, + 7.8, + 3.7 + ], + "drawn": [ + 308.75, + 197, + 7.8, + 3.7 + ], + "settled": [ + 304.952, + 195.587, + 7.8, + 3.7 + ], + "hand": [ + -3.798, + -1.413 + ], + "to": [ + -4.548, + -2.413 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "492:199", + "page": 492, + "line": 6, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 226.2, + 200.7, + 4, + 3.9 + ], + "drawn": [ + 225.45, + 199.7, + 4, + 3.9 + ], + "settled": [ + 221.92, + 198.595, + 4, + 3.73 + ], + "hand": [ + -3.53, + -1.105 + ], + "to": [ + -4.28, + -2.105 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "492:200", + "page": 492, + "line": 6, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 225.4, + 196.2, + 4.4, + 3 + ], + "drawn": [ + 224.65, + 195.2, + 4.4, + 3 + ], + "settled": [ + 221.195, + 193.975, + 4.27, + 2.93 + ], + "hand": [ + -3.455, + -1.225 + ], + "to": [ + -4.205, + -2.225 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "496:192", + "page": 496, + "line": 8, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 221.1, + 271.4, + 4.3, + 3.3 + ], + "drawn": [ + 219.161, + 270.328, + 4.3, + 3.3 + ], + "settled": [ + 217.685, + 270.23, + 4.19, + 3.2 + ], + "hand": [ + -1.476, + -0.098 + ], + "to": [ + -3.415, + -1.17 + ], + "size": [ + 4.19, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "496:200", + "page": 496, + "line": 8, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 111.7, + 287.6, + 6.3, + 3.4 + ], + "drawn": [ + 109.096, + 286.499, + 6.3, + 3.4 + ], + "settled": [ + 111.175, + 286.705, + 5.87, + 3.73 + ], + "hand": [ + 2.079, + 0.206 + ], + "to": [ + -0.525, + -0.895 + ], + "size": [ + 5.87, + 3.73 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "496:203", + "page": 496, + "line": 8, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 76.8, + 285.9, + 7.8, + 5.9 + ], + "drawn": [ + 73.987, + 284.79, + 7.8, + 5.9 + ], + "settled": [ + 73.765, + 284.79, + 7.73, + 5.86 + ], + "hand": [ + -0.222, + 0 + ], + "to": [ + -3.035, + -1.11 + ], + "size": [ + 7.73, + 5.86 + ], + "was": [ + 7.8, + 5.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "502:78", + "page": 502, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 21.8, + 50.1, + 6.3, + 3.4 + ], + "drawn": [ + 19.623, + 49.071, + 6.3, + 3.4 + ], + "settled": [ + 18.444, + 48.669, + 6.3, + 3.4 + ], + "hand": [ + -1.179, + -0.402 + ], + "to": [ + -3.356, + -1.431 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "506:117", + "page": 506, + "line": 4, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 215.8, + 121.1, + 10.3, + 4.3 + ], + "drawn": [ + 215.117, + 119.787, + 10.3, + 4.3 + ], + "settled": [ + 215.308, + 119.618, + 10.3, + 4.3 + ], + "hand": [ + 0.191, + -0.169 + ], + "to": [ + -0.492, + -1.482 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "507:545", + "page": 507, + "line": 14, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 320, + 479.1, + 6.8, + 3.5 + ], + "drawn": [ + 319.978, + 477.435, + 6.8, + 3.5 + ], + "settled": [ + 320.698, + 478.505, + 6.9, + 3.53 + ], + "hand": [ + 0.72, + 1.07 + ], + "to": [ + 0.698, + -0.595 + ], + "size": [ + 6.9, + 3.53 + ], + "was": [ + 6.8, + 3.5 + ], + "goes": 7, + "reshapes": 3, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "514:251", + "page": 514, + "line": 7, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 135.1, + 252.4, + 4.9, + 2.6 + ], + "drawn": [ + 132.983, + 250.952, + 4.9, + 2.6 + ], + "settled": [ + 132.016, + 250.368, + 4.9, + 2.6 + ], + "hand": [ + -0.967, + -0.584 + ], + "to": [ + -3.084, + -2.032 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "514:254", + "page": 514, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 101.4, + 232.9, + 8.5, + 3.9 + ], + "drawn": [ + 98.991, + 231.34, + 8.5, + 3.9 + ], + "settled": [ + 98.275, + 230.921, + 8.5, + 3.9 + ], + "hand": [ + -0.716, + -0.419 + ], + "to": [ + -3.125, + -1.979 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "530:321", + "page": 530, + "line": 9, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 223.6, + 306, + 4.4, + 3 + ], + "drawn": [ + 222.706, + 303.932, + 4.4, + 3 + ], + "settled": [ + 219.92, + 302.775, + 4.4, + 2.93 + ], + "hand": [ + -2.786, + -1.157 + ], + "to": [ + -3.68, + -3.225 + ], + "size": [ + 4.4, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "534:180", + "page": 534, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 126, + 195.8, + 6.3, + 3.4 + ], + "drawn": [ + 125.063, + 194.613, + 6.3, + 3.4 + ], + "settled": [ + 122.693, + 193.447, + 6.3, + 3.4 + ], + "hand": [ + -2.369, + -1.166 + ], + "to": [ + -3.307, + -2.353 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "538:259", + "page": 538, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 125.6, + 226.4, + 4, + 3.9 + ], + "drawn": [ + 123.748, + 225.444, + 4, + 3.9 + ], + "settled": [ + 123.19, + 225.465, + 5.2, + 3.87 + ], + "hand": [ + -0.558, + 0.021 + ], + "to": [ + -2.41, + -0.935 + ], + "size": [ + 5.2, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "554:328", + "page": 554, + "line": 10, + "name": "rounded zero", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 8.8, + 334.2, + 3.2, + 4.1 + ], + "drawn": [ + 8.047, + 332.184, + 3.2, + 4.1 + ], + "settled": [ + 7.545, + 331.164, + 3.2, + 4.35 + ], + "hand": [ + -0.502, + -1.02 + ], + "to": [ + -1.255, + -3.036 + ], + "size": [ + 3.2, + 4.35 + ], + "was": [ + 3.2, + 4.1 + ], + "goes": 4, + "reshapes": 3, + "notes": [] + }, + { + "id": "567:259", + "page": 567, + "line": 7, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 313.5, + 235.9, + 2, + 6.1 + ], + "drawn": [ + 311.242, + 234.666, + 2, + 6.1 + ], + "settled": [ + 309.259, + 233.928, + 2.05, + 6.05 + ], + "hand": [ + -1.983, + -0.738 + ], + "to": [ + -4.241, + -1.972 + ], + "size": [ + 2.05, + 6.05 + ], + "was": [ + 2, + 6.1 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "567:266", + "page": 567, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 246, + 248.2, + 3.9, + 3.9 + ], + "drawn": [ + 243.642, + 246.832, + 3.9, + 3.9 + ], + "settled": [ + 242.175, + 246.665, + 3.87, + 3.87 + ], + "hand": [ + -1.467, + -0.167 + ], + "to": [ + -3.825, + -1.535 + ], + "size": [ + 3.87, + 3.87 + ], + "was": [ + 3.9, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "568:410", + "page": 568, + "line": 14, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 330.8, + 500.2, + 4, + 3.9 + ], + "drawn": [ + 328.467, + 498.155, + 4, + 3.9 + ], + "settled": [ + 328.63, + 497.915, + 4.44, + 3.73 + ], + "hand": [ + 0.163, + -0.24 + ], + "to": [ + -2.17, + -2.285 + ], + "size": [ + 4.44, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "572:98", + "page": 572, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 290.5, + 157.8, + 7.1, + 3.5 + ], + "drawn": [ + 288.678, + 156.944, + 7.1, + 3.5 + ], + "settled": [ + 286.78, + 157.192, + 7.1, + 3.5 + ], + "hand": [ + -1.898, + 0.248 + ], + "to": [ + -3.72, + -0.608 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "572:106", + "page": 572, + "line": 5, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 238.9, + 162.4, + 4.3, + 3.3 + ], + "drawn": [ + 237.089, + 161.166, + 4.3, + 3.3 + ], + "settled": [ + 235.815, + 161.7, + 4.19, + 3.2 + ], + "hand": [ + -1.274, + 0.534 + ], + "to": [ + -3.085, + -0.7 + ], + "size": [ + 4.19, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "572:107", + "page": 572, + "line": 5, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 239.1, + 181.3, + 7, + 3.6 + ], + "drawn": [ + 237.288, + 180.077, + 7, + 3.6 + ], + "settled": [ + 235.885, + 180.475, + 6.93, + 3.47 + ], + "hand": [ + -1.403, + 0.398 + ], + "to": [ + -3.215, + -0.825 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "576:20", + "page": 576, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 179.4, + 14.3, + 6.3, + 3.4 + ], + "drawn": [ + 177.359, + 11.441, + 6.3, + 3.4 + ], + "settled": [ + 177.489, + 11.299, + 6.3, + 3.4 + ], + "hand": [ + 0.13, + -0.142 + ], + "to": [ + -1.911, + -3.001 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "578:125", + "page": 578, + "line": 4, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 267.9, + 119.7, + 4.4, + 3 + ], + "drawn": [ + 266.708, + 117.367, + 4.4, + 3 + ], + "settled": [ + 266.109, + 115.856, + 4.4, + 3 + ], + "hand": [ + -0.599, + -1.511 + ], + "to": [ + -1.791, + -3.844 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "578:141", + "page": 578, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 136.2, + 120.6, + 7, + 3.6 + ], + "drawn": [ + 134.963, + 118.371, + 7, + 3.6 + ], + "settled": [ + 132.286, + 117.018, + 7, + 3.6 + ], + "hand": [ + -2.677, + -1.353 + ], + "to": [ + -3.914, + -3.582 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "578:256", + "page": 578, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 237.7, + 230.6, + 5.6, + 3.1 + ], + "drawn": [ + 235.685, + 229.139, + 5.6, + 3.1 + ], + "settled": [ + 234.234, + 227.297, + 5.6, + 3.1 + ], + "hand": [ + -1.451, + -1.842 + ], + "to": [ + -3.466, + -3.303 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "580:20", + "page": 580, + "line": 1, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 168.4, + 20.1, + 4.4, + 3 + ], + "drawn": [ + 166.16, + 18.62, + 4.4, + 3 + ], + "settled": [ + 164.525, + 17.52, + 4.27, + 2.8 + ], + "hand": [ + -1.635, + -1.1 + ], + "to": [ + -3.875, + -2.58 + ], + "size": [ + 4.27, + 2.8 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "581:18", + "page": 581, + "line": 1, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 207.6, + 36, + 7.4, + 5.9 + ], + "drawn": [ + 207.035, + 34.211, + 7.4, + 5.9 + ], + "settled": [ + 207.111, + 34.561, + 7.2, + 7 + ], + "hand": [ + 0.076, + 0.35 + ], + "to": [ + -0.489, + -1.439 + ], + "size": [ + 7.2, + 7 + ], + "was": [ + 7.4, + 5.9 + ], + "goes": 3, + "reshapes": 2, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "582:249", + "page": 582, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 317.6, + 334.9, + 4, + 3.9 + ], + "drawn": [ + 316.74, + 333.549, + 4, + 3.9 + ], + "settled": [ + 316.092, + 333.416, + 4.6, + 3.9 + ], + "hand": [ + -0.648, + -0.133 + ], + "to": [ + -1.508, + -1.484 + ], + "size": [ + 4.6, + 3.9 + ], + "was": [ + 4, + 3.9 + ], + "goes": 2, + "reshapes": 3, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "582:310", + "page": 582, + "line": 11, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 108, + 380.5, + 4.4, + 3 + ], + "drawn": [ + 106.915, + 379.557, + 4.4, + 3 + ], + "settled": [ + 104.226, + 377.494, + 4.4, + 3 + ], + "hand": [ + -2.689, + -2.063 + ], + "to": [ + -3.774, + -3.006 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "582:322", + "page": 582, + "line": 12, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 319.3, + 412.6, + 7, + 3.6 + ], + "drawn": [ + 318.425, + 411.538, + 7, + 3.6 + ], + "settled": [ + 315.885, + 409.405, + 6.93, + 3.47 + ], + "hand": [ + -2.54, + -2.132 + ], + "to": [ + -3.415, + -3.195 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:57", + "page": 588, + "line": 2, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 188, + 55.9, + 4.4, + 3 + ], + "drawn": [ + 185.216, + 54.467, + 4.4, + 3 + ], + "settled": [ + 184.99, + 54.115, + 4.4, + 2.93 + ], + "hand": [ + -0.226, + -0.352 + ], + "to": [ + -3.01, + -1.785 + ], + "size": [ + 4.4, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:132", + "page": 588, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement" + ], + "fault": true, + "box": [ + 256.9, + 124.3, + 7, + 3.6 + ], + "drawn": [ + 256.947, + 123.487, + 7, + 3.6 + ], + "settled": [ + 250.385, + 121.702, + 7, + 3.6 + ], + "hand": [ + -6.562, + -1.785 + ], + "to": [ + -6.515, + -2.598 + ], + "size": null, + "was": null, + "goes": 1, + "reshapes": 0, + "notes": [] + }, + { + "id": "588:148", + "page": 588, + "line": 4, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 152.2, + 141.1, + 6.3, + 3.4 + ], + "drawn": [ + 152.388, + 140.686, + 6.3, + 3.4 + ], + "settled": [ + 146.245, + 138.775, + 6.13, + 3.33 + ], + "hand": [ + -6.143, + -1.911 + ], + "to": [ + -5.955, + -2.325 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:185", + "page": 588, + "line": 5, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 199.1, + 165, + 4.3, + 3.3 + ], + "drawn": [ + 198.32, + 164.148, + 4.3, + 3.3 + ], + "settled": [ + 193.955, + 162.1, + 4.19, + 3.2 + ], + "hand": [ + -4.365, + -2.048 + ], + "to": [ + -5.145, + -2.9 + ], + "size": [ + 4.19, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:195", + "page": 588, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 96.9, + 157.4, + 5.6, + 3.1 + ], + "drawn": [ + 95.723, + 155.663, + 5.6, + 3.1 + ], + "settled": [ + 92.46, + 154.795, + 5.6, + 3.07 + ], + "hand": [ + -3.263, + -0.868 + ], + "to": [ + -4.44, + -2.605 + ], + "size": [ + 5.6, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "593:139", + "page": 593, + "line": 6, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 24.8, + 215.4, + 5.1, + 5 + ], + "drawn": [ + 24.059, + 214.806, + 5.1, + 5 + ], + "settled": [ + 23.98, + 214.245, + 6, + 5.73 + ], + "hand": [ + -0.079, + -0.561 + ], + "to": [ + -0.82, + -1.155 + ], + "size": [ + 6, + 5.73 + ], + "was": [ + 5.1, + 5 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "604:50", + "page": 604, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 306.9, + 228.9, + 4, + 3.9 + ], + "drawn": [ + 305.376, + 226.414, + 4, + 3.9 + ], + "settled": [ + 304.896, + 226.325, + 5.6, + 3.9 + ], + "hand": [ + -0.48, + -0.089 + ], + "to": [ + -2.004, + -2.575 + ], + "size": [ + 5.6, + 3.9 + ], + "was": [ + 4, + 3.9 + ], + "goes": 3, + "reshapes": 5, + "notes": [ + "Odd — I cannot say how" + ] + } + ] +} diff --git a/docs/validation/rulings/2026-08-17-placement-edge-still-placed.seed23.settled.json b/docs/validation/rulings/2026-08-17-placement-edge-still-placed.seed23.settled.json new file mode 100644 index 00000000..f3001bc2 --- /dev/null +++ b/docs/validation/rulings/2026-08-17-placement-edge-still-placed.seed23.settled.json @@ -0,0 +1,3961 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-19T19:25:27.128Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "5442d961", + "set": "edge", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-edge-b0.55_1.01-ab3a14b92.23.json", + "slice": "-b0.55_1.01-ab3a14b92", + "finished": "2026-08-17T22:38:59.727Z", + "shown": 40, + "seen": 40, + "looked": 40, + "whole": true, + "answers": 82, + "spoke": 40 + }, + { + "file": "mark-report-edge-b0.55_1.01-a8edff256.23.json", + "slice": "-b0.55_1.01-a8edff256", + "finished": "2026-08-19T19:17:27.664Z", + "shown": 40, + "seen": 40, + "looked": 40, + "whole": true, + "answers": 82, + "spoke": 40 + } + ], + "banked": [], + "marks": 80, + "faulted": 80, + "defects": 0, + "answers": 164, + "settledMarks": [ + { + "id": "30:407", + "page": 30, + "line": 10, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 278.6, + 345.6, + 4.4, + 3 + ], + "drawn": [ + 275.779, + 344.212, + 4.4, + 3 + ], + "settled": [ + 275.66, + 343.945, + 4.4, + 2.87 + ], + "hand": [ + -0.119, + -0.267 + ], + "to": [ + -2.94, + -1.655 + ], + "size": [ + 4.4, + 2.87 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "40:107", + "page": 40, + "line": 3, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 16.8, + 81.8, + 5.3, + 6.6 + ], + "drawn": [ + 13.339, + 79.185, + 5.3, + 6.6 + ], + "settled": [ + 13.865, + 79.89, + 5.23, + 6.62 + ], + "hand": [ + 0.526, + 0.705 + ], + "to": [ + -2.935, + -1.91 + ], + "size": [ + 5.23, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "46:342", + "page": 46, + "line": 12, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 99.7, + 409, + 7.7, + 3.9 + ], + "drawn": [ + 98.95, + 407.875, + 7.7, + 3.9 + ], + "settled": [ + 98.075, + 405.99, + 7.73, + 3.86 + ], + "hand": [ + -0.875, + -1.885 + ], + "to": [ + -1.625, + -3.01 + ], + "size": [ + 7.73, + 3.86 + ], + "was": [ + 7.7, + 3.9 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "96:164", + "page": 96, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 321.1, + 197.5, + 7, + 3.5 + ], + "drawn": [ + 318.314, + 195.913, + 7, + 3.5 + ], + "settled": [ + 318.155, + 195.865, + 6.93, + 3.47 + ], + "hand": [ + -0.159, + -0.048 + ], + "to": [ + -2.945, + -1.635 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "113:132", + "page": 113, + "line": 4, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 76.2, + 143.1, + 4.8, + 2.6 + ], + "drawn": [ + 73.053, + 144.992, + 4.8, + 2.6 + ], + "settled": [ + 73.26, + 145.315, + 4.8, + 2.53 + ], + "hand": [ + 0.207, + 0.323 + ], + "to": [ + -2.94, + 2.215 + ], + "size": [ + 4.8, + 2.53 + ], + "was": [ + 4.8, + 2.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "113:217", + "page": 113, + "line": 7, + "name": "maddah", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 180.7, + 232.7, + 6.6, + 3.1 + ], + "drawn": [ + 177.965, + 234.104, + 6.6, + 3.1 + ], + "settled": [ + 177.695, + 234.125, + 6.67, + 3.07 + ], + "hand": [ + -0.27, + 0.021 + ], + "to": [ + -3.005, + 1.425 + ], + "size": [ + 6.67, + 3.07 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "113:230", + "page": 113, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 95.3, + 220.5, + 6.2, + 3.4 + ], + "drawn": [ + 92.148, + 222.031, + 6.2, + 3.4 + ], + "settled": [ + 92.295, + 222.04, + 6.13, + 3.2 + ], + "hand": [ + 0.147, + 0.009 + ], + "to": [ + -3.005, + 1.54 + ], + "size": [ + 6.13, + 3.2 + ], + "was": [ + 6.2, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "116:318", + "page": 116, + "line": 9, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 187.6, + 305.6, + 4.4, + 3 + ], + "drawn": [ + 185.051, + 303.359, + 4.4, + 3 + ], + "settled": [ + 184.59, + 303.11, + 4.4, + 2.8 + ], + "hand": [ + -0.461, + -0.249 + ], + "to": [ + -3.01, + -2.49 + ], + "size": [ + 4.4, + 2.8 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "130:147", + "page": 130, + "line": 4, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 313.7, + 148.1, + 7, + 3.6 + ], + "drawn": [ + 310.74, + 146.598, + 7, + 3.6 + ], + "settled": [ + 310.685, + 146.615, + 6.93, + 3.47 + ], + "hand": [ + -0.055, + 0.017 + ], + "to": [ + -3.015, + -1.485 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "137:111", + "page": 137, + "line": 4, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 313.9, + 127.3, + 4.3, + 3.3 + ], + "drawn": [ + 311.538, + 124.146, + 4.3, + 3.3 + ], + "settled": [ + 311.155, + 124.315, + 4.19, + 3.11 + ], + "hand": [ + -0.383, + 0.169 + ], + "to": [ + -2.745, + -2.985 + ], + "size": [ + 4.19, + 3.11 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "141:148", + "page": 141, + "line": 5, + "name": "maddah", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 309.4, + 159.8, + 6.6, + 3.1 + ], + "drawn": [ + 309.079, + 156.759, + 6.6, + 3.1 + ], + "settled": [ + 308.495, + 156.935, + 6.67, + 3.07 + ], + "hand": [ + -0.584, + 0.176 + ], + "to": [ + -0.905, + -2.865 + ], + "size": [ + 6.67, + 3.07 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "150:287", + "page": 150, + "line": 7, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 42.3, + 233.2, + 2.1, + 6.1 + ], + "drawn": [ + 40.12, + 231.325, + 2.1, + 6.1 + ], + "settled": [ + 39.275, + 231.16, + 2.13, + 6 + ], + "hand": [ + -0.845, + -0.165 + ], + "to": [ + -3.025, + -2.04 + ], + "size": [ + 2.13, + 6 + ], + "was": [ + 2.1, + 6.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "152:483", + "page": 152, + "line": 13, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 321.3, + 445.3, + 6.3, + 3.4 + ], + "drawn": [ + 318.833, + 442.173, + 6.3, + 3.4 + ], + "settled": [ + 318.505, + 442.235, + 6.13, + 3.33 + ], + "hand": [ + -0.328, + 0.062 + ], + "to": [ + -2.795, + -3.065 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "158:74", + "page": 158, + "line": 2, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 56.8, + 49.8, + 5.3, + 6.6 + ], + "drawn": [ + 54.443, + 47.952, + 5.3, + 6.6 + ], + "settled": [ + 53.745, + 48.195, + 5.21, + 6.69 + ], + "hand": [ + -0.698, + 0.243 + ], + "to": [ + -3.055, + -1.605 + ], + "size": [ + 5.21, + 6.69 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "162:167", + "page": 162, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 320.9, + 158, + 7, + 3.6 + ], + "drawn": [ + 320.077, + 154.958, + 7, + 3.6 + ], + "settled": [ + 320.685, + 154.885, + 6.93, + 3.47 + ], + "hand": [ + 0.608, + -0.073 + ], + "to": [ + -0.215, + -3.115 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "168:169", + "page": 168, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 38.2, + 160.8, + 6.2, + 3.4 + ], + "drawn": [ + 36.021, + 159.261, + 6.2, + 3.4 + ], + "settled": [ + 35.295, + 159.045, + 6.27, + 3.33 + ], + "hand": [ + -0.726, + -0.216 + ], + "to": [ + -2.905, + -1.755 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.2, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "172:232", + "page": 172, + "line": 6, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 29.9, + 196.1, + 5.3, + 6.6 + ], + "drawn": [ + 29.556, + 194.843, + 5.3, + 6.6 + ], + "settled": [ + 26.775, + 194.055, + 5.27, + 6.69 + ], + "hand": [ + -2.781, + -0.788 + ], + "to": [ + -3.125, + -2.045 + ], + "size": [ + 5.27, + 6.69 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "176:14", + "page": 176, + "line": 1, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 257.2, + 11.9, + 5.3, + 4.3 + ], + "drawn": [ + 256.145, + 10.492, + 5.3, + 4.3 + ], + "settled": [ + 254.83, + 8.835, + 5.24, + 4.33 + ], + "hand": [ + -1.315, + -1.657 + ], + "to": [ + -2.37, + -3.065 + ], + "size": [ + 5.24, + 4.33 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "192:301", + "page": 192, + "line": 10, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 255.3, + 337.6, + 7.8, + 3.7 + ], + "drawn": [ + 254.675, + 336.663, + 7.8, + 3.7 + ], + "settled": [ + 252.225, + 334.555, + 7.87, + 3.73 + ], + "hand": [ + -2.45, + -2.107 + ], + "to": [ + -3.075, + -3.045 + ], + "size": [ + 7.87, + 3.73 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "198:364", + "page": 198, + "line": 11, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 201.8, + 377.5, + 6.3, + 3.4 + ], + "drawn": [ + 199.097, + 376.384, + 6.3, + 3.4 + ], + "settled": [ + 198.775, + 376.61, + 6.13, + 3.4 + ], + "hand": [ + -0.322, + 0.226 + ], + "to": [ + -3.025, + -0.89 + ], + "size": [ + 6.13, + 3.4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "202:119", + "page": 202, + "line": 4, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 296, + 132.1, + 4, + 3.9 + ], + "drawn": [ + 293.087, + 130.674, + 4, + 3.9 + ], + "settled": [ + 293.035, + 130.395, + 3.91, + 3.87 + ], + "hand": [ + -0.052, + -0.279 + ], + "to": [ + -2.965, + -1.705 + ], + "size": [ + 3.91, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "206:174", + "page": 206, + "line": 5, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 161.1, + 156.9, + 4, + 3.9 + ], + "drawn": [ + 159.32, + 155.722, + 4, + 3.9 + ], + "settled": [ + 158.06, + 154.865, + 4, + 3.73 + ], + "hand": [ + -1.26, + -0.857 + ], + "to": [ + -3.04, + -2.035 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "206:180", + "page": 206, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 118, + 162, + 7.8, + 3.7 + ], + "drawn": [ + 115.91, + 160.805, + 7.8, + 3.7 + ], + "settled": [ + 114.955, + 160.36, + 7.73, + 3.6 + ], + "hand": [ + -0.955, + -0.445 + ], + "to": [ + -3.045, + -1.64 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "216:208", + "page": 216, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 206.7, + 228.1, + 7, + 3.6 + ], + "drawn": [ + 204.916, + 226.648, + 7, + 3.6 + ], + "settled": [ + 203.625, + 225.545, + 6.93, + 3.47 + ], + "hand": [ + -1.291, + -1.103 + ], + "to": [ + -3.075, + -2.555 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "226:205", + "page": 226, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 81.5, + 194.3, + 7, + 3.6 + ], + "drawn": [ + 78.901, + 192.391, + 7, + 3.6 + ], + "settled": [ + 78.425, + 192.345, + 6.93, + 3.47 + ], + "hand": [ + -0.476, + -0.046 + ], + "to": [ + -3.075, + -1.955 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "232:19", + "page": 232, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 155.2, + 12.2, + 4, + 3.9 + ], + "drawn": [ + 152.984, + 10.456, + 4, + 3.9 + ], + "settled": [ + 152.19, + 10.195, + 4, + 3.73 + ], + "hand": [ + -0.794, + -0.261 + ], + "to": [ + -3.01, + -2.005 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "250:199", + "page": 250, + "line": 6, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 159.2, + 195.2, + 5.3, + 6.6 + ], + "drawn": [ + 157.627, + 193.653, + 5.3, + 6.6 + ], + "settled": [ + 156.135, + 193.125, + 5.21, + 6.69 + ], + "hand": [ + -1.492, + -0.528 + ], + "to": [ + -3.065, + -2.075 + ], + "size": [ + 5.21, + 6.69 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "250:292", + "page": 250, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 226.8, + 301.4, + 7.8, + 3.7 + ], + "drawn": [ + 224.437, + 299.579, + 7.8, + 3.7 + ], + "settled": [ + 223.755, + 299.495, + 7.73, + 3.73 + ], + "hand": [ + -0.682, + -0.084 + ], + "to": [ + -3.045, + -1.905 + ], + "size": [ + 7.73, + 3.73 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "254:97", + "page": 254, + "line": 3, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 132.5, + 91.7, + 2.1, + 6 + ], + "drawn": [ + 130.437, + 89.861, + 2.1, + 6 + ], + "settled": [ + 129.495, + 89.145, + 2.03, + 5.87 + ], + "hand": [ + -0.942, + -0.716 + ], + "to": [ + -3.005, + -2.555 + ], + "size": [ + 2.03, + 5.87 + ], + "was": [ + 2.1, + 6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "266:462", + "page": 266, + "line": 13, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 172.4, + 443.5, + 7, + 3.5 + ], + "drawn": [ + 169.708, + 441.533, + 7, + 3.5 + ], + "settled": [ + 169.355, + 441.325, + 6.93, + 3.47 + ], + "hand": [ + -0.353, + -0.208 + ], + "to": [ + -3.045, + -2.175 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "288:206", + "page": 288, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 293.9, + 198.3, + 6.3, + 3.4 + ], + "drawn": [ + 291.163, + 197.487, + 6.3, + 3.4 + ], + "settled": [ + 290.905, + 197.11, + 6.13, + 3.2 + ], + "hand": [ + -0.258, + -0.377 + ], + "to": [ + -2.995, + -1.19 + ], + "size": [ + 6.13, + 3.2 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "299:43", + "page": 299, + "line": 2, + "name": "dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 307.3, + 52.4, + 5.9, + 8.7 + ], + "drawn": [ + 305.008, + 49.805, + 5.9, + 8.7 + ], + "settled": [ + 304.94, + 49.49, + 6, + 8.52 + ], + "hand": [ + -0.068, + -0.315 + ], + "to": [ + -2.36, + -2.91 + ], + "size": [ + 6, + 8.52 + ], + "was": [ + 5.9, + 8.7 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "314:257", + "page": 314, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 38.7, + 200.9, + 7.8, + 3.7 + ], + "drawn": [ + 35.029, + 198.778, + 7.8, + 3.7 + ], + "settled": [ + 35.625, + 198.89, + 7.73, + 3.6 + ], + "hand": [ + 0.596, + 0.112 + ], + "to": [ + -3.075, + -2.01 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "315:147", + "page": 315, + "line": 4, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 26.2, + 124.4, + 4.4, + 3 + ], + "drawn": [ + 24.914, + 123.489, + 4.4, + 3 + ], + "settled": [ + 23.13, + 122.775, + 4.4, + 2.93 + ], + "hand": [ + -1.784, + -0.714 + ], + "to": [ + -3.07, + -1.625 + ], + "size": [ + 4.4, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "320:268", + "page": 320, + "line": 8, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 242.2, + 277.2, + 4, + 3.9 + ], + "drawn": [ + 239.69, + 275.021, + 4, + 3.9 + ], + "settled": [ + 239.195, + 274.925, + 3.87, + 3.87 + ], + "hand": [ + -0.495, + -0.096 + ], + "to": [ + -3.005, + -2.275 + ], + "size": [ + 3.87, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "332:31", + "page": 332, + "line": 3, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 136.9, + 83.6, + 5.3, + 4.3 + ], + "drawn": [ + 135.402, + 82.197, + 5.3, + 4.3 + ], + "settled": [ + 133.9, + 81.865, + 5.24, + 4.27 + ], + "hand": [ + -1.502, + -0.332 + ], + "to": [ + -3, + -1.735 + ], + "size": [ + 5.24, + 4.27 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "334:5", + "page": 334, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 293.4, + 15.5, + 7, + 3.6 + ], + "drawn": [ + 290.564, + 12.846, + 7, + 3.6 + ], + "settled": [ + 290.285, + 12.755, + 6.93, + 3.47 + ], + "hand": [ + -0.279, + -0.091 + ], + "to": [ + -3.115, + -2.745 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "338:357", + "page": 338, + "line": 10, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 163.4, + 341.6, + 7, + 3.5 + ], + "drawn": [ + 161.547, + 340.681, + 7, + 3.5 + ], + "settled": [ + 160.295, + 340.46, + 6.93, + 3.6 + ], + "hand": [ + -1.252, + -0.221 + ], + "to": [ + -3.105, + -1.14 + ], + "size": [ + 6.93, + 3.6 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "344:292", + "page": 344, + "line": 7, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 96.8, + 229, + 5.3, + 6.6 + ], + "drawn": [ + 94.233, + 227.79, + 5.3, + 6.6 + ], + "settled": [ + 93.56, + 227.855, + 5.3, + 6.57 + ], + "hand": [ + -0.673, + 0.065 + ], + "to": [ + -3.24, + -1.145 + ], + "size": [ + 5.3, + 6.57 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "370:421", + "page": 370, + "line": 12, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 177.8, + 408.9, + 7.8, + 3.7 + ], + "drawn": [ + 176.028, + 407.355, + 7.8, + 3.7 + ], + "settled": [ + 174.765, + 406.555, + 7.87, + 3.73 + ], + "hand": [ + -1.263, + -0.8 + ], + "to": [ + -3.035, + -2.345 + ], + "size": [ + 7.87, + 3.73 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "375:7", + "page": 375, + "line": 1, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 286, + 13.5, + 4.3, + 3.3 + ], + "drawn": [ + 285.018, + 11.929, + 4.3, + 3.3 + ], + "settled": [ + 284.085, + 10.37, + 4.19, + 3.2 + ], + "hand": [ + -0.933, + -1.559 + ], + "to": [ + -1.915, + -3.13 + ], + "size": [ + 4.19, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "384:90", + "page": 384, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 234.3, + 81.6, + 6.3, + 3.4 + ], + "drawn": [ + 232.597, + 80.263, + 6.3, + 3.4 + ], + "settled": [ + 231.175, + 80.245, + 6.13, + 3.33 + ], + "hand": [ + -1.422, + -0.018 + ], + "to": [ + -3.125, + -1.355 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "388:213", + "page": 388, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 277.9, + 228.9, + 7.8, + 3.7 + ], + "drawn": [ + 274.843, + 227.993, + 7.8, + 3.7 + ], + "settled": [ + 274.825, + 227.96, + 7.73, + 3.6 + ], + "hand": [ + -0.018, + -0.033 + ], + "to": [ + -3.075, + -0.94 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "389:422", + "page": 389, + "line": 12, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 7, + 407.1, + 6.8, + 3.4 + ], + "drawn": [ + 3.928, + 405.456, + 6.8, + 3.4 + ], + "settled": [ + 3.935, + 405.67, + 6.53, + 3.26 + ], + "hand": [ + 0.007, + 0.214 + ], + "to": [ + -3.065, + -1.43 + ], + "size": [ + 6.53, + 3.26 + ], + "was": [ + 6.8, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "401:310", + "page": 401, + "line": 11, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 311.9, + 372, + 4, + 3.9 + ], + "drawn": [ + 308.906, + 372.001, + 4, + 3.9 + ], + "settled": [ + 308.86, + 371.925, + 4, + 3.73 + ], + "hand": [ + -0.046, + -0.076 + ], + "to": [ + -3.04, + -0.075 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "416:301", + "page": 416, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 64.6, + 301.2, + 7.8, + 3.7 + ], + "drawn": [ + 62.556, + 299.405, + 7.8, + 3.7 + ], + "settled": [ + 61.565, + 298.955, + 7.87, + 3.73 + ], + "hand": [ + -0.991, + -0.45 + ], + "to": [ + -3.035, + -2.245 + ], + "size": [ + 7.87, + 3.73 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "420:337", + "page": 420, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 251.2, + 303.8, + 7, + 3.6 + ], + "drawn": [ + 249.331, + 303.009, + 7, + 3.6 + ], + "settled": [ + 248.355, + 303.21, + 7.07, + 3.6 + ], + "hand": [ + -0.976, + 0.201 + ], + "to": [ + -2.845, + -0.59 + ], + "size": [ + 7.07, + 3.6 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "422:97", + "page": 422, + "line": 3, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 151.5, + 87.5, + 4.3, + 3.3 + ], + "drawn": [ + 150.181, + 85.822, + 4.3, + 3.3 + ], + "settled": [ + 148.38, + 86.1, + 4.12, + 3.2 + ], + "hand": [ + -1.801, + 0.278 + ], + "to": [ + -3.12, + -1.4 + ], + "size": [ + 4.12, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "424:92", + "page": 424, + "line": 3, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 267.5, + 86.7, + 4.4, + 3 + ], + "drawn": [ + 264.837, + 84.301, + 4.4, + 3 + ], + "settled": [ + 264.46, + 84.375, + 4.4, + 2.93 + ], + "hand": [ + -0.377, + 0.074 + ], + "to": [ + -3.04, + -2.325 + ], + "size": [ + 4.4, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "432:134", + "page": 432, + "line": 4, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 181, + 126.4, + 4.4, + 3 + ], + "drawn": [ + 179.426, + 124.379, + 4.4, + 3 + ], + "settled": [ + 177.995, + 123.315, + 4.27, + 2.93 + ], + "hand": [ + -1.431, + -1.064 + ], + "to": [ + -3.005, + -3.085 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "440:69", + "page": 440, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 317.5, + 87.9, + 8.5, + 3.9 + ], + "drawn": [ + 316.625, + 86.65, + 8.5, + 3.9 + ], + "settled": [ + 316.41, + 84.935, + 8.4, + 3.87 + ], + "hand": [ + -0.215, + -1.715 + ], + "to": [ + -1.09, + -2.965 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "450:378", + "page": 450, + "line": 11, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 183.7, + 377.5, + 4, + 3.9 + ], + "drawn": [ + 181.666, + 376.805, + 4, + 3.9 + ], + "settled": [ + 180.73, + 377.455, + 4, + 3.87 + ], + "hand": [ + -0.936, + 0.65 + ], + "to": [ + -2.97, + -0.045 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "456:145", + "page": 456, + "line": 4, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 317, + 121.9, + 4, + 3.9 + ], + "drawn": [ + 313.969, + 120.025, + 4, + 3.9 + ], + "settled": [ + 313.92, + 119.865, + 4, + 3.87 + ], + "hand": [ + -0.049, + -0.16 + ], + "to": [ + -3.08, + -2.035 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "456:150", + "page": 456, + "line": 4, + "name": "wasla", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 278.5, + 118.8, + 5.3, + 4.3 + ], + "drawn": [ + 275.738, + 116.996, + 5.3, + 4.3 + ], + "settled": [ + 275.49, + 116.865, + 5.24, + 4.13 + ], + "hand": [ + -0.248, + -0.131 + ], + "to": [ + -3.01, + -1.935 + ], + "size": [ + 5.24, + 4.13 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "456:152", + "page": 456, + "line": 4, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 264.9, + 121.6, + 4, + 3.9 + ], + "drawn": [ + 262.24, + 119.823, + 4, + 3.9 + ], + "settled": [ + 261.92, + 119.735, + 4, + 3.87 + ], + "hand": [ + -0.32, + -0.088 + ], + "to": [ + -2.98, + -1.865 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "456:156", + "page": 456, + "line": 4, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 247.2, + 130.2, + 2.1, + 6.1 + ], + "drawn": [ + 244.672, + 128.458, + 2.1, + 6.1 + ], + "settled": [ + 244.14, + 128.36, + 2, + 6 + ], + "hand": [ + -0.532, + -0.098 + ], + "to": [ + -3.06, + -1.84 + ], + "size": [ + 2, + 6 + ], + "was": [ + 2.1, + 6.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "466:435", + "page": 466, + "line": 13, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 311, + 441.9, + 7, + 3.6 + ], + "drawn": [ + 310.063, + 440.65, + 7, + 3.6 + ], + "settled": [ + 308.025, + 439.675, + 6.93, + 3.47 + ], + "hand": [ + -2.037, + -0.975 + ], + "to": [ + -2.975, + -2.225 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "474:276", + "page": 474, + "line": 9, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 189.9, + 303.6, + 5.3, + 6.6 + ], + "drawn": [ + 187.709, + 302.181, + 5.3, + 6.6 + ], + "settled": [ + 186.89, + 302.15, + 5.3, + 6.62 + ], + "hand": [ + -0.819, + -0.031 + ], + "to": [ + -3.01, + -1.45 + ], + "size": [ + 5.3, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "474:434", + "page": 474, + "line": 14, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 253, + 482.7, + 5.3, + 6.6 + ], + "drawn": [ + 250.274, + 480.94, + 5.3, + 6.6 + ], + "settled": [ + 250.005, + 480.925, + 5.21, + 6.67 + ], + "hand": [ + -0.269, + -0.015 + ], + "to": [ + -2.995, + -1.775 + ], + "size": [ + 5.21, + 6.67 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "490:227", + "page": 490, + "line": 6, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 41.5, + 217.3, + 4.9, + 2.6 + ], + "drawn": [ + 38.642, + 216.094, + 4.9, + 2.6 + ], + "settled": [ + 38.475, + 215.975, + 4.93, + 2.53 + ], + "hand": [ + -0.167, + -0.119 + ], + "to": [ + -3.025, + -1.325 + ], + "size": [ + 4.93, + 2.53 + ], + "was": [ + 4.9, + 2.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "492:226", + "page": 492, + "line": 7, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 275.5, + 238, + 4.4, + 3 + ], + "drawn": [ + 273.028, + 236.813, + 4.4, + 3 + ], + "settled": [ + 272.525, + 236.645, + 4.27, + 2.93 + ], + "hand": [ + -0.503, + -0.168 + ], + "to": [ + -2.975, + -1.355 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "496:198", + "page": 496, + "line": 8, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 133.3, + 267.7, + 5.3, + 6.6 + ], + "drawn": [ + 130.555, + 266.271, + 5.3, + 6.6 + ], + "settled": [ + 130.23, + 266.52, + 5.3, + 6.66 + ], + "hand": [ + -0.325, + 0.249 + ], + "to": [ + -3.07, + -1.18 + ], + "size": [ + 5.3, + 6.66 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "512:1", + "page": 512, + "line": 1, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 329.1, + 38.7, + 4.9, + 2.6 + ], + "drawn": [ + 326.278, + 35.959, + 4.9, + 2.6 + ], + "settled": [ + 326.01, + 36.05, + 4.8, + 2.4 + ], + "hand": [ + -0.268, + 0.091 + ], + "to": [ + -3.09, + -2.65 + ], + "size": [ + 4.8, + 2.4 + ], + "was": [ + 4.9, + 2.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "515:104", + "page": 515, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 191.1, + 109.3, + 5.6, + 3.1 + ], + "drawn": [ + 189.215, + 107.772, + 5.6, + 3.1 + ], + "settled": [ + 188.06, + 107.735, + 5.6, + 3.07 + ], + "hand": [ + -1.155, + -0.037 + ], + "to": [ + -3.04, + -1.565 + ], + "size": [ + 5.6, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "518:212", + "page": 518, + "line": 8, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 63.6, + 283.8, + 5.6, + 3.1 + ], + "drawn": [ + 62.231, + 282.656, + 5.6, + 3.1 + ], + "settled": [ + 60.535, + 282.125, + 5.47, + 3.07 + ], + "hand": [ + -1.696, + -0.531 + ], + "to": [ + -3.065, + -1.675 + ], + "size": [ + 5.47, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "518:213", + "page": 518, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 49, + 268.3, + 7.8, + 3.7 + ], + "drawn": [ + 47.56, + 267.166, + 7.8, + 3.7 + ], + "settled": [ + 46.025, + 266.63, + 7.73, + 3.6 + ], + "hand": [ + -1.535, + -0.536 + ], + "to": [ + -2.975, + -1.67 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "518:216", + "page": 518, + "line": 8, + "name": "superscript alef", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 27.5, + 271.7, + 2.1, + 6.1 + ], + "drawn": [ + 25.931, + 270.585, + 2.1, + 6.1 + ], + "settled": [ + 24.41, + 270.09, + 2, + 6 + ], + "hand": [ + -1.521, + -0.495 + ], + "to": [ + -3.09, + -1.61 + ], + "size": [ + 2, + 6 + ], + "was": [ + 2.1, + 6.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "530:334", + "page": 530, + "line": 9, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 124.2, + 303.4, + 4.3, + 3.3 + ], + "drawn": [ + 121.948, + 300.832, + 4.3, + 3.3 + ], + "settled": [ + 121.155, + 300.49, + 4.19, + 3.2 + ], + "hand": [ + -0.793, + -0.342 + ], + "to": [ + -3.045, + -2.91 + ], + "size": [ + 4.19, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "530:336", + "page": 530, + "line": 9, + "name": "sukun", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 116.6, + 299, + 4.4, + 3 + ], + "drawn": [ + 114.323, + 296.436, + 4.4, + 3 + ], + "settled": [ + 113.595, + 296.04, + 4.27, + 2.8 + ], + "hand": [ + -0.728, + -0.396 + ], + "to": [ + -3.005, + -2.96 + ], + "size": [ + 4.27, + 2.8 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "534:186", + "page": 534, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 75.6, + 195, + 7, + 3.6 + ], + "drawn": [ + 74.663, + 193.813, + 7, + 3.6 + ], + "settled": [ + 72.565, + 192.615, + 6.93, + 3.47 + ], + "hand": [ + -2.097, + -1.197 + ], + "to": [ + -3.035, + -2.385 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "536:191", + "page": 536, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 29.7, + 161.1, + 7, + 3.6 + ], + "drawn": [ + 28.83, + 159.413, + 7, + 3.6 + ], + "settled": [ + 26.635, + 158.885, + 7.07, + 3.47 + ], + "hand": [ + -2.195, + -0.528 + ], + "to": [ + -3.065, + -2.215 + ], + "size": [ + 7.07, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "554:326", + "page": 554, + "line": 10, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 20.8, + 333.8, + 5.3, + 6.6 + ], + "drawn": [ + 20.065, + 331.835, + 5.3, + 6.6 + ], + "settled": [ + 19.56, + 330.82, + 5.3, + 6.62 + ], + "hand": [ + -0.505, + -1.015 + ], + "to": [ + -1.24, + -2.98 + ], + "size": [ + 5.3, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "560:383", + "page": 560, + "line": 11, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 243.9, + 394.6, + 7.6, + 5.8 + ], + "drawn": [ + 241.098, + 392.479, + 7.6, + 5.8 + ], + "settled": [ + 240.99, + 392.31, + 7.6, + 5.6 + ], + "hand": [ + -0.108, + -0.169 + ], + "to": [ + -2.91, + -2.29 + ], + "size": [ + 7.6, + 5.6 + ], + "was": [ + 7.6, + 5.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "572:114", + "page": 572, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 177.6, + 157.8, + 7.8, + 3.7 + ], + "drawn": [ + 175.622, + 156.05, + 7.8, + 3.7 + ], + "settled": [ + 174.955, + 154.76, + 7.73, + 3.6 + ], + "hand": [ + -0.667, + -1.29 + ], + "to": [ + -2.645, + -3.04 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "576:23", + "page": 576, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 165.5, + 17.4, + 6.3, + 3.4 + ], + "drawn": [ + 163.577, + 14.582, + 6.3, + 3.4 + ], + "settled": [ + 163.575, + 14.385, + 6.13, + 3.33 + ], + "hand": [ + -0.002, + -0.197 + ], + "to": [ + -1.925, + -3.015 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "578:158", + "page": 578, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 327.9, + 159, + 7, + 3.6 + ], + "drawn": [ + 324.67, + 155.945, + 7, + 3.6 + ], + "settled": [ + 324.825, + 156.085, + 6.93, + 3.47 + ], + "hand": [ + 0.155, + 0.14 + ], + "to": [ + -3.075, + -2.915 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "580:203", + "page": 580, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 241.6, + 196.6, + 7, + 3.6 + ], + "drawn": [ + 238.389, + 194.348, + 7, + 3.6 + ], + "settled": [ + 238.685, + 194.345, + 6.93, + 3.47 + ], + "hand": [ + 0.296, + -0.003 + ], + "to": [ + -2.915, + -2.255 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:54", + "page": 588, + "line": 2, + "name": "maddah", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 197.5, + 49.6, + 6.6, + 3.1 + ], + "drawn": [ + 194.657, + 48.201, + 6.6, + 3.1 + ], + "settled": [ + 194.425, + 47.995, + 6.53, + 3.07 + ], + "hand": [ + -0.232, + -0.206 + ], + "to": [ + -3.075, + -1.605 + ], + "size": [ + 6.53, + 3.07 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "596:260", + "page": 596, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 134, + 296, + 7.8, + 3.7 + ], + "drawn": [ + 132.535, + 293.004, + 7.8, + 3.7 + ], + "settled": [ + 132.63, + 293.03, + 7.6, + 3.6 + ], + "hand": [ + 0.095, + 0.026 + ], + "to": [ + -1.37, + -2.97 + ], + "size": [ + 7.6, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "596:263", + "page": 596, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 110.2, + 298.7, + 7, + 3.6 + ], + "drawn": [ + 108.807, + 295.771, + 7, + 3.6 + ], + "settled": [ + 108.695, + 295.605, + 6.93, + 3.33 + ], + "hand": [ + -0.112, + -0.166 + ], + "to": [ + -1.505, + -3.095 + ], + "size": [ + 6.93, + 3.33 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + } + ] +} diff --git a/docs/validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json b/docs/validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json new file mode 100644 index 00000000..f0e05cbd --- /dev/null +++ b/docs/validation/rulings/2026-08-17-placement-weak-size-part1.seed23.settled.json @@ -0,0 +1,4492 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-19T19:08:14.453Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "5442d961", + "set": "weak", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-weak-p1of3-a8edff256.23.json", + "slice": "-p1of3-a8edff256", + "finished": "2026-08-19T19:05:06.640Z", + "shown": 90, + "seen": 90, + "looked": 90, + "whole": true, + "answers": 311, + "spoke": 90 + } + ], + "banked": [ + { + "file": "mark-answers.today.jsonl", + "statements": 105, + "added": 8, + "marks": 29, + "only": 0 + } + ], + "marks": 90, + "faulted": 90, + "defects": 15, + "answers": 319, + "settledMarks": [ + { + "id": "4:281", + "page": 4, + "line": 8, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 194.9, + 263.3, + 4, + 3.9 + ], + "drawn": [ + 193.761, + 261.857, + 4, + 3.9 + ], + "settled": [ + 193.255, + 262.125, + 4.53, + 3.87 + ], + "hand": [ + -0.506, + 0.268 + ], + "to": [ + -1.645, + -1.175 + ], + "size": [ + 4.53, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "30:465", + "page": 30, + "line": 11, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 171.7, + 376.6, + 6.6, + 5.3 + ], + "drawn": [ + 171.11, + 375.434, + 6.6, + 5.3 + ], + "settled": [ + 171.16, + 375.69, + 7.2, + 5.46 + ], + "hand": [ + 0.05, + 0.256 + ], + "to": [ + -0.54, + -0.91 + ], + "size": [ + 7.2, + 5.46 + ], + "was": [ + 6.6, + 5.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "54:420", + "page": 54, + "line": 11, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 93.6, + 386.9, + 4, + 3.9 + ], + "drawn": [ + 92.177, + 384.575, + 4, + 3.9 + ], + "settled": [ + 91.525, + 384.655, + 5.07, + 3.87 + ], + "hand": [ + -0.652, + 0.08 + ], + "to": [ + -2.075, + -2.245 + ], + "size": [ + 5.07, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "64:377", + "page": 64, + "line": 11, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 173.2, + 372.9, + 6.3, + 3.4 + ], + "drawn": [ + 170.177, + 370.655, + 6.3, + 3.4 + ], + "settled": [ + 172.84, + 371.435, + 6, + 4.93 + ], + "hand": [ + 2.663, + 0.78 + ], + "to": [ + -0.36, + -1.465 + ], + "size": [ + 6, + 4.93 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "70:508", + "page": 70, + "line": 14, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 162.2, + 481.9, + 8.2, + 6.4 + ], + "drawn": [ + 161.085, + 481.002, + 8.2, + 6.4 + ], + "settled": [ + 161.29, + 481.045, + 6.82, + 6.89 + ], + "hand": [ + 0.205, + 0.043 + ], + "to": [ + -0.91, + -0.855 + ], + "size": [ + 6.82, + 6.89 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "76:150", + "page": 76, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 212.8, + 160.5, + 8.5, + 3.9 + ], + "drawn": [ + 211.64, + 159.347, + 8.5, + 3.9 + ], + "settled": [ + 212.57, + 159.047, + 8.51, + 5.57 + ], + "hand": [ + 0.93, + -0.3 + ], + "to": [ + -0.23, + -1.453 + ], + "size": [ + 8.51, + 5.57 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 9, + "reshapes": 8, + "notes": [] + }, + { + "id": "76:154", + "page": 76, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 179.8, + 167.7, + 6.3, + 3.4 + ], + "drawn": [ + 178.554, + 166.495, + 6.3, + 3.4 + ], + "settled": [ + 179.11, + 166.315, + 6, + 4.53 + ], + "hand": [ + 0.556, + -0.18 + ], + "to": [ + -0.69, + -1.385 + ], + "size": [ + 6, + 4.53 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "77:391", + "page": 77, + "line": 11, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 326.3, + 368.9, + 6.2, + 3.3 + ], + "drawn": [ + 325.136, + 367.449, + 6.2, + 3.3 + ], + "settled": [ + 325.82, + 367.79, + 6, + 4.2 + ], + "hand": [ + 0.684, + 0.341 + ], + "to": [ + -0.48, + -1.11 + ], + "size": [ + 6, + 4.2 + ], + "was": [ + 6.2, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "86:342", + "page": 86, + "line": 9, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 157.6, + 299.4, + 4, + 3.9 + ], + "drawn": [ + 156.281, + 298.241, + 4, + 3.9 + ], + "settled": [ + 155.79, + 298.125, + 4.8, + 3.87 + ], + "hand": [ + -0.491, + -0.116 + ], + "to": [ + -1.81, + -1.275 + ], + "size": [ + 4.8, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "86:392", + "page": 86, + "line": 10, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 75.4, + 338.7, + 6.8, + 5.4 + ], + "drawn": [ + 74.679, + 337.717, + 6.8, + 5.4 + ], + "settled": [ + 74.595, + 337.64, + 6.79, + 6 + ], + "hand": [ + -0.084, + -0.077 + ], + "to": [ + -0.805, + -1.06 + ], + "size": [ + 6.79, + 6 + ], + "was": [ + 6.8, + 5.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "112:256", + "page": 112, + "line": 8, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 308.3, + 283.6, + 4, + 3.9 + ], + "drawn": [ + 306.944, + 282.079, + 4, + 3.9 + ], + "settled": [ + 306.725, + 282.125, + 4.53, + 3.87 + ], + "hand": [ + -0.219, + 0.046 + ], + "to": [ + -1.575, + -1.475 + ], + "size": [ + 4.53, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "115:470", + "page": 115, + "line": 14, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 64.4, + 476.4, + 8.2, + 6.4 + ], + "drawn": [ + 63.395, + 475.481, + 8.2, + 6.4 + ], + "settled": [ + 63.745, + 475.58, + 7.63, + 6.36 + ], + "hand": [ + 0.35, + 0.099 + ], + "to": [ + -0.655, + -0.82 + ], + "size": [ + 7.63, + 6.36 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "132:74", + "page": 132, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 74.1, + 70.5, + 6.3, + 3.4 + ], + "drawn": [ + 71.508, + 67.202, + 6.3, + 3.4 + ], + "settled": [ + 73.38, + 67.91, + 6, + 6.4 + ], + "hand": [ + 1.872, + 0.708 + ], + "to": [ + -0.72, + -2.59 + ], + "size": [ + 6, + 6.4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "138:226", + "page": 138, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 188.2, + 247.2, + 4, + 3.9 + ], + "drawn": [ + 186.563, + 245.684, + 4, + 3.9 + ], + "settled": [ + 185.835, + 245.195, + 7.11, + 3.87 + ], + "hand": [ + -0.728, + -0.489 + ], + "to": [ + -2.365, + -2.005 + ], + "size": [ + 7.11, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "143:10", + "page": 143, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 256.3, + 13.3, + 4, + 3.9 + ], + "drawn": [ + 255.161, + 11.648, + 4, + 3.9 + ], + "settled": [ + 254.73, + 10.935, + 4.8, + 3.87 + ], + "hand": [ + -0.431, + -0.713 + ], + "to": [ + -1.57, + -2.365 + ], + "size": [ + 4.8, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "154:252", + "page": 154, + "line": 7, + "name": "small yeh", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 316.6, + 241.9, + 6.4, + 4.7 + ], + "drawn": [ + 313.412, + 239.802, + 6.4, + 4.7 + ], + "settled": [ + 312.652, + 238.592, + 10.15, + 4.66 + ], + "hand": [ + -0.76, + -1.21 + ], + "to": [ + -3.948, + -3.308 + ], + "size": [ + 10.15, + 4.66 + ], + "was": [ + 6.4, + 4.7 + ], + "goes": 7, + "reshapes": 9, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "157:231", + "page": 157, + "line": 7, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 255.5, + 245.8, + 8.3, + 6.6 + ], + "drawn": [ + 254.359, + 244.48, + 8.3, + 6.6 + ], + "settled": [ + 254.645, + 244.31, + 7.07, + 8 + ], + "hand": [ + 0.286, + -0.17 + ], + "to": [ + -0.855, + -1.49 + ], + "size": [ + 7.07, + 8 + ], + "was": [ + 8.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "158:44", + "page": 158, + "line": 2, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 330.8, + 68.9, + 4, + 3.9 + ], + "drawn": [ + 327.662, + 67.193, + 4, + 3.9 + ], + "settled": [ + 328.255, + 67.265, + 6.27, + 3.73 + ], + "hand": [ + 0.593, + 0.072 + ], + "to": [ + -2.545, + -1.635 + ], + "size": [ + 6.27, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "168:82", + "page": 168, + "line": 3, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 158.6, + 106, + 7.1, + 5.5 + ], + "drawn": [ + 157.833, + 104.641, + 7.1, + 5.5 + ], + "settled": [ + 157.905, + 104.13, + 7.33, + 5.46 + ], + "hand": [ + 0.072, + -0.511 + ], + "to": [ + -0.695, + -1.87 + ], + "size": [ + 7.33, + 5.46 + ], + "was": [ + 7.1, + 5.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "174:88", + "page": 174, + "line": 3, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 309.8, + 107.1, + 4, + 3.9 + ], + "drawn": [ + 307.414, + 105.555, + 4, + 3.9 + ], + "settled": [ + 307.585, + 105.795, + 4.93, + 3.73 + ], + "hand": [ + 0.171, + 0.24 + ], + "to": [ + -2.215, + -1.305 + ], + "size": [ + 4.93, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "182:442", + "page": 182, + "line": 13, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 66.5, + 442.5, + 8.6, + 4 + ], + "drawn": [ + 65.116, + 441.225, + 8.6, + 4 + ], + "settled": [ + 65.43, + 441.135, + 7.34, + 3.87 + ], + "hand": [ + 0.314, + -0.09 + ], + "to": [ + -1.07, + -1.365 + ], + "size": [ + 7.34, + 3.87 + ], + "was": [ + 8.6, + 4 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "208:40", + "page": 208, + "line": 4, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 290.9, + 142.3, + 4, + 3.9 + ], + "drawn": [ + 289.284, + 140.079, + 4, + 3.9 + ], + "settled": [ + 288.83, + 140.065, + 4.84, + 3.73 + ], + "hand": [ + -0.454, + -0.014 + ], + "to": [ + -2.07, + -2.235 + ], + "size": [ + 4.84, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "216:209", + "page": 216, + "line": 7, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 199.8, + 250.3, + 6.3, + 3.4 + ], + "drawn": [ + 197.993, + 248.845, + 6.3, + 3.4 + ], + "settled": [ + 199.24, + 248.715, + 6, + 5.07 + ], + "hand": [ + 1.247, + -0.13 + ], + "to": [ + -0.56, + -1.585 + ], + "size": [ + 6, + 5.07 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "217:248", + "page": 217, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 306.3, + 235.7, + 6.3, + 3.4 + ], + "drawn": [ + 303.907, + 233.467, + 6.3, + 3.4 + ], + "settled": [ + 305.77, + 234.18, + 6, + 4.8 + ], + "hand": [ + 1.863, + 0.713 + ], + "to": [ + -0.53, + -1.52 + ], + "size": [ + 6, + 4.8 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "218:492", + "page": 218, + "line": 13, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 67.5, + 438.8, + 9.6, + 3.8 + ], + "drawn": [ + 66.497, + 437.659, + 9.6, + 3.8 + ], + "settled": [ + 66.46, + 437.405, + 8, + 3.67 + ], + "hand": [ + -0.037, + -0.254 + ], + "to": [ + -1.04, + -1.395 + ], + "size": [ + 8, + 3.67 + ], + "was": [ + 9.6, + 3.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "222:546", + "page": 222, + "line": 15, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 61.1, + 534.5, + 8.3, + 6.6 + ], + "drawn": [ + 60.726, + 533.833, + 8.3, + 6.6 + ], + "settled": [ + 60.715, + 533.705, + 7.73, + 6.81 + ], + "hand": [ + -0.011, + -0.128 + ], + "to": [ + -0.385, + -0.795 + ], + "size": [ + 7.73, + 6.81 + ], + "was": [ + 8.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "226:326", + "page": 226, + "line": 10, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 217.1, + 362.1, + 8, + 6.5 + ], + "drawn": [ + 215.992, + 360.229, + 8, + 6.5 + ], + "settled": [ + 216.26, + 360.555, + 7.34, + 7.59 + ], + "hand": [ + 0.268, + 0.326 + ], + "to": [ + -0.84, + -1.545 + ], + "size": [ + 7.34, + 7.59 + ], + "was": [ + 8, + 6.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "228:149", + "page": 228, + "line": 4, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 89.5, + 142, + 8.2, + 6.5 + ], + "drawn": [ + 88.179, + 140.679, + 8.2, + 6.5 + ], + "settled": [ + 88.36, + 140.835, + 7.74, + 7.07 + ], + "hand": [ + 0.181, + 0.156 + ], + "to": [ + -1.14, + -1.165 + ], + "size": [ + 7.74, + 7.07 + ], + "was": [ + 8.2, + 6.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "244:132", + "page": 244, + "line": 4, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 53.7, + 117.8, + 5.3, + 6.6 + ], + "drawn": [ + 51.732, + 115.458, + 5.3, + 6.6 + ], + "settled": [ + 49.287, + 113.006, + 5.35, + 6.7 + ], + "hand": [ + -2.445, + -2.452 + ], + "to": [ + -4.413, + -4.794 + ], + "size": [ + 5.35, + 6.7 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 5, + "reshapes": 3, + "notes": [] + }, + { + "id": "255:174", + "page": 255, + "line": 8, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 200.3, + 287.8, + 9.5, + 6.9 + ], + "drawn": [ + 198.791, + 286.727, + 9.5, + 6.9 + ], + "settled": [ + 199.045, + 286.76, + 10.67, + 6.8 + ], + "hand": [ + 0.254, + 0.033 + ], + "to": [ + -1.255, + -1.04 + ], + "size": [ + 10.67, + 6.8 + ], + "was": [ + 9.5, + 6.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "260:248", + "page": 260, + "line": 7, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 63.2, + 228.5, + 8.9, + 4 + ], + "drawn": [ + 61.078, + 224.721, + 8.9, + 4 + ], + "settled": [ + 61.745, + 226.075, + 11.47, + 7.07 + ], + "hand": [ + 0.667, + 1.354 + ], + "to": [ + -1.455, + -2.425 + ], + "size": [ + 11.47, + 7.07 + ], + "was": [ + 8.9, + 4 + ], + "goes": 2, + "reshapes": 2, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "260:262", + "page": 260, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 283.8, + 260.9, + 6.3, + 3.4 + ], + "drawn": [ + 281.808, + 257.958, + 6.3, + 3.4 + ], + "settled": [ + 282.57, + 258.775, + 6, + 4.93 + ], + "hand": [ + 0.762, + 0.817 + ], + "to": [ + -1.23, + -2.125 + ], + "size": [ + 6, + 4.93 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "264:204", + "page": 264, + "line": 6, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 174.9, + 193.7, + 4, + 3.9 + ], + "drawn": [ + 173.889, + 192.16, + 4, + 3.9 + ], + "settled": [ + 173.255, + 192.055, + 4.53, + 3.73 + ], + "hand": [ + -0.634, + -0.105 + ], + "to": [ + -1.645, + -1.645 + ], + "size": [ + 4.53, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "272:238", + "page": 272, + "line": 6, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 37, + 215.1, + 7.5, + 6 + ], + "drawn": [ + 36.136, + 213.963, + 7.5, + 6 + ], + "settled": [ + 36.18, + 213.745, + 8, + 5.73 + ], + "hand": [ + 0.044, + -0.218 + ], + "to": [ + -0.82, + -1.355 + ], + "size": [ + 8, + 5.73 + ], + "was": [ + 7.5, + 6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "274:59", + "page": 274, + "line": 2, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 258.4, + 67.1, + 6.9, + 5.4 + ], + "drawn": [ + 257.533, + 65.827, + 6.9, + 5.4 + ], + "settled": [ + 257.94, + 66.31, + 6.94, + 5.2 + ], + "hand": [ + 0.407, + 0.483 + ], + "to": [ + -0.46, + -0.79 + ], + "size": [ + 6.94, + 5.2 + ], + "was": [ + 6.9, + 5.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "274:119", + "page": 274, + "line": 3, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 116.9, + 88.8, + 9.6, + 3.7 + ], + "drawn": [ + 116.045, + 87.115, + 9.6, + 3.7 + ], + "settled": [ + 116.525, + 87.295, + 9.47, + 4.93 + ], + "hand": [ + 0.48, + 0.18 + ], + "to": [ + -0.375, + -1.505 + ], + "size": [ + 9.47, + 4.93 + ], + "was": [ + 9.6, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "285:24", + "page": 285, + "line": 1, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 158, + 33.7, + 7.3, + 5.8 + ], + "drawn": [ + 157.33, + 32.776, + 7.3, + 5.8 + ], + "settled": [ + 157.28, + 32.45, + 6.8, + 6.4 + ], + "hand": [ + -0.05, + -0.326 + ], + "to": [ + -0.72, + -1.25 + ], + "size": [ + 6.8, + 6.4 + ], + "was": [ + 7.3, + 5.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "286:284", + "page": 286, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 131, + 248.4, + 4, + 3.9 + ], + "drawn": [ + 129.984, + 246.634, + 4, + 3.9 + ], + "settled": [ + 129.465, + 246.325, + 4.67, + 3.73 + ], + "hand": [ + -0.519, + -0.309 + ], + "to": [ + -1.535, + -2.075 + ], + "size": [ + 4.67, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "291:266", + "page": 291, + "line": 8, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 169.1, + 265, + 6.6, + 5.6 + ], + "drawn": [ + 168.133, + 263.592, + 6.6, + 5.6 + ], + "settled": [ + 168.025, + 263.145, + 6.53, + 5.47 + ], + "hand": [ + -0.108, + -0.447 + ], + "to": [ + -1.075, + -1.855 + ], + "size": [ + 6.53, + 5.47 + ], + "was": [ + 6.6, + 5.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "302:367", + "page": 302, + "line": 10, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 49.8, + 339.5, + 7.8, + 3.5 + ], + "drawn": [ + 49.476, + 338.601, + 7.8, + 3.5 + ], + "settled": [ + 49.295, + 338.525, + 8.39, + 4.01 + ], + "hand": [ + -0.181, + -0.076 + ], + "to": [ + -0.505, + -0.975 + ], + "size": [ + 8.39, + 4.01 + ], + "was": [ + 7.8, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "333:319", + "page": 333, + "line": 8, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 15.7, + 283.1, + 7, + 3.6 + ], + "drawn": [ + 14.145, + 281.906, + 7, + 3.6 + ], + "settled": [ + 14.365, + 281.34, + 8.67, + 3.6 + ], + "hand": [ + 0.22, + -0.566 + ], + "to": [ + -1.335, + -1.76 + ], + "size": [ + 8.67, + 3.6 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "363:234", + "page": 363, + "line": 7, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 141.7, + 231.9, + 6.8, + 5.8 + ], + "drawn": [ + 141.173, + 230.952, + 6.8, + 5.8 + ], + "settled": [ + 141.065, + 230.645, + 7.21, + 6.53 + ], + "hand": [ + -0.108, + -0.307 + ], + "to": [ + -0.635, + -1.255 + ], + "size": [ + 7.21, + 6.53 + ], + "was": [ + 6.8, + 5.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "372:74", + "page": 372, + "line": 2, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 9.4, + 53.9, + 8.2, + 6.4 + ], + "drawn": [ + 8.918, + 52.883, + 8.2, + 6.4 + ], + "settled": [ + 8.055, + 52.315, + 7.01, + 6.47 + ], + "hand": [ + -0.863, + -0.568 + ], + "to": [ + -1.345, + -1.585 + ], + "size": [ + 7.01, + 6.47 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "374:17", + "page": 374, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 152.3, + 34.6, + 4, + 3.9 + ], + "drawn": [ + 151.345, + 32.537, + 4, + 3.9 + ], + "settled": [ + 150.66, + 32.465, + 5.2, + 3.73 + ], + "hand": [ + -0.685, + -0.072 + ], + "to": [ + -1.64, + -2.135 + ], + "size": [ + 5.2, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "384:87", + "page": 384, + "line": 3, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 241.5, + 102.5, + 4, + 3.9 + ], + "drawn": [ + 239.829, + 101.162, + 4, + 3.9 + ], + "settled": [ + 239.17, + 101.195, + 5.24, + 3.87 + ], + "hand": [ + -0.659, + 0.033 + ], + "to": [ + -2.33, + -1.305 + ], + "size": [ + 5.24, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "388:335", + "page": 388, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 144.3, + 346.6, + 4, + 3.9 + ], + "drawn": [ + 142.634, + 345.08, + 4, + 3.9 + ], + "settled": [ + 142.359, + 345.201, + 5.41, + 3.85 + ], + "hand": [ + -0.275, + 0.121 + ], + "to": [ + -1.941, + -1.399 + ], + "size": [ + 5.41, + 3.85 + ], + "was": [ + 4, + 3.9 + ], + "goes": 9, + "reshapes": 6, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "389:229", + "page": 389, + "line": 7, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 227.8, + 232.4, + 9.4, + 4 + ], + "drawn": [ + 227.272, + 231.54, + 9.4, + 4 + ], + "settled": [ + 227.16, + 231.275, + 10, + 4.67 + ], + "hand": [ + -0.112, + -0.265 + ], + "to": [ + -0.64, + -1.125 + ], + "size": [ + 10, + 4.67 + ], + "was": [ + 9.4, + 4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "390:376", + "page": 390, + "line": 11, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 152.2, + 392.1, + 4, + 3.9 + ], + "drawn": [ + 151.192, + 390.757, + 4, + 3.9 + ], + "settled": [ + 150.73, + 390.855, + 4.8, + 3.73 + ], + "hand": [ + -0.462, + 0.098 + ], + "to": [ + -1.47, + -1.245 + ], + "size": [ + 4.8, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "406:105", + "page": 406, + "line": 3, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 33.6, + 85.8, + 4, + 3.9 + ], + "drawn": [ + 31.871, + 83.284, + 4, + 3.9 + ], + "settled": [ + 30.735, + 82.995, + 5.07, + 3.73 + ], + "hand": [ + -1.136, + -0.289 + ], + "to": [ + -2.865, + -2.805 + ], + "size": [ + 5.07, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "414:247", + "page": 414, + "line": 7, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 9.3, + 230, + 8.2, + 6.4 + ], + "drawn": [ + 8.704, + 228.542, + 8.2, + 6.4 + ], + "settled": [ + 8.19, + 228.52, + 7.28, + 6.36 + ], + "hand": [ + -0.514, + -0.022 + ], + "to": [ + -1.11, + -1.48 + ], + "size": [ + 7.28, + 6.36 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 8, + "reshapes": 8, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "424:364", + "page": 424, + "line": 10, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 244.9, + 337.2, + 6.3, + 3.4 + ], + "drawn": [ + 243.72, + 335.712, + 6.3, + 3.4 + ], + "settled": [ + 244.575, + 335.505, + 6.27, + 5.07 + ], + "hand": [ + 0.855, + -0.207 + ], + "to": [ + -0.325, + -1.695 + ], + "size": [ + 6.27, + 5.07 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "426:186", + "page": 426, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 313, + 161.9, + 8.5, + 3.9 + ], + "drawn": [ + 311.848, + 159.231, + 8.5, + 3.9 + ], + "settled": [ + 312.54, + 159.795, + 8.4, + 6.67 + ], + "hand": [ + 0.692, + 0.564 + ], + "to": [ + -0.46, + -2.105 + ], + "size": [ + 8.4, + 6.67 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "433:460", + "page": 433, + "line": 13, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 306.2, + 463.2, + 4.9, + 4.9 + ], + "drawn": [ + 305.608, + 461.332, + 4.9, + 4.9 + ], + "settled": [ + 306.135, + 461.085, + 5.33, + 4.67 + ], + "hand": [ + 0.527, + -0.247 + ], + "to": [ + -0.065, + -2.115 + ], + "size": [ + 5.33, + 4.67 + ], + "was": [ + 4.9, + 4.9 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "436:365", + "page": 436, + "line": 11, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 71.7, + 395.1, + 8.5, + 6.7 + ], + "drawn": [ + 69.796, + 393.154, + 8.5, + 6.7 + ], + "settled": [ + 70.61, + 393.455, + 7.2, + 7.73 + ], + "hand": [ + 0.814, + 0.301 + ], + "to": [ + -1.09, + -1.645 + ], + "size": [ + 7.2, + 7.73 + ], + "was": [ + 8.5, + 6.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "445:296", + "page": 445, + "line": 10, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 138, + 356.7, + 9.2, + 7 + ], + "drawn": [ + 137.881, + 355.991, + 9.2, + 7 + ], + "settled": [ + 138.065, + 355.74, + 8.93, + 7.54 + ], + "hand": [ + 0.184, + -0.251 + ], + "to": [ + 0.065, + -0.96 + ], + "size": [ + 8.93, + 7.54 + ], + "was": [ + 9.2, + 7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "452:96", + "page": 452, + "line": 3, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 118.5, + 107.2, + 4, + 3.9 + ], + "drawn": [ + 117.641, + 105.51, + 4, + 3.9 + ], + "settled": [ + 117.035, + 105.335, + 4.97, + 3.87 + ], + "hand": [ + -0.606, + -0.175 + ], + "to": [ + -1.465, + -1.865 + ], + "size": [ + 4.97, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "456:445", + "page": 456, + "line": 11, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 10.6, + 375.6, + 8.2, + 6.4 + ], + "drawn": [ + 10.424, + 374.399, + 8.2, + 6.4 + ], + "settled": [ + 9.985, + 374.305, + 7.05, + 6.47 + ], + "hand": [ + -0.439, + -0.094 + ], + "to": [ + -0.615, + -1.295 + ], + "size": [ + 7.05, + 6.47 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "465:238", + "page": 465, + "line": 7, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 83.9, + 248.3, + 8.1, + 6.6 + ], + "drawn": [ + 82.998, + 246.806, + 8.1, + 6.6 + ], + "settled": [ + 83.145, + 246.715, + 8.13, + 5.87 + ], + "hand": [ + 0.147, + -0.091 + ], + "to": [ + -0.755, + -1.585 + ], + "size": [ + 8.13, + 5.87 + ], + "was": [ + 8.1, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "466:58", + "page": 466, + "line": 2, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 137.6, + 69.4, + 4, + 3.9 + ], + "drawn": [ + 135.934, + 67.543, + 4, + 3.9 + ], + "settled": [ + 134.995, + 67.595, + 4.53, + 3.87 + ], + "hand": [ + -0.939, + 0.052 + ], + "to": [ + -2.605, + -1.805 + ], + "size": [ + 4.53, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "474:414", + "page": 474, + "line": 13, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 142.3, + 447.2, + 8.5, + 3.9 + ], + "drawn": [ + 140.938, + 446.017, + 8.5, + 3.9 + ], + "settled": [ + 141.88, + 445.725, + 8.4, + 5.47 + ], + "hand": [ + 0.942, + -0.292 + ], + "to": [ + -0.42, + -1.475 + ], + "size": [ + 8.4, + 5.47 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "483:372", + "page": 483, + "line": 12, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 100.8, + 432, + 8, + 6 + ], + "drawn": [ + 98.492, + 431.151, + 8, + 6 + ], + "settled": [ + 98.795, + 431.135, + 4.93, + 6.13 + ], + "hand": [ + 0.303, + -0.016 + ], + "to": [ + -2.005, + -0.865 + ], + "size": [ + 4.93, + 6.13 + ], + "was": [ + 8, + 6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "486:491", + "page": 486, + "line": 13, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 247, + 460, + 4, + 3.9 + ], + "drawn": [ + 245.814, + 459.163, + 4, + 3.9 + ], + "settled": [ + 244.925, + 459.115, + 4.67, + 3.73 + ], + "hand": [ + -0.889, + -0.048 + ], + "to": [ + -2.075, + -0.885 + ], + "size": [ + 4.67, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "490:210", + "page": 490, + "line": 6, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 166.5, + 213.1, + 6.3, + 3.4 + ], + "drawn": [ + 163.368, + 211.63, + 6.3, + 3.4 + ], + "settled": [ + 165.78, + 211.845, + 6, + 4.13 + ], + "hand": [ + 2.412, + 0.215 + ], + "to": [ + -0.72, + -1.255 + ], + "size": [ + 6, + 4.13 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "491:522", + "page": 491, + "line": 14, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 218.4, + 477.7, + 9.3, + 4 + ], + "drawn": [ + 217.375, + 476.312, + 9.3, + 4 + ], + "settled": [ + 217.675, + 476.54, + 9.87, + 4.8 + ], + "hand": [ + 0.3, + 0.228 + ], + "to": [ + -0.725, + -1.16 + ], + "size": [ + 9.87, + 4.8 + ], + "was": [ + 9.3, + 4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "492:216", + "page": 492, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 24.9, + 204.2, + 5.1, + 3 + ], + "drawn": [ + 24.15, + 203.2, + 5.1, + 3 + ], + "settled": [ + 24.58, + 202.905, + 4.8, + 3.73 + ], + "hand": [ + 0.43, + -0.295 + ], + "to": [ + -0.32, + -1.295 + ], + "size": [ + 4.8, + 3.73 + ], + "was": [ + 5.1, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "494:422", + "page": 494, + "line": 12, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 11.8, + 431.9, + 8.6, + 6.7 + ], + "drawn": [ + 10.641, + 431.145, + 8.6, + 6.7 + ], + "settled": [ + 10.765, + 430.925, + 7.33, + 6.93 + ], + "hand": [ + 0.124, + -0.22 + ], + "to": [ + -1.035, + -0.975 + ], + "size": [ + 7.33, + 6.93 + ], + "was": [ + 8.6, + 6.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "499:134", + "page": 499, + "line": 5, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 5, + 181.4, + 7.5, + 5.9 + ], + "drawn": [ + 4.915, + 181.155, + 7.5, + 5.9 + ], + "settled": [ + 4.65, + 179.46, + 6, + 7.46 + ], + "hand": [ + -0.265, + -1.695 + ], + "to": [ + -0.35, + -1.94 + ], + "size": [ + 6, + 7.46 + ], + "was": [ + 7.5, + 5.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "504:48", + "page": 504, + "line": 2, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 309.1, + 54.8, + 9.5, + 4 + ], + "drawn": [ + 307.318, + 53.093, + 9.5, + 4 + ], + "settled": [ + 307.84, + 52.95, + 8, + 4 + ], + "hand": [ + 0.522, + -0.143 + ], + "to": [ + -1.26, + -1.85 + ], + "size": [ + 8, + 4 + ], + "was": [ + 9.5, + 4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "508:599", + "page": 508, + "line": 14, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 259.2, + 482.5, + 6.3, + 3.4 + ], + "drawn": [ + 258.207, + 480.972, + 6.3, + 3.4 + ], + "settled": [ + 259.24, + 481.04, + 6, + 4.4 + ], + "hand": [ + 1.033, + 0.068 + ], + "to": [ + 0.04, + -1.46 + ], + "size": [ + 6, + 4.4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "512:418", + "page": 512, + "line": 12, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 241, + 412.5, + 8.4, + 3.5 + ], + "drawn": [ + 240.134, + 411.312, + 8.4, + 3.5 + ], + "settled": [ + 240.255, + 411.125, + 7.73, + 3.33 + ], + "hand": [ + 0.121, + -0.187 + ], + "to": [ + -0.745, + -1.375 + ], + "size": [ + 7.73, + 3.33 + ], + "was": [ + 8.4, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "518:316", + "page": 518, + "line": 11, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 137.8, + 393.2, + 8.9, + 7 + ], + "drawn": [ + 137.16, + 391.987, + 8.9, + 7 + ], + "settled": [ + 137.075, + 391.64, + 9.07, + 6.8 + ], + "hand": [ + -0.085, + -0.347 + ], + "to": [ + -0.725, + -1.56 + ], + "size": [ + 9.07, + 6.8 + ], + "was": [ + 8.9, + 7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "518:350", + "page": 518, + "line": 12, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 201.4, + 410.2, + 8, + 3.9 + ], + "drawn": [ + 200.234, + 408.835, + 8, + 3.9 + ], + "settled": [ + 200.06, + 408.785, + 9.86, + 4.93 + ], + "hand": [ + -0.174, + -0.05 + ], + "to": [ + -1.34, + -1.415 + ], + "size": [ + 9.86, + 4.93 + ], + "was": [ + 8, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "523:402", + "page": 523, + "line": 14, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 215.8, + 498.2, + 8.5, + 6.3 + ], + "drawn": [ + 215.228, + 497.325, + 8.5, + 6.3 + ], + "settled": [ + 214.745, + 497.115, + 8.53, + 6.13 + ], + "hand": [ + -0.483, + -0.21 + ], + "to": [ + -1.055, + -1.085 + ], + "size": [ + 8.53, + 6.13 + ], + "was": [ + 8.5, + 6.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "526:318", + "page": 526, + "line": 11, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "print-defect" + ], + "fault": true, + "box": [ + 329.8, + 370.7, + 5.6, + 3.1 + ], + "drawn": [ + 327.484, + 368.057, + 5.6, + 3.1 + ], + "settled": [ + 327.054, + 368.357, + 5.6, + 3.1 + ], + "hand": [ + -0.43, + 0.3 + ], + "to": [ + -2.746, + -2.343 + ], + "size": null, + "was": null, + "goes": 4, + "reshapes": 0, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "530:341", + "page": 530, + "line": 9, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 72.9, + 322.5, + 6.3, + 3.4 + ], + "drawn": [ + 70.479, + 319.958, + 6.3, + 3.4 + ], + "settled": [ + 72.45, + 320.575, + 6, + 5.33 + ], + "hand": [ + 1.971, + 0.617 + ], + "to": [ + -0.45, + -1.925 + ], + "size": [ + 6, + 5.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "533:255", + "page": 533, + "line": 7, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 171.8, + 250.5, + 7.7, + 5.9 + ], + "drawn": [ + 170.935, + 249.069, + 7.7, + 5.9 + ], + "settled": [ + 171.01, + 249.46, + 7.2, + 6.4 + ], + "hand": [ + 0.075, + 0.391 + ], + "to": [ + -0.79, + -1.04 + ], + "size": [ + 7.2, + 6.4 + ], + "was": [ + 7.7, + 5.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "535:193", + "page": 535, + "line": 6, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 193, + 214.8, + 8.8, + 6.8 + ], + "drawn": [ + 192.469, + 213.934, + 8.8, + 6.8 + ], + "settled": [ + 192.26, + 213.875, + 8.14, + 7.33 + ], + "hand": [ + -0.209, + -0.059 + ], + "to": [ + -0.74, + -0.925 + ], + "size": [ + 8.14, + 7.33 + ], + "was": [ + 8.8, + 6.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "535:206", + "page": 535, + "line": 6, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 53.8, + 213.3, + 8.4, + 6.1 + ], + "drawn": [ + 53.172, + 212.304, + 8.4, + 6.1 + ], + "settled": [ + 53.465, + 212.625, + 8.27, + 5.47 + ], + "hand": [ + 0.293, + 0.321 + ], + "to": [ + -0.335, + -0.675 + ], + "size": [ + 8.27, + 5.47 + ], + "was": [ + 8.4, + 6.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "544:220", + "page": 544, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 28.9, + 154.3, + 6.3, + 3.4 + ], + "drawn": [ + 27.977, + 153.198, + 6.3, + 3.4 + ], + "settled": [ + 28.585, + 152.315, + 6.27, + 5.87 + ], + "hand": [ + 0.608, + -0.883 + ], + "to": [ + -0.315, + -1.985 + ], + "size": [ + 6.27, + 5.87 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "556:5", + "page": 556, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 296.2, + 105, + 4.9, + 2.6 + ], + "drawn": [ + 296.219, + 104.33, + 4.9, + 2.6 + ], + "settled": [ + 296.345, + 104.175, + 4.67, + 2.93 + ], + "hand": [ + 0.126, + -0.155 + ], + "to": [ + 0.145, + -0.825 + ], + "size": [ + 4.67, + 2.93 + ], + "was": [ + 4.9, + 2.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "562:72", + "page": 562, + "line": 4, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 125.1, + 123.1, + 10.1, + 4 + ], + "drawn": [ + 123.141, + 122.09, + 10.1, + 4 + ], + "settled": [ + 123.81, + 122.15, + 11.74, + 4.54 + ], + "hand": [ + 0.669, + 0.06 + ], + "to": [ + -1.29, + -0.95 + ], + "size": [ + 11.74, + 4.54 + ], + "was": [ + 10.1, + 4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "572:91", + "page": 572, + "line": 5, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 321.7, + 155.6, + 4, + 3.9 + ], + "drawn": [ + 319.513, + 154.765, + 4, + 3.9 + ], + "settled": [ + 319.035, + 154.995, + 6.31, + 3.73 + ], + "hand": [ + -0.478, + 0.23 + ], + "to": [ + -2.665, + -0.605 + ], + "size": [ + 6.31, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "578:260", + "page": 578, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 167.8, + 223.9, + 6.3, + 3.4 + ], + "drawn": [ + 165.154, + 221.112, + 6.3, + 3.4 + ], + "settled": [ + 167.104, + 221.912, + 6.18, + 5.4 + ], + "hand": [ + 1.95, + 0.8 + ], + "to": [ + -0.696, + -1.988 + ], + "size": [ + 6.18, + 5.4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 5, + "reshapes": 7, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "579:283", + "page": 579, + "line": 8, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 237.2, + 283.1, + 7.3, + 5.9 + ], + "drawn": [ + 236.209, + 281.915, + 7.3, + 5.9 + ], + "settled": [ + 236.345, + 281.265, + 7.87, + 5.47 + ], + "hand": [ + 0.136, + -0.65 + ], + "to": [ + -0.855, + -1.835 + ], + "size": [ + 7.87, + 5.47 + ], + "was": [ + 7.3, + 5.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "582:206", + "page": 582, + "line": 8, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 75, + 264.8, + 8.3, + 3.6 + ], + "drawn": [ + 74.048, + 263.28, + 8.3, + 3.6 + ], + "settled": [ + 74.25, + 263.675, + 8.4, + 4.27 + ], + "hand": [ + 0.202, + 0.395 + ], + "to": [ + -0.75, + -1.125 + ], + "size": [ + 8.4, + 4.27 + ], + "was": [ + 8.3, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "584:304", + "page": 584, + "line": 9, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 326.9, + 319.7, + 4, + 3.9 + ], + "drawn": [ + 324.623, + 318.461, + 4, + 3.9 + ], + "settled": [ + 324.455, + 318.395, + 5.87, + 3.87 + ], + "hand": [ + -0.168, + -0.066 + ], + "to": [ + -2.445, + -1.305 + ], + "size": [ + 5.87, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "588:51", + "page": 588, + "line": 2, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 239.8, + 72.5, + 8.2, + 6.7 + ], + "drawn": [ + 236.902, + 71.139, + 8.2, + 6.7 + ], + "settled": [ + 237.825, + 71.195, + 5.61, + 7.47 + ], + "hand": [ + 0.923, + 0.056 + ], + "to": [ + -1.975, + -1.305 + ], + "size": [ + 5.61, + 7.47 + ], + "was": [ + 8.2, + 6.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:56", + "page": 588, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 190.3, + 44.9, + 6.2, + 3.4 + ], + "drawn": [ + 187.466, + 43.494, + 6.2, + 3.4 + ], + "settled": [ + 189.916, + 43.734, + 6.46, + 4.63 + ], + "hand": [ + 2.45, + 0.24 + ], + "to": [ + -0.384, + -1.166 + ], + "size": [ + 6.46, + 4.63 + ], + "was": [ + 6.2, + 3.4 + ], + "goes": 9, + "reshapes": 7, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "592:196", + "page": 592, + "line": 8, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 186.1, + 262.8, + 9.6, + 3.7 + ], + "drawn": [ + 185.14, + 261.234, + 9.6, + 3.7 + ], + "settled": [ + 185.46, + 261.165, + 8.66, + 3.87 + ], + "hand": [ + 0.32, + -0.069 + ], + "to": [ + -0.64, + -1.635 + ], + "size": [ + 8.66, + 3.87 + ], + "was": [ + 9.6, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "596:223", + "page": 596, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 126.3, + 267.6, + 6.3, + 3.4 + ], + "drawn": [ + 124.448, + 265.162, + 6.3, + 3.4 + ], + "settled": [ + 126.045, + 265.18, + 6.27, + 6.8 + ], + "hand": [ + 1.597, + 0.018 + ], + "to": [ + -0.255, + -2.42 + ], + "size": [ + 6.27, + 6.8 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + } + ] +} diff --git a/docs/validation/rulings/2026-08-17-placement-what-kind-of-wrong-placed.seed23.settled.json b/docs/validation/rulings/2026-08-17-placement-what-kind-of-wrong-placed.seed23.settled.json new file mode 100644 index 00000000..cf1ac5f2 --- /dev/null +++ b/docs/validation/rulings/2026-08-17-placement-what-kind-of-wrong-placed.seed23.settled.json @@ -0,0 +1,2987 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-17T16:34:31.583Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "1870ee91", + "set": "placed", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-placed-b0.55_0.65-a06e4a186.23.json", + "slice": "-b0.55_0.65-a06e4a186", + "finished": "2026-08-17T16:18:20.787Z", + "shown": 30, + "seen": 30, + "looked": 30, + "whole": true, + "answers": 74, + "spoke": 30 + }, + { + "file": "mark-report-placed-b0.65_0.75-a06e4a186.23.json", + "slice": "-b0.65_0.75-a06e4a186", + "finished": "2026-08-17T16:32:25.610Z", + "shown": 30, + "seen": 30, + "looked": 30, + "whole": true, + "answers": 72, + "spoke": 30 + } + ], + "banked": [], + "marks": 60, + "faulted": 60, + "defects": 2, + "answers": 146, + "settledMarks": [ + { + "id": "13:8", + "page": 13, + "line": 1, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 279.7, + 15.7, + 6.3, + 3.4 + ], + "drawn": [ + 279.138, + 14.825, + 6.3, + 3.4 + ], + "settled": [ + 279.11, + 14.65, + 6, + 3.6 + ], + "hand": [ + -0.027, + -0.175 + ], + "to": [ + -0.59, + -1.05 + ], + "size": [ + 6, + 3.6 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "24:118", + "page": 24, + "line": 4, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 300.2, + 141.7, + 4, + 3.9 + ], + "drawn": [ + 300.138, + 139.575, + 4, + 3.9 + ], + "settled": [ + 298.79, + 139.525, + 4.4, + 3.73 + ], + "hand": [ + -1.347, + -0.05 + ], + "to": [ + -1.41, + -2.175 + ], + "size": [ + 4.4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "56:349", + "page": 56, + "line": 10, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 248.9, + 339.8, + 5.6, + 3.1 + ], + "drawn": [ + 248.65, + 339.113, + 5.6, + 3.1 + ], + "settled": [ + 248.655, + 338.825, + 5.73, + 3.67 + ], + "hand": [ + 0.005, + -0.287 + ], + "to": [ + -0.245, + -0.975 + ], + "size": [ + 5.73, + 3.67 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "60:55", + "page": 60, + "line": 2, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 138.2, + 53.9, + 7, + 3.6 + ], + "drawn": [ + 135.013, + 51.213, + 7, + 3.6 + ], + "settled": [ + 133.095, + 51.945, + 6.93, + 3.47 + ], + "hand": [ + -1.918, + 0.732 + ], + "to": [ + -5.105, + -1.955 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "60:81", + "page": 60, + "line": 3, + "name": "wasla", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 190, + 84.6, + 5.3, + 4.3 + ], + "drawn": [ + 186.75, + 82.6, + 5.3, + 4.3 + ], + "settled": [ + 186.4, + 82.6, + 5.45, + 4.4 + ], + "hand": [ + -0.35, + 0 + ], + "to": [ + -3.6, + -2 + ], + "size": [ + 5.45, + 4.4 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 4, + "reshapes": 4, + "notes": [] + }, + { + "id": "86:23", + "page": 86, + "line": 1, + "name": "superscript alef", + "rule": "ink", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 201, + 24.6, + 2.1, + 6.1 + ], + "drawn": [ + 200.438, + 23.538, + 2.1, + 6.1 + ], + "settled": [ + 200.618, + 23.948, + 2.28, + 6.1 + ], + "hand": [ + 0.181, + 0.411 + ], + "to": [ + -0.382, + -0.652 + ], + "size": [ + 2.28, + 6.1 + ], + "was": [ + 2.1, + 6.1 + ], + "goes": 10, + "reshapes": 2, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "97:421", + "page": 97, + "line": 13, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 266.3, + 465, + 7, + 3.6 + ], + "drawn": [ + 265.925, + 464, + 7, + 3.6 + ], + "settled": [ + 265.965, + 463.535, + 7.07, + 4.53 + ], + "hand": [ + 0.04, + -0.465 + ], + "to": [ + -0.335, + -1.465 + ], + "size": [ + 7.07, + 4.53 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "107:515", + "page": 107, + "line": 13, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 276.2, + 443.1, + 4, + 3.9 + ], + "drawn": [ + 275.888, + 441.85, + 4, + 3.9 + ], + "settled": [ + 275.39, + 441.855, + 4, + 3.87 + ], + "hand": [ + -0.498, + 0.005 + ], + "to": [ + -0.81, + -1.245 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "132:87", + "page": 132, + "line": 3, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 319.7, + 106.6, + 5.6, + 3.1 + ], + "drawn": [ + 316.45, + 103.35, + 5.6, + 3.1 + ], + "settled": [ + 316.925, + 102.995, + 5.47, + 2.93 + ], + "hand": [ + 0.475, + -0.355 + ], + "to": [ + -2.775, + -3.605 + ], + "size": [ + 5.47, + 2.93 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "141:165", + "page": 141, + "line": 5, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 107.7, + 180, + 6.3, + 3.4 + ], + "drawn": [ + 107.2, + 179.063, + 6.3, + 3.4 + ], + "settled": [ + 107.375, + 178.31, + 6.53, + 4.8 + ], + "hand": [ + 0.175, + -0.752 + ], + "to": [ + -0.325, + -1.69 + ], + "size": [ + 6.53, + 4.8 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "149:368", + "page": 149, + "line": 11, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 311, + 392.8, + 4, + 3.9 + ], + "drawn": [ + 310.563, + 391.8, + 4, + 3.9 + ], + "settled": [ + 309.235, + 391.785, + 4.31, + 3.73 + ], + "hand": [ + -1.327, + -0.015 + ], + "to": [ + -1.765, + -1.015 + ], + "size": [ + 4.31, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "158:63", + "page": 158, + "line": 2, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 167, + 52.6, + 4, + 3.9 + ], + "drawn": [ + 163.75, + 50.725, + 4, + 3.9 + ], + "settled": [ + 163.39, + 50.795, + 4, + 3.87 + ], + "hand": [ + -0.36, + 0.07 + ], + "to": [ + -3.61, + -1.805 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "168:241", + "page": 168, + "line": 7, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 62.4, + 234.6, + 5.6, + 3.1 + ], + "drawn": [ + 62.025, + 233.85, + 5.6, + 3.1 + ], + "settled": [ + 61.865, + 233.59, + 5.47, + 3.6 + ], + "hand": [ + -0.16, + -0.26 + ], + "to": [ + -0.535, + -1.01 + ], + "size": [ + 5.47, + 3.6 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "175:209", + "page": 175, + "line": 6, + "name": "successive fathatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 159.7, + 196.5, + 9.8, + 4.2 + ], + "drawn": [ + 158.763, + 195.625, + 9.8, + 4.2 + ], + "settled": [ + 158.96, + 195.575, + 10.26, + 4.39 + ], + "hand": [ + 0.198, + -0.05 + ], + "to": [ + -0.74, + -0.925 + ], + "size": [ + 10.26, + 4.39 + ], + "was": [ + 9.8, + 4.2 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "205:251", + "page": 205, + "line": 8, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 138.3, + 285.1, + 6.3, + 3.4 + ], + "drawn": [ + 137.8, + 284.163, + 6.3, + 3.4 + ], + "settled": [ + 137.785, + 283.98, + 6.27, + 3.6 + ], + "hand": [ + -0.015, + -0.183 + ], + "to": [ + -0.515, + -1.12 + ], + "size": [ + 6.27, + 3.6 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "207:401", + "page": 207, + "line": 14, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 167.6, + 484, + 5.6, + 3.1 + ], + "drawn": [ + 167.288, + 483.188, + 5.6, + 3.1 + ], + "settled": [ + 167.255, + 482.92, + 5.33, + 3.6 + ], + "hand": [ + -0.032, + -0.268 + ], + "to": [ + -0.345, + -1.08 + ], + "size": [ + 5.33, + 3.6 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "213:536", + "page": 213, + "line": 14, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 261.6, + 477.6, + 4, + 3.9 + ], + "drawn": [ + 261.538, + 476.1, + 4, + 3.9 + ], + "settled": [ + 260.46, + 476.055, + 4, + 3.73 + ], + "hand": [ + -1.077, + -0.045 + ], + "to": [ + -1.14, + -1.545 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "216:156", + "page": 216, + "line": 5, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 71, + 156.5, + 4, + 3.9 + ], + "drawn": [ + 70.75, + 154.313, + 4, + 3.9 + ], + "settled": [ + 70.2, + 154.395, + 4, + 3.87 + ], + "hand": [ + -0.55, + 0.083 + ], + "to": [ + -0.8, + -2.105 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "217:587", + "page": 217, + "line": 15, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 99.1, + 516.5, + 6.3, + 3.4 + ], + "drawn": [ + 98.725, + 515.75, + 6.3, + 3.4 + ], + "settled": [ + 98.715, + 515.705, + 6.27, + 3.33 + ], + "hand": [ + -0.01, + -0.045 + ], + "to": [ + -0.385, + -0.795 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "218:379", + "page": 218, + "line": 10, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 55, + 355.3, + 4, + 3.9 + ], + "drawn": [ + 54.25, + 353.925, + 4, + 3.9 + ], + "settled": [ + 53.445, + 353.925, + 3.91, + 3.73 + ], + "hand": [ + -0.805, + 0 + ], + "to": [ + -1.555, + -1.375 + ], + "size": [ + 3.91, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "223:533", + "page": 223, + "line": 15, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 313, + 535.7, + 6.2, + 3.4 + ], + "drawn": [ + 312.688, + 534.888, + 6.2, + 3.4 + ], + "settled": [ + 312.685, + 534.705, + 6.13, + 3.73 + ], + "hand": [ + -0.003, + -0.182 + ], + "to": [ + -0.315, + -0.995 + ], + "size": [ + 6.13, + 3.73 + ], + "was": [ + 6.2, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "227:37", + "page": 227, + "line": 1, + "name": "successive kasratan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 62.3, + 34.5, + 7, + 5.5 + ], + "drawn": [ + 61.425, + 33.438, + 7, + 5.5 + ], + "settled": [ + 61.365, + 33.465, + 6.93, + 5.19 + ], + "hand": [ + -0.06, + 0.028 + ], + "to": [ + -0.935, + -1.035 + ], + "size": [ + 6.93, + 5.19 + ], + "was": [ + 7, + 5.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "228:408", + "page": 228, + "line": 10, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 20, + 333.6, + 6.3, + 3.4 + ], + "drawn": [ + 19.313, + 331.663, + 6.3, + 3.4 + ], + "settled": [ + 18.115, + 332.105, + 6.13, + 3.33 + ], + "hand": [ + -1.197, + 0.442 + ], + "to": [ + -1.885, + -1.495 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "257:226", + "page": 257, + "line": 6, + "name": "maddah", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 8, + 192.5, + 6.6, + 3.1 + ], + "drawn": [ + 7.75, + 191.75, + 6.6, + 3.1 + ], + "settled": [ + 7.705, + 191.665, + 6.67, + 3.47 + ], + "hand": [ + -0.045, + -0.085 + ], + "to": [ + -0.295, + -0.835 + ], + "size": [ + 6.67, + 3.47 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "273:241", + "page": 273, + "line": 7, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 286.3, + 249.6, + 5.6, + 3.1 + ], + "drawn": [ + 288.425, + 248.1, + 5.6, + 3.1 + ], + "settled": [ + 282.99, + 245.865, + 5.6, + 3.07 + ], + "hand": [ + -5.435, + -2.235 + ], + "to": [ + -3.31, + -3.735 + ], + "size": [ + 5.6, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "296:39", + "page": 296, + "line": 1, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 17.8, + 13.2, + 4, + 3.9 + ], + "drawn": [ + 17.175, + 11.825, + 4, + 3.9 + ], + "settled": [ + 16.135, + 11.865, + 3.87, + 3.87 + ], + "hand": [ + -1.04, + 0.04 + ], + "to": [ + -1.665, + -1.335 + ], + "size": [ + 3.87, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "312:266", + "page": 312, + "line": 10, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 97.5, + 350.7, + 4, + 3.9 + ], + "drawn": [ + 97.375, + 349.7, + 4, + 3.9 + ], + "settled": [ + 96.33, + 349.655, + 4, + 3.73 + ], + "hand": [ + -1.045, + -0.045 + ], + "to": [ + -1.17, + -1.045 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "321:109", + "page": 321, + "line": 3, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 151.5, + 81.2, + 6.3, + 3.4 + ], + "drawn": [ + 150.75, + 80.013, + 6.3, + 3.4 + ], + "settled": [ + 150.445, + 80.115, + 6.27, + 3.33 + ], + "hand": [ + -0.305, + 0.103 + ], + "to": [ + -1.055, + -1.085 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "330:394", + "page": 330, + "line": 14, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 300.2, + 498.4, + 6.3, + 3.4 + ], + "drawn": [ + 299.138, + 497.65, + 6.3, + 3.4 + ], + "settled": [ + 299.11, + 497.365, + 6, + 3.73 + ], + "hand": [ + -0.028, + -0.285 + ], + "to": [ + -1.09, + -1.035 + ], + "size": [ + 6, + 3.73 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "345:100", + "page": 345, + "line": 3, + "name": "successive kasratan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 163, + 105.4, + 9.1, + 6.8 + ], + "drawn": [ + 162.063, + 104.65, + 9.1, + 6.8 + ], + "settled": [ + 162.11, + 104.68, + 9.2, + 6.8 + ], + "hand": [ + 0.047, + 0.03 + ], + "to": [ + -0.89, + -0.72 + ], + "size": [ + 9.2, + 6.8 + ], + "was": [ + 9.1, + 6.8 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "345:271", + "page": 345, + "line": 8, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 317, + 269.1, + 6.3, + 3.4 + ], + "drawn": [ + 316.563, + 268.288, + 6.3, + 3.4 + ], + "settled": [ + 316.575, + 267.645, + 6.27, + 4.53 + ], + "hand": [ + 0.013, + -0.643 + ], + "to": [ + -0.425, + -1.455 + ], + "size": [ + 6.27, + 4.53 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "350:555", + "page": 350, + "line": 14, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 176.9, + 500.2, + 5.5, + 3.1 + ], + "drawn": [ + 176.525, + 499.263, + 5.5, + 3.1 + ], + "settled": [ + 176.575, + 498.985, + 5.47, + 3.47 + ], + "hand": [ + 0.05, + -0.278 + ], + "to": [ + -0.325, + -1.215 + ], + "size": [ + 5.47, + 3.47 + ], + "was": [ + 5.5, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "359:32", + "page": 359, + "line": 1, + "name": "small yeh", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 92.9, + 26.3, + 6.4, + 4.7 + ], + "drawn": [ + 92.338, + 25.238, + 6.4, + 4.7 + ], + "settled": [ + 91.835, + 25.325, + 6.47, + 4.65 + ], + "hand": [ + -0.502, + 0.088 + ], + "to": [ + -1.065, + -0.975 + ], + "size": [ + 6.47, + 4.65 + ], + "was": [ + 6.4, + 4.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "364:156", + "page": 364, + "line": 5, + "name": "sukun", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 319.9, + 167.1, + 4.4, + 3 + ], + "drawn": [ + 316.65, + 165.225, + 4.4, + 3 + ], + "settled": [ + 316.395, + 165.25, + 4.27, + 2.8 + ], + "hand": [ + -0.255, + 0.025 + ], + "to": [ + -3.505, + -1.85 + ], + "size": [ + 4.27, + 2.8 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "364:447", + "page": 364, + "line": 13, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 176.9, + 465.1, + 6.3, + 3.4 + ], + "drawn": [ + 175.9, + 464.038, + 6.3, + 3.4 + ], + "settled": [ + 175.91, + 463.84, + 6, + 3.6 + ], + "hand": [ + 0.01, + -0.197 + ], + "to": [ + -0.99, + -1.26 + ], + "size": [ + 6, + 3.6 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "369:274", + "page": 369, + "line": 7, + "name": "damma", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 32.2, + 225.8, + 5.3, + 6.6 + ], + "drawn": [ + 32.138, + 224.738, + 5.3, + 6.6 + ], + "settled": [ + 32.115, + 224.69, + 5.27, + 6.62 + ], + "hand": [ + -0.023, + -0.048 + ], + "to": [ + -0.085, + -1.11 + ], + "size": [ + 5.27, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "375:57", + "page": 375, + "line": 2, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 218.9, + 49.2, + 4, + 3.9 + ], + "drawn": [ + 218.525, + 47.263, + 4, + 3.9 + ], + "settled": [ + 217.93, + 47.335, + 4, + 3.87 + ], + "hand": [ + -0.595, + 0.073 + ], + "to": [ + -0.97, + -1.865 + ], + "size": [ + 4, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "391:235", + "page": 391, + "line": 8, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 318.3, + 266.3, + 7, + 3.6 + ], + "drawn": [ + 317.613, + 265.55, + 7, + 3.6 + ], + "settled": [ + 317.82, + 265.015, + 6.8, + 4.27 + ], + "hand": [ + 0.208, + -0.535 + ], + "to": [ + -0.48, + -1.285 + ], + "size": [ + 6.8, + 4.27 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "403:349", + "page": 403, + "line": 10, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 308.7, + 336.9, + 6.3, + 3.4 + ], + "drawn": [ + 308.513, + 336.213, + 6.3, + 3.4 + ], + "settled": [ + 308.44, + 335.91, + 6, + 4 + ], + "hand": [ + -0.073, + -0.302 + ], + "to": [ + -0.26, + -0.99 + ], + "size": [ + 6, + 4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "425:133", + "page": 425, + "line": 4, + "name": "fathatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 132.5, + 126.6, + 7.5, + 6.1 + ], + "drawn": [ + 131.063, + 125.163, + 7.5, + 6.1 + ], + "settled": [ + 131.045, + 125.095, + 7.33, + 5.87 + ], + "hand": [ + -0.018, + -0.067 + ], + "to": [ + -1.455, + -1.505 + ], + "size": [ + 7.33, + 5.87 + ], + "was": [ + 7.5, + 6.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "426:218", + "page": 426, + "line": 5, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 36.2, + 164.5, + 4, + 3.9 + ], + "drawn": [ + 35.825, + 162.063, + 4, + 3.9 + ], + "settled": [ + 34.575, + 162.065, + 4.31, + 3.73 + ], + "hand": [ + -1.25, + 0.002 + ], + "to": [ + -1.625, + -2.435 + ], + "size": [ + 4.31, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "427:54", + "page": 427, + "line": 2, + "name": "fathatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 197.3, + 53.9, + 6.6, + 5.6 + ], + "drawn": [ + 196.113, + 52.15, + 6.6, + 5.6 + ], + "settled": [ + 196.165, + 52.08, + 6.27, + 5.46 + ], + "hand": [ + 0.052, + -0.07 + ], + "to": [ + -1.135, + -1.82 + ], + "size": [ + 6.27, + 5.46 + ], + "was": [ + 6.6, + 5.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "431:295", + "page": 431, + "line": 8, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 43.7, + 286.5, + 6.3, + 3.4 + ], + "drawn": [ + 43.2, + 285.625, + 6.3, + 3.4 + ], + "settled": [ + 43.245, + 285.715, + 6.27, + 3.07 + ], + "hand": [ + 0.045, + 0.09 + ], + "to": [ + -0.455, + -0.785 + ], + "size": [ + 6.27, + 3.07 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "448:211", + "page": 448, + "line": 6, + "name": "successive dammatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 86.1, + 199.5, + 8.2, + 6.4 + ], + "drawn": [ + 85.413, + 198.813, + 8.2, + 6.4 + ], + "settled": [ + 85.615, + 198.72, + 7.65, + 6.48 + ], + "hand": [ + 0.203, + -0.093 + ], + "to": [ + -0.485, + -0.78 + ], + "size": [ + 7.65, + 6.48 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "450:176", + "page": 450, + "line": 5, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 109, + 178.3, + 6.3, + 3.4 + ], + "drawn": [ + 108.625, + 177.613, + 6.3, + 3.4 + ], + "settled": [ + 108.575, + 176.975, + 6.27, + 4.53 + ], + "hand": [ + -0.05, + -0.637 + ], + "to": [ + -0.425, + -1.325 + ], + "size": [ + 6.27, + 4.53 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "455:469", + "page": 455, + "line": 12, + "name": "successive kasratan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 255.8, + 427.2, + 8.3, + 6.6 + ], + "drawn": [ + 255.425, + 426.013, + 8.3, + 6.6 + ], + "settled": [ + 255.11, + 426.24, + 8, + 6.8 + ], + "hand": [ + -0.315, + 0.228 + ], + "to": [ + -0.69, + -0.96 + ], + "size": [ + 8, + 6.8 + ], + "was": [ + 8.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "459:574", + "page": 459, + "line": 15, + "name": "damma", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 200.7, + 517.4, + 5.3, + 6.6 + ], + "drawn": [ + 198.888, + 515.713, + 5.3, + 6.6 + ], + "settled": [ + 198.9, + 515.745, + 5.28, + 6.61 + ], + "hand": [ + 0.012, + 0.032 + ], + "to": [ + -1.8, + -1.655 + ], + "size": [ + 5.28, + 6.61 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "463:130", + "page": 463, + "line": 4, + "name": "wasla", + "rule": "ink", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 42.7, + 120.6, + 5.3, + 4.3 + ], + "drawn": [ + 41.075, + 118.975, + 5.3, + 4.3 + ], + "settled": [ + 41.415, + 118.935, + 6.13, + 4.27 + ], + "hand": [ + 0.34, + -0.04 + ], + "to": [ + -1.285, + -1.665 + ], + "size": [ + 6.13, + 4.27 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "478:95", + "page": 478, + "line": 3, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 198.5, + 86.2, + 6.3, + 3.4 + ], + "drawn": [ + 197.75, + 85.638, + 6.3, + 3.4 + ], + "settled": [ + 197.91, + 85.315, + 6, + 3.87 + ], + "hand": [ + 0.16, + -0.322 + ], + "to": [ + -0.59, + -0.885 + ], + "size": [ + 6, + 3.87 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "487:59", + "page": 487, + "line": 2, + "name": "small yeh", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 204.6, + 61.4, + 6.4, + 4.7 + ], + "drawn": [ + 202.725, + 59.65, + 6.4, + 4.7 + ], + "settled": [ + 202.695, + 59.64, + 6.33, + 4.62 + ], + "hand": [ + -0.03, + -0.01 + ], + "to": [ + -1.905, + -1.76 + ], + "size": [ + 6.33, + 4.62 + ], + "was": [ + 6.4, + 4.7 + ], + "goes": 3, + "reshapes": 3, + "notes": [] + }, + { + "id": "490:513", + "page": 490, + "line": 15, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 310.8, + 519, + 8.5, + 3.9 + ], + "drawn": [ + 310.113, + 517.438, + 8.5, + 3.9 + ], + "settled": [ + 309.87, + 517.915, + 8.4, + 4.53 + ], + "hand": [ + -0.243, + 0.478 + ], + "to": [ + -0.93, + -1.085 + ], + "size": [ + 8.4, + 4.53 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "505:41", + "page": 505, + "line": 2, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 326.2, + 70.6, + 5.6, + 3.1 + ], + "drawn": [ + 325.825, + 69.85, + 5.6, + 3.1 + ], + "settled": [ + 325.785, + 69.6, + 5.33, + 3.6 + ], + "hand": [ + -0.04, + -0.25 + ], + "to": [ + -0.415, + -1 + ], + "size": [ + 5.33, + 3.6 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "506:386", + "page": 506, + "line": 11, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 224.6, + 367.2, + 6.3, + 3.4 + ], + "drawn": [ + 224.163, + 366.513, + 6.3, + 3.4 + ], + "settled": [ + 224.175, + 366.505, + 6.27, + 3.33 + ], + "hand": [ + 0.013, + -0.007 + ], + "to": [ + -0.425, + -0.695 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "511:308", + "page": 511, + "line": 10, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 74.4, + 333.7, + 7, + 3.6 + ], + "drawn": [ + 74.213, + 332.7, + 7, + 3.6 + ], + "settled": [ + 73.835, + 332.81, + 7.87, + 3.6 + ], + "hand": [ + -0.377, + 0.11 + ], + "to": [ + -0.565, + -0.89 + ], + "size": [ + 7.87, + 3.6 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "517:159", + "page": 517, + "line": 4, + "name": "successive kasratan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 87.8, + 145.8, + 7.5, + 5.7 + ], + "drawn": [ + 87.113, + 144.675, + 7.5, + 5.7 + ], + "settled": [ + 87.115, + 144.63, + 7.47, + 5.6 + ], + "hand": [ + 0.002, + -0.045 + ], + "to": [ + -0.685, + -1.17 + ], + "size": [ + 7.47, + 5.6 + ], + "was": [ + 7.5, + 5.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "526:59", + "page": 526, + "line": 4, + "name": "small waw", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 129.8, + 136.2, + 5.2, + 5.8 + ], + "drawn": [ + 127.363, + 134.075, + 5.2, + 5.8 + ], + "settled": [ + 127.39, + 133.995, + 5.2, + 5.77 + ], + "hand": [ + 0.027, + -0.08 + ], + "to": [ + -2.41, + -2.205 + ], + "size": [ + 5.2, + 5.77 + ], + "was": [ + 5.2, + 5.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "538:211", + "page": 538, + "line": 6, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 213.2, + 215.9, + 5.6, + 3.1 + ], + "drawn": [ + 212.638, + 215.15, + 5.6, + 3.1 + ], + "settled": [ + 212.595, + 215.055, + 5.33, + 3.33 + ], + "hand": [ + -0.042, + -0.095 + ], + "to": [ + -0.605, + -0.845 + ], + "size": [ + 5.33, + 3.33 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "539:205", + "page": 539, + "line": 6, + "name": "superscript alef", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 313.6, + 200.6, + 2.1, + 6.1 + ], + "drawn": [ + 312.725, + 199.975, + 2.1, + 6.1 + ], + "settled": [ + 312.67, + 199.96, + 2, + 6 + ], + "hand": [ + -0.055, + -0.015 + ], + "to": [ + -0.93, + -0.64 + ], + "size": [ + 2, + 6 + ], + "was": [ + 2.1, + 6.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "581:152", + "page": 581, + "line": 4, + "name": "successive kasratan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 81.1, + 140.9, + 8.7, + 7 + ], + "drawn": [ + 80.663, + 140.088, + 8.7, + 7 + ], + "settled": [ + 80.515, + 139.98, + 8.27, + 7.2 + ], + "hand": [ + -0.147, + -0.108 + ], + "to": [ + -0.585, + -0.92 + ], + "size": [ + 8.27, + 7.2 + ], + "was": [ + 8.7, + 7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "582:425", + "page": 582, + "line": 15, + "name": "successive fathatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 248.3, + 517.3, + 9.3, + 4.2 + ], + "drawn": [ + 247.8, + 516.675, + 9.3, + 4.2 + ], + "settled": [ + 247.74, + 516.635, + 9.2, + 3.87 + ], + "hand": [ + -0.06, + -0.04 + ], + "to": [ + -0.56, + -0.665 + ], + "size": [ + 9.2, + 3.87 + ], + "was": [ + 9.3, + 4.2 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + } + ] +} diff --git a/docs/validation/rulings/2026-08-20-placement-weak-size-part3.seed23.settled.json b/docs/validation/rulings/2026-08-20-placement-weak-size-part3.seed23.settled.json new file mode 100644 index 00000000..51451e13 --- /dev/null +++ b/docs/validation/rulings/2026-08-20-placement-weak-size-part3.seed23.settled.json @@ -0,0 +1,4530 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-20T03:14:20.580Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "5442d961", + "set": "weak", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-weak-p3of3-a8edff256.23.json", + "slice": "-p3of3-a8edff256", + "finished": "2026-08-20T03:12:47.200Z", + "shown": 91, + "seen": 91, + "looked": 91, + "whole": true, + "answers": 301, + "spoke": 91 + } + ], + "banked": [], + "marks": 91, + "faulted": 91, + "defects": 17, + "answers": 301, + "settledMarks": [ + { + "id": "21:466", + "page": 21, + "line": 13, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 129.5, + 443.7, + 6.1, + 4.9 + ], + "drawn": [ + 128.772, + 442.656, + 6.1, + 4.9 + ], + "settled": [ + 128.95, + 442.755, + 5.74, + 4.53 + ], + "hand": [ + 0.178, + 0.099 + ], + "to": [ + -0.55, + -0.945 + ], + "size": [ + 5.74, + 4.53 + ], + "was": [ + 6.1, + 4.9 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "25:529", + "page": 25, + "line": 14, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 137, + 479.3, + 8.2, + 6.4 + ], + "drawn": [ + 136.382, + 478.45, + 8.2, + 6.4 + ], + "settled": [ + 136.235, + 478.51, + 7.49, + 6.62 + ], + "hand": [ + -0.147, + 0.06 + ], + "to": [ + -0.765, + -0.79 + ], + "size": [ + 7.49, + 6.62 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "46:237", + "page": 46, + "line": 9, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 319.7, + 295.6, + 6.3, + 3.4 + ], + "drawn": [ + 318.942, + 294.108, + 6.3, + 3.4 + ], + "settled": [ + 319.245, + 293.98, + 6.27, + 5.2 + ], + "hand": [ + 0.303, + -0.128 + ], + "to": [ + -0.455, + -1.62 + ], + "size": [ + 6.27, + 5.2 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "60:99", + "page": 60, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 25.1, + 106.6, + 6.3, + 3.4 + ], + "drawn": [ + 22.596, + 105.182, + 6.3, + 3.4 + ], + "settled": [ + 24.45, + 105.25, + 6, + 4 + ], + "hand": [ + 1.854, + 0.068 + ], + "to": [ + -0.65, + -1.35 + ], + "size": [ + 6, + 4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "76:148", + "page": 76, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 228.9, + 155.8, + 6.3, + 3.4 + ], + "drawn": [ + 227.778, + 154.669, + 6.3, + 3.4 + ], + "settled": [ + 228.438, + 154.249, + 6.17, + 4.8 + ], + "hand": [ + 0.66, + -0.42 + ], + "to": [ + -0.462, + -1.551 + ], + "size": [ + 6.17, + 4.8 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 8, + "reshapes": 4, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "94:470", + "page": 94, + "line": 12, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 212.2, + 408.7, + 5.5, + 4.6 + ], + "drawn": [ + 211.591, + 407.707, + 5.5, + 4.6 + ], + "settled": [ + 211.51, + 407.635, + 6, + 4.93 + ], + "hand": [ + -0.081, + -0.072 + ], + "to": [ + -0.69, + -1.065 + ], + "size": [ + 6, + 4.93 + ], + "was": [ + 5.5, + 4.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "95:189", + "page": 95, + "line": 6, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 241, + 193.2, + 7.3, + 3.3 + ], + "drawn": [ + 240.138, + 191.992, + 7.3, + 3.3 + ], + "settled": [ + 240.145, + 191.96, + 7.73, + 4 + ], + "hand": [ + 0.007, + -0.032 + ], + "to": [ + -0.855, + -1.24 + ], + "size": [ + 7.73, + 4 + ], + "was": [ + 7.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "104:27", + "page": 104, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 165.6, + 36.8, + 4, + 3.9 + ], + "drawn": [ + 164.29, + 35.458, + 4, + 3.9 + ], + "settled": [ + 163.865, + 35.065, + 4.67, + 3.87 + ], + "hand": [ + -0.425, + -0.393 + ], + "to": [ + -1.735, + -1.735 + ], + "size": [ + 4.67, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "110:273", + "page": 110, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 257.7, + 356.7, + 4, + 3.9 + ], + "drawn": [ + 255.952, + 355.081, + 4, + 3.9 + ], + "settled": [ + 255.195, + 354.185, + 5.47, + 3.73 + ], + "hand": [ + -0.757, + -0.896 + ], + "to": [ + -2.505, + -2.515 + ], + "size": [ + 5.47, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "125:472", + "page": 125, + "line": 12, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 43, + 409.2, + 4, + 3.9 + ], + "drawn": [ + 42.069, + 408.041, + 4, + 3.9 + ], + "settled": [ + 40.845, + 406.325, + 5.11, + 3.73 + ], + "hand": [ + -1.224, + -1.716 + ], + "to": [ + -2.155, + -2.875 + ], + "size": [ + 5.11, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "132:85", + "page": 132, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 8.7, + 55.6, + 5.6, + 3.1 + ], + "drawn": [ + 5.531, + 51.643, + 5.6, + 3.1 + ], + "settled": [ + 7.265, + 53, + 5.33, + 4.8 + ], + "hand": [ + 1.734, + 1.357 + ], + "to": [ + -1.435, + -2.6 + ], + "size": [ + 5.33, + 4.8 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "138:398", + "page": 138, + "line": 12, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 259, + 409, + 4, + 3.9 + ], + "drawn": [ + 257.83, + 407.087, + 4, + 3.9 + ], + "settled": [ + 256.99, + 407.055, + 4.8, + 3.87 + ], + "hand": [ + -0.84, + -0.032 + ], + "to": [ + -2.01, + -1.945 + ], + "size": [ + 4.8, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "153:353", + "page": 153, + "line": 10, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 228.1, + 336.9, + 9.1, + 3.5 + ], + "drawn": [ + 227.267, + 335.843, + 9.1, + 3.5 + ], + "settled": [ + 227.31, + 335.925, + 9.74, + 3.87 + ], + "hand": [ + 0.043, + 0.082 + ], + "to": [ + -0.79, + -0.975 + ], + "size": [ + 9.74, + 3.87 + ], + "was": [ + 9.1, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "160:69", + "page": 160, + "line": 3, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 247.8, + 87.9, + 10.8, + 3.7 + ], + "drawn": [ + 246.622, + 86.796, + 10.8, + 3.7 + ], + "settled": [ + 246.59, + 86.63, + 9.46, + 3.6 + ], + "hand": [ + -0.032, + -0.166 + ], + "to": [ + -1.21, + -1.27 + ], + "size": [ + 9.46, + 3.6 + ], + "was": [ + 10.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "162:188", + "page": 162, + "line": 5, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 153, + 163.9, + 6.8, + 5.1 + ], + "drawn": [ + 152.169, + 161.399, + 6.8, + 5.1 + ], + "settled": [ + 152.125, + 161.335, + 6.13, + 5.07 + ], + "hand": [ + -0.044, + -0.064 + ], + "to": [ + -0.875, + -2.565 + ], + "size": [ + 6.13, + 5.07 + ], + "was": [ + 6.8, + 5.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "216:377", + "page": 216, + "line": 12, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 117.7, + 405.3, + 4, + 3.9 + ], + "drawn": [ + 116.551, + 404.271, + 4, + 3.9 + ], + "settled": [ + 116.13, + 403.925, + 5.2, + 3.73 + ], + "hand": [ + -0.421, + -0.346 + ], + "to": [ + -1.57, + -1.375 + ], + "size": [ + 5.2, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "220:73", + "page": 220, + "line": 2, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 11.7, + 51.8, + 6.1, + 3.4 + ], + "drawn": [ + 9.897, + 49.005, + 6.1, + 3.4 + ], + "settled": [ + 11.15, + 50.045, + 5.6, + 5.07 + ], + "hand": [ + 1.253, + 1.04 + ], + "to": [ + -0.55, + -1.755 + ], + "size": [ + 5.6, + 5.07 + ], + "was": [ + 6.1, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "222:56", + "page": 222, + "line": 2, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 99.8, + 73.4, + 8.6, + 6.7 + ], + "drawn": [ + 98.86, + 72.099, + 8.6, + 6.7 + ], + "settled": [ + 99.025, + 71.995, + 7.87, + 7.47 + ], + "hand": [ + 0.165, + -0.104 + ], + "to": [ + -0.775, + -1.405 + ], + "size": [ + 7.87, + 7.47 + ], + "was": [ + 8.6, + 6.7 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "228:392", + "page": 228, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 137.7, + 357.9, + 4, + 3.9 + ], + "drawn": [ + 136.605, + 356.282, + 4, + 3.9 + ], + "settled": [ + 135.66, + 355.855, + 4.8, + 3.87 + ], + "hand": [ + -0.945, + -0.427 + ], + "to": [ + -2.04, + -2.045 + ], + "size": [ + 4.8, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "239:443", + "page": 239, + "line": 11, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 233, + 373.7, + 7.8, + 3.7 + ], + "drawn": [ + 232.15, + 372.458, + 7.8, + 3.7 + ], + "settled": [ + 232.49, + 372.49, + 7.6, + 3.6 + ], + "hand": [ + 0.34, + 0.032 + ], + "to": [ + -0.51, + -1.21 + ], + "size": [ + 7.6, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "242:161", + "page": 242, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 11.6, + 125.3, + 6.3, + 3.4 + ], + "drawn": [ + 11.071, + 123.919, + 6.3, + 3.4 + ], + "settled": [ + 11.115, + 123.515, + 6.27, + 4.53 + ], + "hand": [ + 0.044, + -0.404 + ], + "to": [ + -0.485, + -1.785 + ], + "size": [ + 6.27, + 4.53 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "247:272", + "page": 247, + "line": 8, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 251.8, + 259.7, + 6.3, + 3.4 + ], + "drawn": [ + 251.104, + 258.299, + 6.3, + 3.4 + ], + "settled": [ + 251.11, + 258.445, + 6, + 4.27 + ], + "hand": [ + 0.006, + 0.146 + ], + "to": [ + -0.69, + -1.255 + ], + "size": [ + 6, + 4.27 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "252:230", + "page": 252, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 255.4, + 196, + 5.6, + 3.1 + ], + "drawn": [ + 254.028, + 194.564, + 5.6, + 3.1 + ], + "settled": [ + 255.125, + 194.395, + 5.33, + 4.67 + ], + "hand": [ + 1.097, + -0.169 + ], + "to": [ + -0.275, + -1.605 + ], + "size": [ + 5.33, + 4.67 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "252:249", + "page": 252, + "line": 6, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 141, + 192.9, + 6.3, + 3.4 + ], + "drawn": [ + 139.383, + 191.278, + 6.3, + 3.4 + ], + "settled": [ + 140.515, + 191.51, + 5.87, + 4.8 + ], + "hand": [ + 1.132, + 0.232 + ], + "to": [ + -0.485, + -1.39 + ], + "size": [ + 5.87, + 4.8 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "256:347", + "page": 256, + "line": 11, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 230.8, + 385.9, + 4, + 3.9 + ], + "drawn": [ + 229.665, + 385.09, + 4, + 3.9 + ], + "settled": [ + 228.725, + 384.855, + 4.53, + 3.73 + ], + "hand": [ + -0.94, + -0.235 + ], + "to": [ + -2.075, + -1.045 + ], + "size": [ + 4.53, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "264:5", + "page": 264, + "line": 1, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 297.6, + 33.6, + 4, + 3.9 + ], + "drawn": [ + 295.464, + 31.606, + 4, + 3.9 + ], + "settled": [ + 295.325, + 31.535, + 4.67, + 3.73 + ], + "hand": [ + -0.139, + -0.071 + ], + "to": [ + -2.275, + -2.065 + ], + "size": [ + 4.67, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "264:421", + "page": 264, + "line": 12, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 75.6, + 431.9, + 8.6, + 6.8 + ], + "drawn": [ + 74.818, + 431.008, + 8.6, + 6.8 + ], + "settled": [ + 74.695, + 430.805, + 8.79, + 6.53 + ], + "hand": [ + -0.123, + -0.203 + ], + "to": [ + -0.905, + -1.095 + ], + "size": [ + 8.79, + 6.53 + ], + "was": [ + 8.6, + 6.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "266:286", + "page": 266, + "line": 8, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 147.5, + 265.1, + 6.6, + 5.6 + ], + "drawn": [ + 146.786, + 263.985, + 6.6, + 5.6 + ], + "settled": [ + 146.96, + 263.81, + 6.26, + 5.46 + ], + "hand": [ + 0.174, + -0.175 + ], + "to": [ + -0.54, + -1.29 + ], + "size": [ + 6.26, + 5.46 + ], + "was": [ + 6.6, + 5.6 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "272:455", + "page": 272, + "line": 12, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 207.4, + 408.6, + 8.2, + 6.4 + ], + "drawn": [ + 206.393, + 407.521, + 8.2, + 6.4 + ], + "settled": [ + 206.22, + 407.45, + 6.18, + 7.42 + ], + "hand": [ + -0.173, + -0.071 + ], + "to": [ + -1.18, + -1.15 + ], + "size": [ + 6.18, + 7.42 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "274:112", + "page": 274, + "line": 3, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 172.6, + 105.8, + 7.2, + 5.6 + ], + "drawn": [ + 171.802, + 104.37, + 7.2, + 5.6 + ], + "settled": [ + 171.925, + 104.18, + 6.67, + 7 + ], + "hand": [ + 0.123, + -0.19 + ], + "to": [ + -0.675, + -1.62 + ], + "size": [ + 6.67, + 7 + ], + "was": [ + 7.2, + 5.6 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "281:95", + "page": 281, + "line": 3, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 210.4, + 103.7, + 6.3, + 3.4 + ], + "drawn": [ + 208.652, + 101.945, + 6.3, + 3.4 + ], + "settled": [ + 209.78, + 102.245, + 6, + 4.67 + ], + "hand": [ + 1.128, + 0.3 + ], + "to": [ + -0.62, + -1.455 + ], + "size": [ + 6, + 4.67 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "296:283", + "page": 296, + "line": 8, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 134.4, + 267.9, + 8.5, + 4.1 + ], + "drawn": [ + 133.396, + 266.735, + 8.5, + 4.1 + ], + "settled": [ + 133.61, + 266.495, + 7.6, + 4.01 + ], + "hand": [ + 0.214, + -0.24 + ], + "to": [ + -0.79, + -1.405 + ], + "size": [ + 7.6, + 4.01 + ], + "was": [ + 8.5, + 4.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "298:65", + "page": 298, + "line": 2, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 159, + 47.7, + 8.3, + 3.5 + ], + "drawn": [ + 158.184, + 46.68, + 8.3, + 3.5 + ], + "settled": [ + 158.38, + 46.53, + 8.4, + 4 + ], + "hand": [ + 0.196, + -0.15 + ], + "to": [ + -0.62, + -1.17 + ], + "size": [ + 8.4, + 4 + ], + "was": [ + 8.3, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "301:205", + "page": 301, + "line": 6, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 207, + 196.2, + 8.7, + 4.2 + ], + "drawn": [ + 205.584, + 194.901, + 8.7, + 4.2 + ], + "settled": [ + 206.245, + 195.11, + 9.33, + 4.8 + ], + "hand": [ + 0.661, + 0.209 + ], + "to": [ + -0.755, + -1.09 + ], + "size": [ + 9.33, + 4.8 + ], + "was": [ + 8.7, + 4.2 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "308:130", + "page": 308, + "line": 4, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 315.7, + 117.2, + 6.3, + 3.4 + ], + "drawn": [ + 314.2, + 115.682, + 6.3, + 3.4 + ], + "settled": [ + 315.175, + 115.91, + 5.87, + 4 + ], + "hand": [ + 0.975, + 0.228 + ], + "to": [ + -0.525, + -1.29 + ], + "size": [ + 5.87, + 4 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "318:358", + "page": 318, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 33.6, + 333.3, + 4, + 3.9 + ], + "drawn": [ + 33.523, + 331.324, + 4, + 3.9 + ], + "settled": [ + 32.245, + 331.385, + 4.71, + 3.73 + ], + "hand": [ + -1.278, + 0.061 + ], + "to": [ + -1.355, + -1.915 + ], + "size": [ + 4.71, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "334:143", + "page": 334, + "line": 4, + "name": "dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 114.2, + 120, + 5.9, + 8.7 + ], + "drawn": [ + 113.476, + 119.015, + 5.9, + 8.7 + ], + "settled": [ + 113.33, + 118.825, + 5.64, + 9.59 + ], + "hand": [ + -0.146, + -0.19 + ], + "to": [ + -0.87, + -1.175 + ], + "size": [ + 5.64, + 9.59 + ], + "was": [ + 5.9, + 8.7 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "343:278", + "page": 343, + "line": 8, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 232.1, + 283.6, + 6, + 5.1 + ], + "drawn": [ + 230.975, + 282.926, + 6, + 5.1 + ], + "settled": [ + 230.595, + 282.925, + 7.33, + 5.07 + ], + "hand": [ + -0.38, + -0.001 + ], + "to": [ + -1.505, + -0.675 + ], + "size": [ + 7.33, + 5.07 + ], + "was": [ + 6, + 5.1 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "346:243", + "page": 346, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 277.1, + 227.6, + 6.3, + 3.4 + ], + "drawn": [ + 276.475, + 226.663, + 6.3, + 3.4 + ], + "settled": [ + 276.475, + 225.573, + 6.15, + 6.14 + ], + "hand": [ + 0, + -1.09 + ], + "to": [ + -0.625, + -2.027 + ], + "size": [ + 6.15, + 6.14 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 6, + "reshapes": 8, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "347:329", + "page": 347, + "line": 10, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 275.5, + 360.5, + 4.9, + 2.6 + ], + "drawn": [ + 275.017, + 360.085, + 4.9, + 2.6 + ], + "settled": [ + 275.275, + 359.705, + 4.93, + 2.53 + ], + "hand": [ + 0.258, + -0.38 + ], + "to": [ + -0.225, + -0.795 + ], + "size": [ + 4.93, + 2.53 + ], + "was": [ + 4.9, + 2.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "350:311", + "page": 350, + "line": 9, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 152.4, + 300.9, + 3.9, + 3.9 + ], + "drawn": [ + 151.431, + 298.828, + 3.9, + 3.9 + ], + "settled": [ + 150.975, + 298.265, + 5.47, + 3.87 + ], + "hand": [ + -0.456, + -0.563 + ], + "to": [ + -1.425, + -2.635 + ], + "size": [ + 5.47, + 3.87 + ], + "was": [ + 3.9, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "356:33", + "page": 356, + "line": 1, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 112.5, + 17.1, + 7.6, + 3.8 + ], + "drawn": [ + 111.042, + 15.646, + 7.6, + 3.8 + ], + "settled": [ + 111.125, + 15.45, + 9.47, + 5.06 + ], + "hand": [ + 0.083, + -0.196 + ], + "to": [ + -1.375, + -1.65 + ], + "size": [ + 9.47, + 5.06 + ], + "was": [ + 7.6, + 3.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "357:232", + "page": 357, + "line": 7, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 287.6, + 235.4, + 7.5, + 3.9 + ], + "drawn": [ + 285.673, + 233.488, + 7.5, + 3.9 + ], + "settled": [ + 285.835, + 233.59, + 9.73, + 5.46 + ], + "hand": [ + 0.162, + 0.102 + ], + "to": [ + -1.765, + -1.81 + ], + "size": [ + 9.73, + 5.46 + ], + "was": [ + 7.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "360:140", + "page": 360, + "line": 4, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 11.6, + 127.3, + 7.7, + 3.7 + ], + "drawn": [ + 9.476, + 125.15, + 7.7, + 3.7 + ], + "settled": [ + 10.215, + 125.235, + 5.87, + 3.47 + ], + "hand": [ + 0.739, + 0.085 + ], + "to": [ + -1.385, + -2.065 + ], + "size": [ + 5.87, + 3.47 + ], + "was": [ + 7.7, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "374:9", + "page": 374, + "line": 1, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 234.3, + 34.6, + 5.6, + 5.4 + ], + "drawn": [ + 233.946, + 32.609, + 5.6, + 5.4 + ], + "settled": [ + 234.055, + 31.985, + 5.87, + 5.33 + ], + "hand": [ + 0.109, + -0.624 + ], + "to": [ + -0.245, + -2.615 + ], + "size": [ + 5.87, + 5.33 + ], + "was": [ + 5.6, + 5.4 + ], + "goes": 2, + "reshapes": 2, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "376:143", + "page": 376, + "line": 5, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 42.2, + 179.5, + 7, + 3.6 + ], + "drawn": [ + 40.116, + 177.668, + 7, + 3.6 + ], + "settled": [ + 41.83, + 177.945, + 6.8, + 5.07 + ], + "hand": [ + 1.714, + 0.277 + ], + "to": [ + -0.37, + -1.555 + ], + "size": [ + 6.8, + 5.07 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "380:192", + "page": 380, + "line": 5, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 217.6, + 155.3, + 4, + 3.9 + ], + "drawn": [ + 215.813, + 154.327, + 4, + 3.9 + ], + "settled": [ + 214.39, + 154.325, + 5.2, + 3.73 + ], + "hand": [ + -1.423, + -0.002 + ], + "to": [ + -3.21, + -0.975 + ], + "size": [ + 5.2, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "384:193", + "page": 384, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 9.1, + 158.2, + 6.3, + 3.4 + ], + "drawn": [ + 5.882, + 156.162, + 6.3, + 3.4 + ], + "settled": [ + 8.45, + 156.975, + 6, + 4.53 + ], + "hand": [ + 2.568, + 0.813 + ], + "to": [ + -0.65, + -1.225 + ], + "size": [ + 6, + 4.53 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "385:34", + "page": 385, + "line": 1, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 68, + 33.3, + 5.1, + 4.9 + ], + "drawn": [ + 67.351, + 32.341, + 5.1, + 4.9 + ], + "settled": [ + 67.71, + 32.235, + 5.2, + 5.33 + ], + "hand": [ + 0.359, + -0.106 + ], + "to": [ + -0.29, + -1.065 + ], + "size": [ + 5.2, + 5.33 + ], + "was": [ + 5.1, + 4.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "396:425", + "page": 396, + "line": 14, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 179.6, + 499.4, + 8.1, + 6.4 + ], + "drawn": [ + 178.783, + 498.103, + 8.1, + 6.4 + ], + "settled": [ + 179.01, + 498.235, + 7.6, + 6.87 + ], + "hand": [ + 0.227, + 0.132 + ], + "to": [ + -0.59, + -1.165 + ], + "size": [ + 7.6, + 6.87 + ], + "was": [ + 8.1, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "400:295", + "page": 400, + "line": 9, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 98.3, + 318, + 8.9, + 6.7 + ], + "drawn": [ + 96.228, + 316.93, + 8.9, + 6.7 + ], + "settled": [ + 97.15, + 317.045, + 7.44, + 6.79 + ], + "hand": [ + 0.922, + 0.115 + ], + "to": [ + -1.15, + -0.955 + ], + "size": [ + 7.44, + 6.79 + ], + "was": [ + 8.9, + 6.7 + ], + "goes": 12, + "reshapes": 9, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "410:229", + "page": 410, + "line": 7, + "name": "damma", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 274.9, + 233.7, + 5.3, + 6.6 + ], + "drawn": [ + 271.48, + 230.886, + 5.3, + 6.6 + ], + "settled": [ + 271.335, + 230.82, + 5.21, + 6.62 + ], + "hand": [ + -0.145, + -0.066 + ], + "to": [ + -3.565, + -2.88 + ], + "size": [ + 5.21, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "411:251", + "page": 411, + "line": 9, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 58.3, + 324.9, + 6.6, + 5.8 + ], + "drawn": [ + 57.479, + 323.938, + 6.6, + 5.8 + ], + "settled": [ + 57.365, + 323.64, + 6.53, + 5.6 + ], + "hand": [ + -0.114, + -0.298 + ], + "to": [ + -0.935, + -1.26 + ], + "size": [ + 6.53, + 5.6 + ], + "was": [ + 6.6, + 5.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "424:81", + "page": 424, + "line": 2, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 11.5, + 51.8, + 7.3, + 5.9 + ], + "drawn": [ + 10.939, + 51.187, + 7.3, + 5.9 + ], + "settled": [ + 10.75, + 50.795, + 8.14, + 6.13 + ], + "hand": [ + -0.189, + -0.392 + ], + "to": [ + -0.75, + -1.005 + ], + "size": [ + 8.14, + 6.13 + ], + "was": [ + 7.3, + 5.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "429:277", + "page": 429, + "line": 8, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 296.7, + 271.6, + 8.2, + 6.4 + ], + "drawn": [ + 295.244, + 270.399, + 8.2, + 6.4 + ], + "settled": [ + 295.55, + 270.25, + 6.98, + 6.36 + ], + "hand": [ + 0.306, + -0.149 + ], + "to": [ + -1.15, + -1.35 + ], + "size": [ + 6.98, + 6.36 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "430:32", + "page": 430, + "line": 1, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 9.2, + 38.6, + 8.5, + 6.8 + ], + "drawn": [ + 8.181, + 38.02, + 8.5, + 6.8 + ], + "settled": [ + 8.545, + 37.42, + 7.87, + 7.2 + ], + "hand": [ + 0.364, + -0.6 + ], + "to": [ + -0.655, + -1.18 + ], + "size": [ + 7.87, + 7.2 + ], + "was": [ + 8.5, + 6.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "432:292", + "page": 432, + "line": 8, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 172.3, + 262.9, + 4, + 3.9 + ], + "drawn": [ + 171.086, + 261.059, + 4, + 3.9 + ], + "settled": [ + 170.195, + 260.595, + 5.33, + 3.73 + ], + "hand": [ + -0.891, + -0.464 + ], + "to": [ + -2.105, + -2.305 + ], + "size": [ + 5.33, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "438:236", + "page": 438, + "line": 7, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 198.2, + 225.8, + 4, + 3.9 + ], + "drawn": [ + 197.089, + 224.46, + 4, + 3.9 + ], + "settled": [ + 196.395, + 223.655, + 4.93, + 3.73 + ], + "hand": [ + -0.694, + -0.805 + ], + "to": [ + -1.805, + -2.145 + ], + "size": [ + 4.93, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "440:240", + "page": 440, + "line": 9, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 24.2, + 318, + 4, + 3.9 + ], + "drawn": [ + 23.606, + 315.973, + 4, + 3.9 + ], + "settled": [ + 22.325, + 316.055, + 4.53, + 3.73 + ], + "hand": [ + -1.281, + 0.082 + ], + "to": [ + -1.875, + -1.945 + ], + "size": [ + 4.53, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "441:456", + "page": 441, + "line": 13, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 232.2, + 462.5, + 4, + 3.9 + ], + "drawn": [ + 231.2, + 460.966, + 4, + 3.9 + ], + "settled": [ + 230.325, + 460.855, + 4.53, + 3.73 + ], + "hand": [ + -0.875, + -0.111 + ], + "to": [ + -1.875, + -1.645 + ], + "size": [ + 4.53, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "442:229", + "page": 442, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 292.9, + 233, + 7, + 3.6 + ], + "drawn": [ + 291.26, + 231.651, + 7, + 3.6 + ], + "settled": [ + 292.625, + 231.545, + 7.07, + 4.53 + ], + "hand": [ + 1.365, + -0.106 + ], + "to": [ + -0.275, + -1.455 + ], + "size": [ + 7.07, + 4.53 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "442:246", + "page": 442, + "line": 7, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 78.8, + 249.3, + 7.9, + 6.5 + ], + "drawn": [ + 76.379, + 247.937, + 7.9, + 6.5 + ], + "settled": [ + 77.18, + 247.625, + 9.6, + 6.27 + ], + "hand": [ + 0.801, + -0.312 + ], + "to": [ + -1.62, + -1.675 + ], + "size": [ + 9.6, + 6.27 + ], + "was": [ + 7.9, + 6.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "454:431", + "page": 454, + "line": 11, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 207.3, + 372.9, + 6.3, + 3.4 + ], + "drawn": [ + 205.807, + 371.207, + 6.3, + 3.4 + ], + "settled": [ + 207.175, + 371.575, + 5.87, + 4.67 + ], + "hand": [ + 1.368, + 0.368 + ], + "to": [ + -0.125, + -1.325 + ], + "size": [ + 5.87, + 4.67 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "456:331", + "page": 456, + "line": 8, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 138, + 288, + 8.8, + 6.8 + ], + "drawn": [ + 137.424, + 287.039, + 8.8, + 6.8 + ], + "settled": [ + 137.325, + 287.075, + 8.39, + 7.07 + ], + "hand": [ + -0.099, + 0.036 + ], + "to": [ + -0.675, + -0.925 + ], + "size": [ + 8.39, + 7.07 + ], + "was": [ + 8.8, + 6.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "457:46", + "page": 457, + "line": 2, + "name": "fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 297.7, + 49.3, + 5.9, + 5.3 + ], + "drawn": [ + 296.564, + 48.379, + 5.9, + 5.3 + ], + "settled": [ + 296.71, + 48.43, + 6.94, + 5.06 + ], + "hand": [ + 0.146, + 0.051 + ], + "to": [ + -0.99, + -0.87 + ], + "size": [ + 6.94, + 5.06 + ], + "was": [ + 5.9, + 5.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "464:279", + "page": 464, + "line": 8, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 103.8, + 283.8, + 7.9, + 6.7 + ], + "drawn": [ + 102.805, + 282.839, + 7.9, + 6.7 + ], + "settled": [ + 103.445, + 282.795, + 7.73, + 7.21 + ], + "hand": [ + 0.64, + -0.044 + ], + "to": [ + -0.355, + -1.005 + ], + "size": [ + 7.73, + 7.21 + ], + "was": [ + 7.9, + 6.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "470:58", + "page": 470, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "print-defect" + ], + "fault": true, + "box": [ + 166.1, + 74, + 7, + 3.6 + ], + "drawn": [ + 163.907, + 71.45, + 7, + 3.6 + ], + "settled": [ + 162.417, + 70.87, + 7, + 3.6 + ], + "hand": [ + -1.49, + -0.58 + ], + "to": [ + -3.683, + -3.13 + ], + "size": null, + "was": null, + "goes": 7, + "reshapes": 0, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "489:81", + "page": 489, + "line": 3, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 325.1, + 94.3, + 6.3, + 3.4 + ], + "drawn": [ + 323.268, + 92.653, + 6.3, + 3.4 + ], + "settled": [ + 324.57, + 92.915, + 6, + 4.67 + ], + "hand": [ + 1.302, + 0.262 + ], + "to": [ + -0.53, + -1.385 + ], + "size": [ + 6, + 4.67 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "508:165", + "page": 508, + "line": 4, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 111.9, + 119.8, + 4, + 3.9 + ], + "drawn": [ + 111.164, + 118.613, + 4, + 3.9 + ], + "settled": [ + 110.294, + 118.323, + 4.98, + 3.81 + ], + "hand": [ + -0.87, + -0.29 + ], + "to": [ + -1.606, + -1.477 + ], + "size": [ + 4.98, + 3.81 + ], + "was": [ + 4, + 3.9 + ], + "goes": 6, + "reshapes": 7, + "notes": [] + }, + { + "id": "522:244", + "page": 522, + "line": 7, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 316.9, + 235.8, + 6.3, + 3.4 + ], + "drawn": [ + 315.055, + 233.115, + 6.3, + 3.4 + ], + "settled": [ + 316.44, + 234.11, + 6, + 5.2 + ], + "hand": [ + 1.385, + 0.995 + ], + "to": [ + -0.46, + -1.69 + ], + "size": [ + 6, + 5.2 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "526:317", + "page": 526, + "line": 11, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 331.1, + 373.4, + 4, + 3.9 + ], + "drawn": [ + 328.783, + 370.754, + 4, + 3.9 + ], + "settled": [ + 328.923, + 370.934, + 5.34, + 3.85 + ], + "hand": [ + 0.14, + 0.18 + ], + "to": [ + -2.177, + -2.466 + ], + "size": [ + 5.34, + 3.85 + ], + "was": [ + 4, + 3.9 + ], + "goes": 5, + "reshapes": 6, + "notes": [] + }, + { + "id": "530:346", + "page": 530, + "line": 9, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 35.6, + 322.3, + 6.3, + 3.4 + ], + "drawn": [ + 33.054, + 319.777, + 6.3, + 3.4 + ], + "settled": [ + 35.11, + 320.505, + 6, + 5.47 + ], + "hand": [ + 2.056, + 0.728 + ], + "to": [ + -0.49, + -1.795 + ], + "size": [ + 6, + 5.47 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "535:386", + "page": 535, + "line": 11, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 25.6, + 389.6, + 8.1, + 5.9 + ], + "drawn": [ + 24.973, + 389.027, + 8.1, + 5.9 + ], + "settled": [ + 25.345, + 388.79, + 8.27, + 5.6 + ], + "hand": [ + 0.372, + -0.237 + ], + "to": [ + -0.255, + -0.81 + ], + "size": [ + 8.27, + 5.6 + ], + "was": [ + 8.1, + 5.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "554:240", + "page": 554, + "line": 9, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 329.6, + 318.6, + 4, + 3.9 + ], + "drawn": [ + 328.142, + 318.592, + 4, + 3.9 + ], + "settled": [ + 328.035, + 318.655, + 4.57, + 3.87 + ], + "hand": [ + -0.107, + 0.063 + ], + "to": [ + -1.565, + 0.055 + ], + "size": [ + 4.57, + 3.87 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "562:223", + "page": 562, + "line": 8, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 169, + 271, + 8.2, + 3.6 + ], + "drawn": [ + 167.896, + 269.523, + 8.2, + 3.6 + ], + "settled": [ + 168.09, + 269.475, + 7.2, + 3.59 + ], + "hand": [ + 0.194, + -0.048 + ], + "to": [ + -0.91, + -1.525 + ], + "size": [ + 7.2, + 3.59 + ], + "was": [ + 8.2, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "564:155", + "page": 564, + "line": 4, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 27.6, + 144.1, + 9.3, + 7.1 + ], + "drawn": [ + 25.887, + 140.893, + 9.3, + 7.1 + ], + "settled": [ + 26.475, + 142.065, + 8.53, + 8.53 + ], + "hand": [ + 0.588, + 1.172 + ], + "to": [ + -1.125, + -2.035 + ], + "size": [ + 8.53, + 8.53 + ], + "was": [ + 9.3, + 7.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "566:519", + "page": 566, + "line": 15, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 22, + 536.1, + 8.4, + 6.8 + ], + "drawn": [ + 21.338, + 535.18, + 8.4, + 6.8 + ], + "settled": [ + 21.265, + 535.205, + 8.41, + 6.53 + ], + "hand": [ + -0.073, + 0.025 + ], + "to": [ + -0.735, + -0.895 + ], + "size": [ + 8.41, + 6.53 + ], + "was": [ + 8.4, + 6.8 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "568:379", + "page": 568, + "line": 13, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 318.3, + 460.8, + 6.7, + 5.6 + ], + "drawn": [ + 316.245, + 458.744, + 6.7, + 5.6 + ], + "settled": [ + 317.935, + 459.744, + 6.7, + 5.5 + ], + "hand": [ + 1.69, + 1 + ], + "to": [ + -0.365, + -1.056 + ], + "size": [ + 6.7, + 5.5 + ], + "was": [ + 6.7, + 5.6 + ], + "goes": 4, + "reshapes": 2, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "570:284", + "page": 570, + "line": 10, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 244.3, + 358.1, + 4, + 3.9 + ], + "drawn": [ + 243.23, + 356.913, + 4, + 3.9 + ], + "settled": [ + 242.395, + 356.585, + 5.47, + 3.73 + ], + "hand": [ + -0.835, + -0.328 + ], + "to": [ + -1.905, + -1.515 + ], + "size": [ + 5.47, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "574:399", + "page": 574, + "line": 12, + "name": "shadda", + "rule": "line-tilt", + "words": [ + "placement", + "print-defect" + ], + "fault": true, + "box": [ + 32.4, + 414.6, + 4.3, + 3.3 + ], + "drawn": [ + 30.715, + 413.676, + 4.3, + 3.3 + ], + "settled": [ + 31.815, + 413.576, + 4.3, + 3.3 + ], + "hand": [ + 1.1, + -0.1 + ], + "to": [ + -0.585, + -1.024 + ], + "size": null, + "was": null, + "goes": 3, + "reshapes": 0, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "576:3", + "page": 576, + "line": 1, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 309.6, + 14.7, + 6.3, + 3.4 + ], + "drawn": [ + 306.601, + 11.431, + 6.3, + 3.4 + ], + "settled": [ + 306.445, + 11.52, + 6.27, + 3.2 + ], + "hand": [ + -0.156, + 0.089 + ], + "to": [ + -3.155, + -3.18 + ], + "size": [ + 6.27, + 3.2 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "578:139", + "page": 578, + "line": 4, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 145.6, + 140.5, + 4, + 3.9 + ], + "drawn": [ + 142.839, + 137.173, + 4, + 3.9 + ], + "settled": [ + 142.325, + 136.865, + 5.33, + 3.73 + ], + "hand": [ + -0.514, + -0.308 + ], + "to": [ + -3.275, + -3.635 + ], + "size": [ + 5.33, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "585:192", + "page": 585, + "line": 7, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "retracted", + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 214.3, + 248.3, + 7.3, + 5.6 + ], + "drawn": [ + 213.662, + 247.378, + 7.3, + 5.6 + ], + "settled": [ + 213.692, + 247.388, + 7.39, + 5.5 + ], + "hand": [ + 0.03, + 0.01 + ], + "to": [ + -0.608, + -0.912 + ], + "size": [ + 7.39, + 5.5 + ], + "was": [ + 7.3, + 5.6 + ], + "goes": 4, + "reshapes": 1, + "notes": [] + }, + { + "id": "586:152", + "page": 586, + "line": 7, + "name": "successive kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 209.6, + 254.5, + 8.4, + 6.6 + ], + "drawn": [ + 209.28, + 253.084, + 8.4, + 6.6 + ], + "settled": [ + 209.455, + 253.045, + 8.53, + 6.53 + ], + "hand": [ + 0.175, + -0.039 + ], + "to": [ + -0.145, + -1.455 + ], + "size": [ + 8.53, + 6.53 + ], + "was": [ + 8.4, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:47", + "page": 588, + "line": 2, + "name": "kasra", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 286.5, + 73.2, + 5.6, + 3.1 + ], + "drawn": [ + 283.544, + 71.88, + 5.6, + 3.1 + ], + "settled": [ + 285.855, + 71.995, + 5.47, + 4.13 + ], + "hand": [ + 2.311, + 0.115 + ], + "to": [ + -0.645, + -1.205 + ], + "size": [ + 5.47, + 4.13 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "588:133", + "page": 588, + "line": 4, + "name": "kasratan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 250.9, + 141.7, + 6.2, + 5.3 + ], + "drawn": [ + 249.357, + 140.198, + 6.2, + 5.3 + ], + "settled": [ + 250.42, + 140.96, + 6.4, + 4.8 + ], + "hand": [ + 1.063, + 0.762 + ], + "to": [ + -0.48, + -0.74 + ], + "size": [ + 6.4, + 4.8 + ], + "was": [ + 6.2, + 5.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "588:179", + "page": 588, + "line": 5, + "name": "fatha", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 250.5, + 157, + 6.3, + 3.4 + ], + "drawn": [ + 246.827, + 154.214, + 6.3, + 3.4 + ], + "settled": [ + 251.705, + 156.715, + 6.93, + 3.47 + ], + "hand": [ + 4.878, + 2.501 + ], + "to": [ + 1.205, + -0.285 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "589:272", + "page": 589, + "line": 10, + "name": "successive fathatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 276.5, + 339, + 8.8, + 4.1 + ], + "drawn": [ + 275.266, + 336.957, + 8.8, + 4.1 + ], + "settled": [ + 275.125, + 337.36, + 9.59, + 5.2 + ], + "hand": [ + -0.141, + 0.403 + ], + "to": [ + -1.375, + -1.64 + ], + "size": [ + 9.59, + 5.2 + ], + "was": [ + 8.8, + 4.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "598:221", + "page": 598, + "line": 8, + "name": "dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 156.2, + 267.1, + 5.9, + 8.7 + ], + "drawn": [ + 154.667, + 265.312, + 5.9, + 8.7 + ], + "settled": [ + 154.645, + 265.31, + 7.33, + 10.74 + ], + "hand": [ + -0.022, + -0.002 + ], + "to": [ + -1.555, + -1.79 + ], + "size": [ + 7.33, + 10.74 + ], + "was": [ + 5.9, + 8.7 + ], + "goes": 3, + "reshapes": 3, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "598:322", + "page": 598, + "line": 12, + "name": "successive dammatan", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 9.3, + 412.2, + 8.2, + 6.4 + ], + "drawn": [ + 8.671, + 411.455, + 8.2, + 6.4 + ], + "settled": [ + 8.335, + 410.905, + 7.31, + 6.35 + ], + "hand": [ + -0.336, + -0.55 + ], + "to": [ + -0.965, + -1.295 + ], + "size": [ + 7.31, + 6.35 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "599:201", + "page": 599, + "line": 8, + "name": "hamza", + "rule": "line-tilt", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 328.2, + 284.9, + 4, + 3.9 + ], + "drawn": [ + 325.69, + 282.833, + 4, + 3.9 + ], + "settled": [ + 326.305, + 282.455, + 5.11, + 3.73 + ], + "hand": [ + 0.615, + -0.378 + ], + "to": [ + -1.895, + -2.445 + ], + "size": [ + 5.11, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + } + ] +} diff --git a/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.75_0.85.seed23.settled.json b/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.75_0.85.seed23.settled.json new file mode 100644 index 00000000..ad9d875f --- /dev/null +++ b/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.75_0.85.seed23.settled.json @@ -0,0 +1,1503 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-20T02:48:30.300Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "5442d961", + "set": "placed", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-placed-b0.75_0.85-a8edff256.23.json", + "slice": "-b0.75_0.85-a8edff256", + "finished": "2026-08-20T02:38:35.453Z", + "shown": 30, + "seen": 30, + "looked": 30, + "whole": true, + "answers": 63, + "spoke": 30 + } + ], + "banked": [], + "marks": 30, + "faulted": 30, + "defects": 1, + "answers": 63, + "settledMarks": [ + { + "id": "2:89", + "page": 2, + "line": 10, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 14, + 108, + 6.1, + 3 + ], + "drawn": [ + 13.313, + 107.063, + 6.1, + 3 + ], + "settled": [ + 13.21, + 107.11, + 5.6, + 2.8 + ], + "hand": [ + -0.103, + 0.047 + ], + "to": [ + -0.79, + -0.89 + ], + "size": [ + 5.6, + 2.8 + ], + "was": [ + 6.1, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "10:5", + "page": 10, + "line": 1, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 304.4, + 10.5, + 4.3, + 3.3 + ], + "drawn": [ + 303.9, + 9.5, + 4.3, + 3.3 + ], + "settled": [ + 303.84, + 9.57, + 4.12, + 3.2 + ], + "hand": [ + -0.06, + 0.07 + ], + "to": [ + -0.56, + -0.93 + ], + "size": [ + 4.12, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "22:529", + "page": 22, + "line": 15, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 152.3, + 539.7, + 4.3, + 2.3 + ], + "drawn": [ + 151.488, + 538.325, + 4.3, + 2.3 + ], + "settled": [ + 151.515, + 538.385, + 4.27, + 2.27 + ], + "hand": [ + 0.027, + 0.06 + ], + "to": [ + -0.785, + -1.315 + ], + "size": [ + 4.27, + 2.27 + ], + "was": [ + 4.3, + 2.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "33:496", + "page": 33, + "line": 13, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 214.5, + 466.9, + 7, + 3.6 + ], + "drawn": [ + 213.625, + 465.713, + 7, + 3.6 + ], + "settled": [ + 213.625, + 465.675, + 6.93, + 3.47 + ], + "hand": [ + 0, + -0.038 + ], + "to": [ + -0.875, + -1.225 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "49:532", + "page": 49, + "line": 15, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 90, + 536.5, + 6.3, + 3.4 + ], + "drawn": [ + 90.063, + 535.563, + 6.3, + 3.4 + ], + "settled": [ + 89.575, + 535.705, + 6.13, + 3.33 + ], + "hand": [ + -0.487, + 0.142 + ], + "to": [ + -0.425, + -0.795 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "102:475", + "page": 102, + "line": 13, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 80.9, + 447.8, + 6.3, + 3.4 + ], + "drawn": [ + 80.588, + 446.8, + 6.3, + 3.4 + ], + "settled": [ + 80.45, + 446.835, + 6, + 3.47 + ], + "hand": [ + -0.137, + 0.035 + ], + "to": [ + -0.45, + -0.965 + ], + "size": [ + 6, + 3.47 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "103:122", + "page": 103, + "line": 3, + "name": "fathatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 8.1, + 86.8, + 6.8, + 5.4 + ], + "drawn": [ + 7.663, + 85.925, + 6.8, + 5.4 + ], + "settled": [ + 7.735, + 85.91, + 6.67, + 5.2 + ], + "hand": [ + 0.073, + -0.015 + ], + "to": [ + -0.365, + -0.89 + ], + "size": [ + 6.67, + 5.2 + ], + "was": [ + 6.8, + 5.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "111:81", + "page": 111, + "line": 2, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 26.2, + 67.9, + 5.6, + 3.1 + ], + "drawn": [ + 25.825, + 66.963, + 5.6, + 3.1 + ], + "settled": [ + 25.8, + 66.995, + 5.6, + 2.93 + ], + "hand": [ + -0.025, + 0.032 + ], + "to": [ + -0.4, + -0.905 + ], + "size": [ + 5.6, + 2.93 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "113:31", + "page": 113, + "line": 1, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 74.4, + 9, + 5.6, + 3.1 + ], + "drawn": [ + 71.463, + 9.563, + 5.6, + 3.1 + ], + "settled": [ + 70.86, + 9.735, + 5.6, + 3.07 + ], + "hand": [ + -0.602, + 0.173 + ], + "to": [ + -3.54, + 0.735 + ], + "size": [ + 5.6, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "123:391", + "page": 123, + "line": 11, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 235.5, + 372.8, + 6.3, + 3.4 + ], + "drawn": [ + 234.375, + 371.613, + 6.3, + 3.4 + ], + "settled": [ + 234.315, + 371.64, + 6.27, + 3.2 + ], + "hand": [ + -0.06, + 0.028 + ], + "to": [ + -1.185, + -1.16 + ], + "size": [ + 6.27, + 3.2 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "128:85", + "page": 128, + "line": 5, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape", + "print-defect" + ], + "fault": true, + "box": [ + 231, + 170.5, + 4.3, + 3.3 + ], + "drawn": [ + 229.063, + 168.5, + 4.3, + 3.3 + ], + "settled": [ + 229.015, + 168.505, + 4.19, + 3.21 + ], + "hand": [ + -0.048, + 0.005 + ], + "to": [ + -1.985, + -1.995 + ], + "size": [ + 4.19, + 3.21 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [ + "Odd — I cannot say how" + ] + }, + { + "id": "153:279", + "page": 153, + "line": 8, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 159.7, + 264.5, + 6.3, + 3.4 + ], + "drawn": [ + 159.263, + 263.813, + 6.3, + 3.4 + ], + "settled": [ + 159.175, + 263.845, + 6.13, + 3.33 + ], + "hand": [ + -0.088, + 0.032 + ], + "to": [ + -0.525, + -0.655 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "154:105", + "page": 154, + "line": 3, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 139.1, + 108.4, + 7, + 3.6 + ], + "drawn": [ + 137.6, + 107.713, + 7, + 3.6 + ], + "settled": [ + 138.09, + 107.415, + 6.8, + 3.47 + ], + "hand": [ + 0.49, + -0.297 + ], + "to": [ + -1.01, + -0.985 + ], + "size": [ + 6.8, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "205:199", + "page": 205, + "line": 7, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 193.4, + 247.7, + 7, + 3.6 + ], + "drawn": [ + 191.838, + 245.888, + 7, + 3.6 + ], + "settled": [ + 192.025, + 245.74, + 6.93, + 3.6 + ], + "hand": [ + 0.188, + -0.147 + ], + "to": [ + -1.375, + -1.96 + ], + "size": [ + 6.93, + 3.6 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "205:326", + "page": 205, + "line": 10, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 150.7, + 338.9, + 4.3, + 3.3 + ], + "drawn": [ + 149.513, + 338.088, + 4.3, + 3.3 + ], + "settled": [ + 149.425, + 338.09, + 4.19, + 3.2 + ], + "hand": [ + -0.087, + 0.002 + ], + "to": [ + -1.275, + -0.81 + ], + "size": [ + 4.19, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "228:61", + "page": 228, + "line": 2, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 207.1, + 57.9, + 4.3, + 3.3 + ], + "drawn": [ + 205.413, + 56.338, + 4.3, + 3.3 + ], + "settled": [ + 205.31, + 56.36, + 4.12, + 3.2 + ], + "hand": [ + -0.103, + 0.022 + ], + "to": [ + -1.79, + -1.54 + ], + "size": [ + 4.12, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "231:186", + "page": 231, + "line": 6, + "name": "successive kasratan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 83, + 213.9, + 8.2, + 6.5 + ], + "drawn": [ + 81.5, + 212.713, + 8.2, + 6.5 + ], + "settled": [ + 81.43, + 212.76, + 8, + 6.4 + ], + "hand": [ + -0.07, + 0.048 + ], + "to": [ + -1.57, + -1.14 + ], + "size": [ + 8, + 6.4 + ], + "was": [ + 8.2, + 6.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "250:408", + "page": 250, + "line": 12, + "name": "small yeh", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 250, + 420.3, + 6.4, + 4.6 + ], + "drawn": [ + 249.625, + 419.488, + 6.4, + 4.6 + ], + "settled": [ + 249.39, + 419.495, + 6.4, + 4.65 + ], + "hand": [ + -0.235, + 0.007 + ], + "to": [ + -0.61, + -0.805 + ], + "size": [ + 6.4, + 4.65 + ], + "was": [ + 6.4, + 4.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "251:236", + "page": 251, + "line": 6, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 146.6, + 218.4, + 6.3, + 3.4 + ], + "drawn": [ + 146.225, + 217.65, + 6.3, + 3.4 + ], + "settled": [ + 146.245, + 217.575, + 6.13, + 3.33 + ], + "hand": [ + 0.02, + -0.075 + ], + "to": [ + -0.355, + -0.825 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "286:267", + "page": 286, + "line": 7, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 275.2, + 226.4, + 6.3, + 3.4 + ], + "drawn": [ + 273.7, + 224.088, + 6.3, + 3.4 + ], + "settled": [ + 273.645, + 223.98, + 6.27, + 3.6 + ], + "hand": [ + -0.055, + -0.107 + ], + "to": [ + -1.555, + -2.42 + ], + "size": [ + 6.27, + 3.6 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "321:59", + "page": 321, + "line": 2, + "name": "small meem", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 155.1, + 56.3, + 3.3, + 9.5 + ], + "drawn": [ + 154.663, + 55.425, + 3.3, + 9.5 + ], + "settled": [ + 154.68, + 55.395, + 3.2, + 9.33 + ], + "hand": [ + 0.018, + -0.03 + ], + "to": [ + -0.42, + -0.905 + ], + "size": [ + 3.2, + 9.33 + ], + "was": [ + 3.3, + 9.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "373:371", + "page": 373, + "line": 11, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 321.6, + 371.6, + 4.3, + 3.3 + ], + "drawn": [ + 320.85, + 370.85, + 4.3, + 3.3 + ], + "settled": [ + 320.81, + 370.89, + 4.06, + 3.2 + ], + "hand": [ + -0.04, + 0.04 + ], + "to": [ + -0.79, + -0.71 + ], + "size": [ + 4.06, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "378:389", + "page": 378, + "line": 10, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 41.8, + 328.2, + 6.3, + 3.4 + ], + "drawn": [ + 41.55, + 327.013, + 6.3, + 3.4 + ], + "settled": [ + 41.315, + 327.035, + 6.13, + 3.33 + ], + "hand": [ + -0.235, + 0.022 + ], + "to": [ + -0.485, + -1.165 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "405:341", + "page": 405, + "line": 10, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 283.5, + 335, + 4.3, + 3.3 + ], + "drawn": [ + 283, + 334.188, + 4.3, + 3.3 + ], + "settled": [ + 282.91, + 334.22, + 4.12, + 3.2 + ], + "hand": [ + -0.09, + 0.032 + ], + "to": [ + -0.59, + -0.78 + ], + "size": [ + 4.12, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "416:342", + "page": 416, + "line": 10, + "name": "successive kasratan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 9.3, + 356.4, + 8, + 6.4 + ], + "drawn": [ + 7.613, + 355.15, + 8, + 6.4 + ], + "settled": [ + 7.665, + 355.145, + 8.01, + 6.27 + ], + "hand": [ + 0.052, + -0.005 + ], + "to": [ + -1.635, + -1.255 + ], + "size": [ + 8.01, + 6.27 + ], + "was": [ + 8, + 6.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "419:303", + "page": 419, + "line": 9, + "name": "successive fathatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 256.6, + 303.8, + 9.2, + 4.6 + ], + "drawn": [ + 255.725, + 301.988, + 9.2, + 4.6 + ], + "settled": [ + 255.66, + 301.91, + 9.2, + 4.4 + ], + "hand": [ + -0.065, + -0.077 + ], + "to": [ + -0.94, + -1.89 + ], + "size": [ + 9.2, + 4.4 + ], + "was": [ + 9.2, + 4.6 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "499:404", + "page": 499, + "line": 11, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 60.8, + 368.1, + 6.3, + 3.4 + ], + "drawn": [ + 59.8, + 366.788, + 6.3, + 3.4 + ], + "settled": [ + 59.785, + 366.775, + 6.27, + 3.33 + ], + "hand": [ + -0.015, + -0.012 + ], + "to": [ + -1.015, + -1.325 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "504:547", + "page": 504, + "line": 13, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 94.5, + 438.5, + 6.3, + 3.4 + ], + "drawn": [ + 93.625, + 436.75, + 6.3, + 3.4 + ], + "settled": [ + 93.575, + 436.775, + 6.13, + 3.33 + ], + "hand": [ + -0.05, + 0.025 + ], + "to": [ + -0.925, + -1.725 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "508:230", + "page": 508, + "line": 5, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 16.8, + 156.7, + 4.3, + 3.3 + ], + "drawn": [ + 16.738, + 156.075, + 4.3, + 3.3 + ], + "settled": [ + 16.65, + 156.1, + 4.12, + 3.2 + ], + "hand": [ + -0.087, + 0.025 + ], + "to": [ + -0.15, + -0.6 + ], + "size": [ + 4.12, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "531:446", + "page": 531, + "line": 15, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 52.6, + 513.1, + 7.8, + 3.7 + ], + "drawn": [ + 52.1, + 512.35, + 7.8, + 3.7 + ], + "settled": [ + 52.165, + 512.285, + 7.73, + 3.47 + ], + "hand": [ + 0.065, + -0.065 + ], + "to": [ + -0.435, + -0.815 + ], + "size": [ + 7.73, + 3.47 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + } + ] +} diff --git a/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.85_0.95.seed23.settled.json b/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.85_0.95.seed23.settled.json new file mode 100644 index 00000000..41bed3e7 --- /dev/null +++ b/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.85_0.95.seed23.settled.json @@ -0,0 +1,1500 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-20T02:48:31.162Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "5442d961", + "set": "placed", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-placed-b0.85_0.95-a8edff256.23.json", + "slice": "-b0.85_0.95-a8edff256", + "finished": "2026-08-20T02:39:35.820Z", + "shown": 30, + "seen": 30, + "looked": 30, + "whole": true, + "answers": 62, + "spoke": 30 + } + ], + "banked": [], + "marks": 30, + "faulted": 30, + "defects": 0, + "answers": 62, + "settledMarks": [ + { + "id": "12:71", + "page": 12, + "line": 2, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 39.2, + 68.6, + 4.3, + 2.3 + ], + "drawn": [ + 38.763, + 67.163, + 4.3, + 2.3 + ], + "settled": [ + 38.715, + 67.195, + 4.27, + 2.27 + ], + "hand": [ + -0.047, + 0.032 + ], + "to": [ + -0.485, + -1.405 + ], + "size": [ + 4.27, + 2.27 + ], + "was": [ + 4.3, + 2.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "22:70", + "page": 22, + "line": 2, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 60, + 74.1, + 4.3, + 2.3 + ], + "drawn": [ + 59.438, + 73.163, + 4.3, + 2.3 + ], + "settled": [ + 59.385, + 73.195, + 4.27, + 2.27 + ], + "hand": [ + -0.052, + 0.032 + ], + "to": [ + -0.615, + -0.905 + ], + "size": [ + 4.27, + 2.27 + ], + "was": [ + 4.3, + 2.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "25:525", + "page": 25, + "line": 14, + "name": "sukun", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 166.3, + 485.1, + 4.4, + 3 + ], + "drawn": [ + 165.613, + 484.475, + 4.4, + 3 + ], + "settled": [ + 165.595, + 484.365, + 4.27, + 2.93 + ], + "hand": [ + -0.017, + -0.11 + ], + "to": [ + -0.705, + -0.735 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "58:385", + "page": 58, + "line": 11, + "name": "superscript alef", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 319.3, + 380.4, + 2.1, + 6.1 + ], + "drawn": [ + 318.675, + 378.4, + 2.1, + 6.1 + ], + "settled": [ + 318.605, + 378.49, + 2.13, + 6 + ], + "hand": [ + -0.07, + 0.09 + ], + "to": [ + -0.695, + -1.91 + ], + "size": [ + 2.13, + 6 + ], + "was": [ + 2.1, + 6.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "91:371", + "page": 91, + "line": 10, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 23.4, + 357.9, + 5.6, + 3.1 + ], + "drawn": [ + 22.838, + 357.213, + 5.6, + 3.1 + ], + "settled": [ + 22.86, + 357.195, + 5.6, + 3.07 + ], + "hand": [ + 0.022, + -0.017 + ], + "to": [ + -0.54, + -0.705 + ], + "size": [ + 5.6, + 3.07 + ], + "was": [ + 5.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "117:645", + "page": 117, + "line": 15, + "name": "sukun", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 218.5, + 515.4, + 4.4, + 3 + ], + "drawn": [ + 218.125, + 514.525, + 4.4, + 3 + ], + "settled": [ + 218.19, + 514.44, + 4.4, + 2.8 + ], + "hand": [ + 0.065, + -0.085 + ], + "to": [ + -0.31, + -0.96 + ], + "size": [ + 4.4, + 2.8 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "146:297", + "page": 146, + "line": 9, + "name": "superscript alef", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 27, + 307.3, + 2.1, + 6 + ], + "drawn": [ + 26.063, + 306.3, + 2.1, + 6 + ], + "settled": [ + 26.01, + 306.345, + 2, + 5.87 + ], + "hand": [ + -0.052, + 0.045 + ], + "to": [ + -0.99, + -0.955 + ], + "size": [ + 2, + 5.87 + ], + "was": [ + 2.1, + 6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "239:556", + "page": 239, + "line": 14, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 326.5, + 501.5, + 7, + 3.6 + ], + "drawn": [ + 326, + 500, + 7, + 3.6 + ], + "settled": [ + 326.155, + 499.805, + 6.93, + 3.47 + ], + "hand": [ + 0.155, + -0.195 + ], + "to": [ + -0.345, + -1.695 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "251:538", + "page": 251, + "line": 13, + "name": "shadda", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 164.1, + 447.5, + 4.3, + 3.3 + ], + "drawn": [ + 163.663, + 446.75, + 4.3, + 3.3 + ], + "settled": [ + 163.555, + 446.75, + 4.19, + 3.2 + ], + "hand": [ + -0.108, + 0 + ], + "to": [ + -0.545, + -0.75 + ], + "size": [ + 4.19, + 3.2 + ], + "was": [ + 4.3, + 3.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "277:407", + "page": 277, + "line": 12, + "name": "damma", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 35.6, + 410.6, + 5.3, + 6.6 + ], + "drawn": [ + 35.163, + 409.413, + 5.3, + 6.6 + ], + "settled": [ + 35.215, + 409.35, + 5.21, + 6.62 + ], + "hand": [ + 0.052, + -0.062 + ], + "to": [ + -0.385, + -1.25 + ], + "size": [ + 5.21, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "310:183", + "page": 310, + "line": 5, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 70.1, + 156.6, + 6.3, + 3.4 + ], + "drawn": [ + 69.163, + 155.413, + 6.3, + 3.4 + ], + "settled": [ + 69.115, + 155.315, + 6.27, + 3.33 + ], + "hand": [ + -0.047, + -0.097 + ], + "to": [ + -0.985, + -1.285 + ], + "size": [ + 6.27, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "326:30", + "page": 326, + "line": 1, + "name": "wasla", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 65.6, + 13.9, + 5.3, + 4.3 + ], + "drawn": [ + 64.6, + 11.65, + 5.3, + 4.3 + ], + "settled": [ + 64.57, + 11.605, + 5.24, + 4.27 + ], + "hand": [ + -0.03, + -0.045 + ], + "to": [ + -1.03, + -2.295 + ], + "size": [ + 5.24, + 4.27 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "347:102", + "page": 347, + "line": 3, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 64.8, + 86.4, + 7, + 3.6 + ], + "drawn": [ + 63.988, + 85.088, + 7, + 3.6 + ], + "settled": [ + 64.165, + 84.885, + 6.93, + 3.47 + ], + "hand": [ + 0.178, + -0.202 + ], + "to": [ + -0.635, + -1.515 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "350:244", + "page": 350, + "line": 8, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 301.7, + 269.3, + 6.9, + 3.6 + ], + "drawn": [ + 301.138, + 268.8, + 6.9, + 3.6 + ], + "settled": [ + 301.135, + 268.745, + 6.93, + 3.47 + ], + "hand": [ + -0.002, + -0.055 + ], + "to": [ + -0.565, + -0.555 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 6.9, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "366:77", + "page": 366, + "line": 3, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 215.6, + 89.3, + 7, + 3.5 + ], + "drawn": [ + 214.35, + 87.738, + 7, + 3.5 + ], + "settled": [ + 214.295, + 87.735, + 6.93, + 3.47 + ], + "hand": [ + -0.055, + -0.002 + ], + "to": [ + -1.305, + -1.565 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "401:119", + "page": 401, + "line": 4, + "name": "sukun", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 116.6, + 125.7, + 4.4, + 3 + ], + "drawn": [ + 116.1, + 124.763, + 4.4, + 3 + ], + "settled": [ + 116.135, + 124.645, + 4.27, + 2.93 + ], + "hand": [ + 0.035, + -0.117 + ], + "to": [ + -0.465, + -1.055 + ], + "size": [ + 4.27, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "402:26", + "page": 402, + "line": 1, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 114.4, + 6.8, + 7, + 3.6 + ], + "drawn": [ + 114.025, + 5.988, + 7, + 3.6 + ], + "settled": [ + 114.095, + 5.955, + 7.07, + 3.47 + ], + "hand": [ + 0.07, + -0.032 + ], + "to": [ + -0.305, + -0.845 + ], + "size": [ + 7.07, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "415:35", + "page": 415, + "line": 4, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 279.2, + 127.1, + 6.3, + 3.4 + ], + "drawn": [ + 277.513, + 125.288, + 6.3, + 3.4 + ], + "settled": [ + 277.445, + 125.315, + 6.13, + 3.33 + ], + "hand": [ + -0.067, + 0.028 + ], + "to": [ + -1.755, + -1.785 + ], + "size": [ + 6.13, + 3.33 + ], + "was": [ + 6.3, + 3.4 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "440:354", + "page": 440, + "line": 13, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 301.3, + 439.9, + 7, + 3.6 + ], + "drawn": [ + 300.238, + 438.588, + 7, + 3.6 + ], + "settled": [ + 300.625, + 438.375, + 7.07, + 3.53 + ], + "hand": [ + 0.387, + -0.212 + ], + "to": [ + -0.675, + -1.525 + ], + "size": [ + 7.07, + 3.53 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "458:141", + "page": 458, + "line": 6, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 218.6, + 198.1, + 7.8, + 3.7 + ], + "drawn": [ + 217.913, + 197.163, + 7.8, + 3.7 + ], + "settled": [ + 217.755, + 197.16, + 7.73, + 3.6 + ], + "hand": [ + -0.157, + -0.002 + ], + "to": [ + -0.845, + -0.94 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "460:20", + "page": 460, + "line": 1, + "name": "wasla", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 214.2, + 13.2, + 5.3, + 4.3 + ], + "drawn": [ + 213.888, + 12.138, + 5.3, + 4.3 + ], + "settled": [ + 213.9, + 12.065, + 5.24, + 4.13 + ], + "hand": [ + 0.013, + -0.073 + ], + "to": [ + -0.3, + -1.135 + ], + "size": [ + 5.24, + 4.13 + ], + "was": [ + 5.3, + 4.3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "470:203", + "page": 470, + "line": 6, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 144.5, + 209.7, + 4, + 3.9 + ], + "drawn": [ + 143.625, + 208.638, + 4, + 3.9 + ], + "settled": [ + 143.66, + 208.595, + 4, + 3.73 + ], + "hand": [ + 0.035, + -0.042 + ], + "to": [ + -0.84, + -1.105 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "491:555", + "page": 491, + "line": 15, + "name": "damma", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 254.6, + 513.9, + 5.3, + 6.6 + ], + "drawn": [ + 253.975, + 513.15, + 5.3, + 6.6 + ], + "settled": [ + 253.96, + 513.185, + 5.3, + 6.67 + ], + "hand": [ + -0.015, + 0.035 + ], + "to": [ + -0.64, + -0.715 + ], + "size": [ + 5.3, + 6.67 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "538:603", + "page": 538, + "line": 15, + "name": "successive dammatan", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 27.5, + 516.5, + 8.2, + 6.4 + ], + "drawn": [ + 27.063, + 515.75, + 8.2, + 6.4 + ], + "settled": [ + 27, + 515.71, + 8.1, + 6.36 + ], + "hand": [ + -0.062, + -0.04 + ], + "to": [ + -0.5, + -0.79 + ], + "size": [ + 8.1, + 6.36 + ], + "was": [ + 8.2, + 6.4 + ], + "goes": 2, + "reshapes": 2, + "notes": [] + }, + { + "id": "552:393", + "page": 552, + "line": 9, + "name": "sukun", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 252.3, + 304.7, + 4.4, + 3 + ], + "drawn": [ + 251.113, + 303.513, + 4.4, + 3 + ], + "settled": [ + 251.12, + 303.415, + 4.4, + 2.87 + ], + "hand": [ + 0.008, + -0.097 + ], + "to": [ + -1.18, + -1.285 + ], + "size": [ + 4.4, + 2.87 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "552:438", + "page": 552, + "line": 10, + "name": "damma", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 258.6, + 330.6, + 5.3, + 6.6 + ], + "drawn": [ + 257.975, + 329.913, + 5.3, + 6.6 + ], + "settled": [ + 257.875, + 330.02, + 5.21, + 6.62 + ], + "hand": [ + -0.1, + 0.108 + ], + "to": [ + -0.725, + -0.58 + ], + "size": [ + 5.21, + 6.62 + ], + "was": [ + 5.3, + 6.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "567:234", + "page": 567, + "line": 6, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 165.4, + 198.3, + 5.9, + 3.2 + ], + "drawn": [ + 164.213, + 196.863, + 5.9, + 3.2 + ], + "settled": [ + 164.245, + 196.81, + 5.73, + 3.2 + ], + "hand": [ + 0.032, + -0.052 + ], + "to": [ + -1.155, + -1.49 + ], + "size": [ + 5.73, + 3.2 + ], + "was": [ + 5.9, + 3.2 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "577:363", + "page": 577, + "line": 11, + "name": "hamza", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 33.5, + 390.6, + 4, + 3.9 + ], + "drawn": [ + 33.125, + 389.788, + 4, + 3.9 + ], + "settled": [ + 33.13, + 389.785, + 4, + 3.73 + ], + "hand": [ + 0.005, + -0.002 + ], + "to": [ + -0.37, + -0.815 + ], + "size": [ + 4, + 3.73 + ], + "was": [ + 4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "580:128", + "page": 580, + "line": 4, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 277.1, + 143.5, + 7, + 3.6 + ], + "drawn": [ + 276.663, + 142.75, + 7, + 3.6 + ], + "settled": [ + 276.685, + 142.615, + 6.93, + 3.47 + ], + "hand": [ + 0.023, + -0.135 + ], + "to": [ + -0.415, + -0.885 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "590:145", + "page": 590, + "line": 7, + "name": "sukun", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 324.4, + 232.5, + 4.4, + 3 + ], + "drawn": [ + 323.588, + 231.188, + 4.4, + 3 + ], + "settled": [ + 323.66, + 231.045, + 4.4, + 2.93 + ], + "hand": [ + 0.073, + -0.143 + ], + "to": [ + -0.74, + -1.455 + ], + "size": [ + 4.4, + 2.93 + ], + "was": [ + 4.4, + 3 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + } + ] +} diff --git a/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.95_1.01.seed23.settled.json b/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.95_1.01.seed23.settled.json new file mode 100644 index 00000000..bac7b5bf --- /dev/null +++ b/docs/validation/rulings/2026-08-20-placement-what-kind-of-wrong-placed.b0.95_1.01.seed23.settled.json @@ -0,0 +1,1500 @@ +{ + "built": "mark-settled", + "settledAt": "2026-08-20T02:50:05.376Z", + "rows": "out/mark-rows.line-tilt.json", + "rowsFingerprint": "5442d961", + "set": "placed", + "seed": 23, + "radius": 3, + "iouFloor": 0.55, + "sittings": [ + { + "file": "mark-report-placed-b0.95_1.01-a8edff256.23.json", + "slice": "-b0.95_1.01-a8edff256", + "finished": "2026-08-20T02:40:21.271Z", + "shown": 30, + "seen": 30, + "looked": 30, + "whole": true, + "answers": 60, + "spoke": 30 + } + ], + "banked": [], + "marks": 30, + "faulted": 30, + "defects": 0, + "answers": 60, + "settledMarks": [ + { + "id": "10:192", + "page": 10, + "line": 6, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 228.2, + 190, + 7.8, + 3.7 + ], + "drawn": [ + 227.075, + 188.25, + 7.8, + 3.7 + ], + "settled": [ + 227.085, + 188.23, + 7.73, + 3.6 + ], + "hand": [ + 0.01, + -0.02 + ], + "to": [ + -1.115, + -1.77 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "23:149", + "page": 23, + "line": 5, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 320.4, + 158.3, + 8.5, + 3.9 + ], + "drawn": [ + 319.338, + 157.238, + 8.5, + 3.9 + ], + "settled": [ + 319.34, + 157.195, + 8.4, + 3.87 + ], + "hand": [ + 0.002, + -0.042 + ], + "to": [ + -1.06, + -1.105 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "40:125", + "page": 40, + "line": 4, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 194.1, + 121.2, + 7, + 3.6 + ], + "drawn": [ + 192.163, + 119.7, + 7, + 3.6 + ], + "settled": [ + 192.155, + 119.685, + 6.93, + 3.47 + ], + "hand": [ + -0.008, + -0.015 + ], + "to": [ + -1.945, + -1.515 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "58:442", + "page": 58, + "line": 12, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 103.8, + 411.5, + 7, + 3.6 + ], + "drawn": [ + 102.988, + 410.5, + 7, + 3.6 + ], + "settled": [ + 102.955, + 410.475, + 6.93, + 3.47 + ], + "hand": [ + -0.032, + -0.025 + ], + "to": [ + -0.845, + -1.025 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "81:379", + "page": 81, + "line": 13, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 307.9, + 447, + 8.4, + 3.9 + ], + "drawn": [ + 306.9, + 445.875, + 8.4, + 3.9 + ], + "settled": [ + 306.99, + 445.855, + 8.4, + 3.87 + ], + "hand": [ + 0.09, + -0.02 + ], + "to": [ + -0.91, + -1.145 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "88:176", + "page": 88, + "line": 5, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 20.3, + 160.3, + 8.5, + 3.9 + ], + "drawn": [ + 18.863, + 158.238, + 8.5, + 3.9 + ], + "settled": [ + 18.81, + 158.265, + 8.4, + 3.87 + ], + "hand": [ + -0.052, + 0.027 + ], + "to": [ + -1.49, + -2.035 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "171:122", + "page": 171, + "line": 4, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 197.7, + 122.6, + 7, + 3.6 + ], + "drawn": [ + 197.2, + 121.6, + 7, + 3.6 + ], + "settled": [ + 197.225, + 121.545, + 6.93, + 3.47 + ], + "hand": [ + 0.025, + -0.055 + ], + "to": [ + -0.475, + -1.055 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "171:326", + "page": 171, + "line": 11, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 256.5, + 377.1, + 7, + 3.6 + ], + "drawn": [ + 255.875, + 376.35, + 7, + 3.6 + ], + "settled": [ + 255.895, + 376.345, + 6.93, + 3.47 + ], + "hand": [ + 0.02, + -0.005 + ], + "to": [ + -0.605, + -0.755 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "172:319", + "page": 172, + "line": 9, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 242.4, + 305.6, + 7.8, + 3.7 + ], + "drawn": [ + 241.338, + 303.788, + 7.8, + 3.7 + ], + "settled": [ + 241.355, + 303.83, + 7.73, + 3.6 + ], + "hand": [ + 0.018, + 0.042 + ], + "to": [ + -1.045, + -1.77 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "173:454", + "page": 173, + "line": 12, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 168.4, + 412.5, + 7.8, + 3.7 + ], + "drawn": [ + 167.4, + 411.063, + 7.8, + 3.7 + ], + "settled": [ + 167.355, + 411.02, + 7.73, + 3.6 + ], + "hand": [ + -0.045, + -0.042 + ], + "to": [ + -1.045, + -1.48 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "184:460", + "page": 184, + "line": 13, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 161, + 445.8, + 7, + 3.6 + ], + "drawn": [ + 160.25, + 444.613, + 7, + 3.6 + ], + "settled": [ + 160.295, + 444.605, + 6.93, + 3.47 + ], + "hand": [ + 0.045, + -0.008 + ], + "to": [ + -0.705, + -1.195 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "187:344", + "page": 187, + "line": 10, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 277.6, + 361.8, + 7, + 3.6 + ], + "drawn": [ + 277.413, + 360.363, + 7, + 3.6 + ], + "settled": [ + 277.355, + 360.345, + 6.93, + 3.47 + ], + "hand": [ + -0.057, + -0.018 + ], + "to": [ + -0.245, + -1.455 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "198:372", + "page": 198, + "line": 11, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 142.2, + 373.7, + 8.5, + 3.9 + ], + "drawn": [ + 139.075, + 372.263, + 8.5, + 3.9 + ], + "settled": [ + 139.015, + 372.255, + 8.27, + 3.87 + ], + "hand": [ + -0.06, + -0.008 + ], + "to": [ + -3.185, + -1.445 + ], + "size": [ + 8.27, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "203:511", + "page": 203, + "line": 14, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 180.1, + 481.7, + 8.5, + 3.9 + ], + "drawn": [ + 179.725, + 480.888, + 8.5, + 3.9 + ], + "settled": [ + 179.61, + 480.855, + 8.4, + 3.73 + ], + "hand": [ + -0.115, + -0.032 + ], + "to": [ + -0.49, + -0.845 + ], + "size": [ + 8.4, + 3.73 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "239:114", + "page": 239, + "line": 3, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 201.6, + 84.1, + 7, + 3.6 + ], + "drawn": [ + 201.225, + 82.663, + 7, + 3.6 + ], + "settled": [ + 201.225, + 82.615, + 6.93, + 3.47 + ], + "hand": [ + 0, + -0.048 + ], + "to": [ + -0.375, + -1.485 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "242:346", + "page": 242, + "line": 10, + "name": "maddah", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 284.1, + 346.8, + 6.6, + 3.1 + ], + "drawn": [ + 283.475, + 345.863, + 6.6, + 3.1 + ], + "settled": [ + 283.555, + 345.855, + 6.67, + 3.07 + ], + "hand": [ + 0.08, + -0.007 + ], + "to": [ + -0.545, + -0.945 + ], + "size": [ + 6.67, + 3.07 + ], + "was": [ + 6.6, + 3.1 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "243:181", + "page": 243, + "line": 6, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 279.4, + 193.2, + 7, + 3.6 + ], + "drawn": [ + 277.838, + 191.638, + 7, + 3.6 + ], + "settled": [ + 277.895, + 191.545, + 6.93, + 3.47 + ], + "hand": [ + 0.058, + -0.093 + ], + "to": [ + -1.505, + -1.655 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "313:190", + "page": 313, + "line": 6, + "name": "kasra", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 283.9, + 218.5, + 7, + 3.6 + ], + "drawn": [ + 283.4, + 217.75, + 7, + 3.6 + ], + "settled": [ + 283.355, + 217.675, + 6.93, + 3.47 + ], + "hand": [ + -0.045, + -0.075 + ], + "to": [ + -0.545, + -0.825 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "313:361", + "page": 313, + "line": 11, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 244.6, + 376.9, + 8.4, + 3.9 + ], + "drawn": [ + 244.038, + 375.963, + 8.4, + 3.9 + ], + "settled": [ + 244.06, + 375.995, + 8.4, + 3.87 + ], + "hand": [ + 0.022, + 0.032 + ], + "to": [ + -0.54, + -0.905 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.4, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "352:601", + "page": 352, + "line": 15, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 109.9, + 511.7, + 7.8, + 3.7 + ], + "drawn": [ + 109.338, + 510.888, + 7.8, + 3.7 + ], + "settled": [ + 109.355, + 510.89, + 7.73, + 3.6 + ], + "hand": [ + 0.017, + 0.002 + ], + "to": [ + -0.545, + -0.81 + ], + "size": [ + 7.73, + 3.6 + ], + "was": [ + 7.8, + 3.7 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "354:320", + "page": 354, + "line": 8, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 104, + 266.2, + 7, + 3.5 + ], + "drawn": [ + 103.063, + 264.638, + 7, + 3.5 + ], + "settled": [ + 103.095, + 264.665, + 6.93, + 3.47 + ], + "hand": [ + 0.032, + 0.028 + ], + "to": [ + -0.905, + -1.535 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.5 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "359:255", + "page": 359, + "line": 7, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 139.9, + 229.7, + 7, + 3.6 + ], + "drawn": [ + 138.588, + 228.638, + 7, + 3.6 + ], + "settled": [ + 138.565, + 228.615, + 6.93, + 3.47 + ], + "hand": [ + -0.022, + -0.022 + ], + "to": [ + -1.335, + -1.085 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "388:381", + "page": 388, + "line": 11, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 84.1, + 374, + 8.5, + 3.9 + ], + "drawn": [ + 82.475, + 372.25, + 8.5, + 3.9 + ], + "settled": [ + 82.41, + 372.22, + 8.4, + 3.8 + ], + "hand": [ + -0.065, + -0.03 + ], + "to": [ + -1.69, + -1.78 + ], + "size": [ + 8.4, + 3.8 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "389:218", + "page": 389, + "line": 7, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 320.5, + 233.2, + 8.5, + 3.9 + ], + "drawn": [ + 319.938, + 232.7, + 8.5, + 3.9 + ], + "settled": [ + 319.87, + 232.725, + 8.4, + 3.73 + ], + "hand": [ + -0.067, + 0.025 + ], + "to": [ + -0.63, + -0.475 + ], + "size": [ + 8.4, + 3.73 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "432:66", + "page": 432, + "line": 2, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 74.6, + 56.3, + 7, + 3.6 + ], + "drawn": [ + 73.663, + 54.863, + 7, + 3.6 + ], + "settled": [ + 73.625, + 54.885, + 6.93, + 3.47 + ], + "hand": [ + -0.037, + 0.022 + ], + "to": [ + -0.975, + -1.415 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "500:479", + "page": 500, + "line": 13, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 204.7, + 442.9, + 8.5, + 3.9 + ], + "drawn": [ + 203.7, + 441.713, + 8.5, + 3.9 + ], + "settled": [ + 203.61, + 441.725, + 8.4, + 3.87 + ], + "hand": [ + -0.09, + 0.012 + ], + "to": [ + -1.09, + -1.175 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "517:608", + "page": 517, + "line": 15, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 92.2, + 522.5, + 8.5, + 3.9 + ], + "drawn": [ + 91.763, + 521.563, + 8.5, + 3.9 + ], + "settled": [ + 91.61, + 521.585, + 8.4, + 3.87 + ], + "hand": [ + -0.152, + 0.022 + ], + "to": [ + -0.59, + -0.915 + ], + "size": [ + 8.4, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "538:133", + "page": 538, + "line": 4, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 223.3, + 124.6, + 8.5, + 3.9 + ], + "drawn": [ + 222.238, + 122.6, + 8.5, + 3.9 + ], + "settled": [ + 222.205, + 122.535, + 8.53, + 3.87 + ], + "hand": [ + -0.032, + -0.065 + ], + "to": [ + -1.095, + -2.065 + ], + "size": [ + 8.53, + 3.87 + ], + "was": [ + 8.5, + 3.9 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "580:275", + "page": 580, + "line": 10, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 273.4, + 337.5, + 7.1, + 3.6 + ], + "drawn": [ + 272.275, + 336.5, + 7.1, + 3.6 + ], + "settled": [ + 272.235, + 336.475, + 6.93, + 3.47 + ], + "hand": [ + -0.04, + -0.025 + ], + "to": [ + -1.165, + -1.025 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7.1, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + }, + { + "id": "594:335", + "page": 594, + "line": 12, + "name": "fatha", + "rule": "ink", + "words": [ + "placement", + "wrong-shape" + ], + "fault": true, + "box": [ + 213.8, + 412.6, + 7, + 3.6 + ], + "drawn": [ + 213.3, + 411.725, + 7, + 3.6 + ], + "settled": [ + 213.355, + 411.675, + 6.93, + 3.47 + ], + "hand": [ + 0.055, + -0.05 + ], + "to": [ + -0.445, + -0.925 + ], + "size": [ + 6.93, + 3.47 + ], + "was": [ + 7, + 3.6 + ], + "goes": 1, + "reshapes": 1, + "notes": [] + } + ] +} diff --git a/docs/validation/rulings/README.md b/docs/validation/rulings/README.md new file mode 100644 index 00000000..40fe0ea2 --- /dev/null +++ b/docs/validation/rulings/README.md @@ -0,0 +1,168 @@ +# Rulings + +The answers themselves. One file per sitting where a person was asked a question over and +over and a tool wrote down what they said — the input a scorer needs to arrive at a verdict +a second time, on a different machine, months later. + +``` +docs/validation/rulings/<when>-<check-id>.seed<N>.json +docs/validation/rulings/<when>-<check-id>.seed<N>.reader<X>.json # when two people sat it +docs/validation/rulings/<when>-<check-id>-<population>.seed<N>.settled.json # one row per mark +``` + +## Why these are here and not in a downloads folder + +A session page hands its answers back as a download, because that is the only thing a page +opened from a file can do. That is where they stayed, which meant the numbers banked in +`../ledger.json` were reproducible by exactly one person on exactly one laptop until the +browser cleared it. A verdict whose working cannot be re-run is a verdict nobody can argue +with, and this project's whole shape is built on being able to argue with one. + +So the answers come home, the same way the transcript beside them does. + +## The three directories, and which is which + +| directory | what it holds | +| --- | --- | +| `../sessions/*.jsonl` | the transcript — what a person did, step by step, as it happened | +| `../evidence/<id>.json` | a machine run's exit code, written by `make validate-auto` | +| `rulings/` | the answers themselves — the input a scorer re-reads to reproduce a verdict | + +A transcript says *a sitting happened and here is how it went*. A ruling says *here is what +was answered*. They pair by stem: `2026-08-12T1650-placement-residual-by-hand.jsonl` is the +sitting, `2026-08-12T1650-placement-residual-by-hand.seed23.json` is what came out of it. + +## Why the seed is in the name + +The trials are not stored in the file — only the answers, keyed by index and trial id. The +seed is what rebuilds the trial list, so the seed is what makes an answer mean anything. A +ruling separated from its seed is a column of numbers. + +The file a session hands back carries the seed in *its* name too, and a narrowed session adds +the page fingerprint after it. That is not the naming convention above — it is what lands in a +downloads folder before anybody renames it — and it exists because two sittings under one seed +that asked about different pages would otherwise arrive as the same file, where the browser +appends a number and nothing on the outside of either says which is which. + +The file also carries the fingerprint of the displacements it was placed against +(`shiftFingerprint`), and both scorers refuse to read a ruling against a different set. That +is deliberate: silently scoring yesterday's answers against today's measurement is the one +way this could go wrong without anybody noticing. + +A session built on a **subset** of pages — either kind — carries a second thing: the ordered +list of pages it was allowed to draw from, and a fingerprint of that list (`select`). The +scorer replays that list instead of choosing again, for the reason the seed exists at all — the +trial list is rebuilt, not stored, and a rebuild that narrowed the pages differently would put +every trial index against a different mark while nothing threw. It refuses on that fingerprint +too, and refuses a third time if the displacements it is handed carry no row for a page the +sitting used. That last check is not redundant with the first: a file can carry the right +fingerprint and still be the wrong file for these pages, and the error message says so in those +words. + +Both kinds can be narrowed, and they narrow differently on purpose. A placing session wants the +pages where the proposed move is largest and smallest, because it is estimating *how far* and +that is bought with leverage. A forced choice only ever asks which of two rectangles is closer, +so it takes an even walk through the print instead: it has no leverage to gain and it would +lose the one thing its headline claims, which is that the number is about the mus'haf rather +than about its strangest pages. + +## Why some of them also name a person + +A placing session says how far a rectangle is from where somebody put it, and that number has +two readings it cannot separate on its own: *the print is out by this much*, and *this person +puts rectangles this way*. Only a second person working the identical session — the same marks, +the same order, the same starting points — tells those apart, so the session records **whose +hand it was**, and when there are two of them the name goes in the file name after the seed: + +``` +2026-08-13T…-placement-holds-off-its-own-pages.seed31.readerB.json +``` + +That is not decoration. Two people sitting the same build otherwise produce two files with the +same name, and a half-finished sitting resumes out of whichever answers the browser saw last — +the second reader would resume into the first one's work and never know. Scoring one against the +other is `--against`, and it refuses two sittings that were not the same build, or two by the +same person, which compares a hand with itself and returns a beautiful number about nothing. + +## Reading one + +``` +pnpm nudge:score docs/validation/rulings/<file> # the placing sessions +pnpm adjudicate:score docs/validation/rulings/<file> # the forced choices +``` + +Both take `--shift <path>` when the displacements they were made against are not the ones +sitting in `packages/etl/out/`. + +## The settled ones, and why the route is thrown away + +A `.settled.json` is a third kind, and it is the only one in this directory that is not the raw +answers. The sittings where a person can move a rectangle record every press: a mark pushed left, +back right, left again and finally let go arrives as four statements about one mark. That list is +the **route**. What the reader said is the **resting place**, and settling collapses one to the +other — position and size take the last thing the reader did, the words they used gather and +repeat once, and what the route leaves behind is kept only as a count. + +Reading the route instead is not a subtle error and it has already cost this project a number: +averaging the presses says a mark never moved, and adding up their sizes says it moved twice as +far as it did. Neither reads as wrong. So the settled file is what anything downstream quotes, +and the transcript stays beside it for anybody who wants to check the collapse. + +``` +pnpm report:settle packages/etl/out/<transcript>.json --rows packages/etl/out/<displacements>.json +``` + +Pass the running log the serving side keeps as well, whenever there is one. There are two ways +an answer can leave a sitting — banked as it is given, or written into a file when the reader +hands over — and a mark answered either way is taken off the screen for good. So an answer that +was banked and never handed over is work nobody can see was done: on the first sitting over the +marks we could not place from ink, that was twenty-five marks. The same answer usually arrives +by both routes and is counted once, so nothing is inflated by reading both. + +Two things about the name. It carries the **population** the sitting was drawn from, because the +marks placed from their own printed ink and the marks that fell back to the printed line are two +different options being decided between, and a rate over both together is a fact about whichever +mix was drawn rather than about the print. And the date is the **sitting's**, never the day the +settling was run — a file re-settled next month is about the same hour it was always about, and +stamping it with today would file one hour under two dates. + +The `review-sitting` skill is the whole route from a handed-over transcript to this file and on +into the registers that own what it found. + +## The displacements, beside the answers that were given about them + +A ruling on its own is still not re-scorable. It says where a reader put a rectangle; it does +not say what rectangle they were shown, and that came from a file of per-page displacements +which is derived, lands in `packages/etl/out/`, and is ignored wholesale because of what else +lands there. So a committed ruling was auditable only by somebody who could first rebuild the +measurement — which is most of the way back to a verdict nobody can argue with. + +Those files are now here too. They are the smallest thing that closes the gap: a page number +and two offsets per row, no ink, no scripture, and nothing a person typed. + +| file | pages | what it is | +| --- | --- | --- | +| `mark-shift.40pages.c8528da9.json` | 40 of 604 | what the placing session and the forced choice were both built from | +| `mark-shift.604pages.c849e72d.json` | 600 of 604 | the full pass, 2026-08-12 — the input for anything built after | + +**The fingerprint is in the name on purpose.** A ruling pins the displacements it was placed +against, and the scorers refuse to read it against any others; putting that same hex in the +filename means the pairing is legible before either file is opened, rather than being a thing +you discover from an exit code. So the sitting of 2026-08-12 re-scores, from this directory +and nothing else: + +``` +pnpm nudge:score docs/validation/rulings/2026-08-12T1650-placement-residual-by-hand.seed23.json \ + --shift docs/validation/rulings/mark-shift.40pages.c8528da9.json +``` + +The 604-page file is not what that sitting was asked about and must never be substituted into +it — the scorer will refuse, but the reason is worth knowing anyway: it would be scoring one +day's answers against another day's measurement, which is the single failure this whole +naming exists to make loud. + +## Are these committed? + +Yes. They contain no Qur'anic text: a ruling is page numbers, mark indices, offsets in page +units, and millisecond timings. No field can carry anything about the person who sat the +session except how long they took. diff --git a/docs/validation/rulings/mark-shift.40pages.c8528da9.json b/docs/validation/rulings/mark-shift.40pages.c8528da9.json new file mode 100644 index 00000000..c15e4a2d --- /dev/null +++ b/docs/validation/rulings/mark-shift.40pages.c8528da9.json @@ -0,0 +1,251 @@ +{ + "ran": "2026-08-12", + "from": "probe-mark-ink.mjs", + "how": "the median displacement of that page's marks, measured against the ink the app ships", + "units": "page units of the shipped page frame; add these to a claimed rectangle to correct it", + "seed": 7, + "sampled": 4000, + "minMarksPerPage": 20, + "shifts": [ + { + "page": 8, + "dx": -0.6875, + "dy": -1.0625, + "n": 110 + }, + { + "page": 24, + "dx": -0.625, + "dy": -1.125, + "n": 109 + }, + { + "page": 39, + "dx": -0.5625, + "dy": -1, + "n": 91 + }, + { + "page": 45, + "dx": -0.625, + "dy": -0.9375, + "n": 102 + }, + { + "page": 86, + "dx": -0.875, + "dy": -1.125, + "n": 101 + }, + { + "page": 93, + "dx": -0.5625, + "dy": -0.875, + "n": 87 + }, + { + "page": 95, + "dx": -0.5625, + "dy": -0.9375, + "n": 100 + }, + { + "page": 107, + "dx": -0.5625, + "dy": -0.875, + "n": 109 + }, + { + "page": 115, + "dx": -0.625, + "dy": -1, + "n": 84 + }, + { + "page": 120, + "dx": -0.6875, + "dy": -0.9375, + "n": 93 + }, + { + "page": 146, + "dx": -0.75, + "dy": -0.9375, + "n": 78 + }, + { + "page": 150, + "dx": -0.75, + "dy": -1.1875, + "n": 107 + }, + { + "page": 157, + "dx": -0.5625, + "dy": -1, + "n": 95 + }, + { + "page": 179, + "dx": -0.75, + "dy": -0.9375, + "n": 111 + }, + { + "page": 180, + "dx": -0.875, + "dy": -1.0625, + "n": 105 + }, + { + "page": 181, + "dx": -0.5625, + "dy": -0.8125, + "n": 83 + }, + { + "page": 218, + "dx": -0.625, + "dy": -1.0625, + "n": 110 + }, + { + "page": 223, + "dx": -0.5625, + "dy": -0.9375, + "n": 109 + }, + { + "page": 245, + "dx": -0.625, + "dy": -1.0625, + "n": 104 + }, + { + "page": 282, + "dx": -0.875, + "dy": -1.125, + "n": 81 + }, + { + "page": 314, + "dx": -0.75, + "dy": -1, + "n": 105 + }, + { + "page": 315, + "dx": -0.5625, + "dy": -0.9375, + "n": 125 + }, + { + "page": 326, + "dx": -0.625, + "dy": -1.125, + "n": 94 + }, + { + "page": 336, + "dx": -0.875, + "dy": -1.0625, + "n": 108 + }, + { + "page": 337, + "dx": -0.625, + "dy": -0.9375, + "n": 112 + }, + { + "page": 340, + "dx": -0.8125, + "dy": -1, + "n": 97 + }, + { + "page": 379, + "dx": -0.625, + "dy": -0.9375, + "n": 125 + }, + { + "page": 380, + "dx": -0.625, + "dy": -1, + "n": 89 + }, + { + "page": 414, + "dx": -0.75, + "dy": -1.1875, + "n": 102 + }, + { + "page": 423, + "dx": -0.75, + "dy": -1, + "n": 112 + }, + { + "page": 443, + "dx": -0.5625, + "dy": -1, + "n": 95 + }, + { + "page": 463, + "dx": -0.75, + "dy": -0.9375, + "n": 88 + }, + { + "page": 464, + "dx": -0.75, + "dy": -1, + "n": 103 + }, + { + "page": 501, + "dx": -0.625, + "dy": -0.9375, + "n": 114 + }, + { + "page": 531, + "dx": -0.625, + "dy": -0.9375, + "n": 76 + }, + { + "page": 552, + "dx": -0.625, + "dy": -1, + "n": 117 + }, + { + "page": 557, + "dx": -0.5, + "dy": -0.9375, + "n": 90 + }, + { + "page": 583, + "dx": -0.5, + "dy": -0.9375, + "n": 83 + }, + { + "page": 591, + "dx": -0.875, + "dy": -0.9375, + "n": 72 + }, + { + "page": 592, + "dx": -0.8125, + "dy": -1.0625, + "n": 78 + } + ] +} diff --git a/docs/validation/rulings/mark-shift.604pages.c849e72d.json b/docs/validation/rulings/mark-shift.604pages.c849e72d.json new file mode 100644 index 00000000..1c394a46 --- /dev/null +++ b/docs/validation/rulings/mark-shift.604pages.c849e72d.json @@ -0,0 +1,3619 @@ +{ + "ran": "2026-08-12", + "from": "probe-mark-ink.mjs", + "how": "the median displacement of that page's marks, measured against the ink the app ships", + "units": "page units of the shipped page frame; add these to a claimed rectangle to correct it", + "seed": 7, + "sampled": 30000, + "minMarksPerPage": 20, + "coverage": { + "measured": 600, + "ofMushaf": 604, + "pct": 99.3, + "pagesOpened": 604, + "pagesInCache": 604, + "note": "a page with no row below has no measured correction; it was not looked at, not found to be right" + }, + "shifts": [ + { + "page": 3, + "dx": -0.625, + "dy": -0.875, + "n": 71 + }, + { + "page": 4, + "dx": -0.75, + "dy": -1.3125, + "n": 54 + }, + { + "page": 5, + "dx": -0.625, + "dy": -1.0625, + "n": 45 + }, + { + "page": 6, + "dx": -0.9375, + "dy": -1.25, + "n": 61 + }, + { + "page": 7, + "dx": -0.5625, + "dy": -0.9375, + "n": 54 + }, + { + "page": 8, + "dx": -0.6875, + "dy": -1.125, + "n": 56 + }, + { + "page": 9, + "dx": -0.5625, + "dy": -0.75, + "n": 53 + }, + { + "page": 10, + "dx": -0.75, + "dy": -1.1875, + "n": 51 + }, + { + "page": 11, + "dx": -0.625, + "dy": -0.75, + "n": 54 + }, + { + "page": 12, + "dx": -0.875, + "dy": -1, + "n": 36 + }, + { + "page": 13, + "dx": -0.625, + "dy": -0.75, + "n": 43 + }, + { + "page": 14, + "dx": -0.75, + "dy": -1.1875, + "n": 40 + }, + { + "page": 15, + "dx": -0.6875, + "dy": -1.125, + "n": 46 + }, + { + "page": 16, + "dx": -0.8125, + "dy": -0.9375, + "n": 45 + }, + { + "page": 17, + "dx": -0.375, + "dy": -0.8125, + "n": 34 + }, + { + "page": 18, + "dx": -0.75, + "dy": -0.9375, + "n": 52 + }, + { + "page": 19, + "dx": -0.625, + "dy": -1.25, + "n": 61 + }, + { + "page": 20, + "dx": -0.8125, + "dy": -1.0625, + "n": 45 + }, + { + "page": 21, + "dx": -0.75, + "dy": -0.875, + "n": 47 + }, + { + "page": 22, + "dx": -0.625, + "dy": -1, + "n": 52 + }, + { + "page": 23, + "dx": -0.8125, + "dy": -0.8125, + "n": 64 + }, + { + "page": 24, + "dx": -0.6875, + "dy": -1.1875, + "n": 62 + }, + { + "page": 25, + "dx": -0.6875, + "dy": -0.9375, + "n": 51 + }, + { + "page": 26, + "dx": -0.9375, + "dy": -0.9375, + "n": 48 + }, + { + "page": 27, + "dx": -0.5625, + "dy": -0.9375, + "n": 57 + }, + { + "page": 28, + "dx": -0.8125, + "dy": -1, + "n": 41 + }, + { + "page": 29, + "dx": -0.6875, + "dy": -0.875, + "n": 45 + }, + { + "page": 30, + "dx": -0.6875, + "dy": -1, + "n": 69 + }, + { + "page": 31, + "dx": -0.6875, + "dy": -1, + "n": 37 + }, + { + "page": 32, + "dx": -0.9375, + "dy": -1.25, + "n": 47 + }, + { + "page": 33, + "dx": -0.625, + "dy": -1.0625, + "n": 42 + }, + { + "page": 34, + "dx": -0.5, + "dy": -1, + "n": 49 + }, + { + "page": 35, + "dx": -0.5, + "dy": -0.9375, + "n": 40 + }, + { + "page": 36, + "dx": -0.875, + "dy": -1.0625, + "n": 44 + }, + { + "page": 37, + "dx": -0.5625, + "dy": -0.875, + "n": 47 + }, + { + "page": 38, + "dx": -1.0625, + "dy": -0.875, + "n": 39 + }, + { + "page": 39, + "dx": -0.5625, + "dy": -1, + "n": 42 + }, + { + "page": 40, + "dx": -0.75, + "dy": -1.0625, + "n": 52 + }, + { + "page": 41, + "dx": -0.625, + "dy": -0.9375, + "n": 52 + }, + { + "page": 42, + "dx": -0.875, + "dy": -1.0625, + "n": 57 + }, + { + "page": 43, + "dx": -0.5, + "dy": -0.875, + "n": 44 + }, + { + "page": 44, + "dx": -0.875, + "dy": -0.9375, + "n": 43 + }, + { + "page": 45, + "dx": -0.6875, + "dy": -1, + "n": 54 + }, + { + "page": 46, + "dx": -0.9375, + "dy": -1.125, + "n": 36 + }, + { + "page": 47, + "dx": -0.625, + "dy": -0.875, + "n": 56 + }, + { + "page": 48, + "dx": -0.9375, + "dy": -0.9375, + "n": 50 + }, + { + "page": 49, + "dx": -0.5, + "dy": -0.8125, + "n": 44 + }, + { + "page": 50, + "dx": -0.9375, + "dy": -1.1875, + "n": 49 + }, + { + "page": 51, + "dx": -0.5625, + "dy": -0.875, + "n": 52 + }, + { + "page": 52, + "dx": -0.75, + "dy": -1.1875, + "n": 48 + }, + { + "page": 53, + "dx": -0.75, + "dy": -1.0625, + "n": 53 + }, + { + "page": 54, + "dx": -0.75, + "dy": -1, + "n": 59 + }, + { + "page": 55, + "dx": -0.625, + "dy": -1.0625, + "n": 55 + }, + { + "page": 56, + "dx": -0.8125, + "dy": -0.9375, + "n": 42 + }, + { + "page": 57, + "dx": -0.75, + "dy": -0.9375, + "n": 41 + }, + { + "page": 58, + "dx": -0.6875, + "dy": -1.1875, + "n": 56 + }, + { + "page": 59, + "dx": -0.625, + "dy": -1.0625, + "n": 59 + }, + { + "page": 60, + "dx": -1, + "dy": -0.9375, + "n": 50 + }, + { + "page": 61, + "dx": -0.5, + "dy": -0.875, + "n": 38 + }, + { + "page": 62, + "dx": -0.5625, + "dy": -0.9375, + "n": 62 + }, + { + "page": 63, + "dx": -0.5625, + "dy": -0.8125, + "n": 59 + }, + { + "page": 64, + "dx": -1.0625, + "dy": -1.1875, + "n": 42 + }, + { + "page": 65, + "dx": -0.625, + "dy": -0.875, + "n": 45 + }, + { + "page": 66, + "dx": -0.8125, + "dy": -0.9375, + "n": 50 + }, + { + "page": 67, + "dx": -0.625, + "dy": -0.9375, + "n": 32 + }, + { + "page": 68, + "dx": -0.8125, + "dy": -1.0625, + "n": 46 + }, + { + "page": 69, + "dx": -0.75, + "dy": -0.9375, + "n": 42 + }, + { + "page": 70, + "dx": -0.9375, + "dy": -1, + "n": 57 + }, + { + "page": 71, + "dx": -0.5625, + "dy": -0.875, + "n": 60 + }, + { + "page": 72, + "dx": -0.5625, + "dy": -0.9375, + "n": 49 + }, + { + "page": 73, + "dx": -0.75, + "dy": -1.0625, + "n": 57 + }, + { + "page": 74, + "dx": -0.6875, + "dy": -1.0625, + "n": 64 + }, + { + "page": 75, + "dx": -0.875, + "dy": -1, + "n": 46 + }, + { + "page": 76, + "dx": -0.625, + "dy": -0.8125, + "n": 44 + }, + { + "page": 77, + "dx": -0.8125, + "dy": -0.9375, + "n": 55 + }, + { + "page": 78, + "dx": -0.875, + "dy": -1, + "n": 50 + }, + { + "page": 79, + "dx": -0.75, + "dy": -1.0625, + "n": 56 + }, + { + "page": 80, + "dx": -1.0625, + "dy": -1.3125, + "n": 35 + }, + { + "page": 81, + "dx": -0.5, + "dy": -1, + "n": 39 + }, + { + "page": 82, + "dx": -0.75, + "dy": -1.0625, + "n": 58 + }, + { + "page": 83, + "dx": -0.6875, + "dy": -1.0625, + "n": 43 + }, + { + "page": 84, + "dx": -0.75, + "dy": -1.0625, + "n": 55 + }, + { + "page": 85, + "dx": -0.5625, + "dy": -0.9375, + "n": 54 + }, + { + "page": 86, + "dx": -0.8125, + "dy": -1.1875, + "n": 46 + }, + { + "page": 87, + "dx": -0.625, + "dy": -0.875, + "n": 49 + }, + { + "page": 88, + "dx": -1.125, + "dy": -0.75, + "n": 37 + }, + { + "page": 89, + "dx": -0.5625, + "dy": -0.9375, + "n": 48 + }, + { + "page": 90, + "dx": -0.75, + "dy": -1, + "n": 62 + }, + { + "page": 91, + "dx": -0.75, + "dy": -1, + "n": 62 + }, + { + "page": 92, + "dx": -0.6875, + "dy": -0.8125, + "n": 56 + }, + { + "page": 93, + "dx": -0.6875, + "dy": -0.9375, + "n": 48 + }, + { + "page": 94, + "dx": -0.625, + "dy": -0.9375, + "n": 68 + }, + { + "page": 95, + "dx": -0.5625, + "dy": -0.9375, + "n": 60 + }, + { + "page": 96, + "dx": -1, + "dy": -1.1875, + "n": 58 + }, + { + "page": 97, + "dx": -0.75, + "dy": -1, + "n": 53 + }, + { + "page": 98, + "dx": -0.625, + "dy": -1, + "n": 52 + }, + { + "page": 99, + "dx": -0.6875, + "dy": -0.875, + "n": 41 + }, + { + "page": 100, + "dx": -0.625, + "dy": -1.125, + "n": 50 + }, + { + "page": 101, + "dx": -0.625, + "dy": -0.875, + "n": 51 + }, + { + "page": 102, + "dx": -1.0625, + "dy": -1.25, + "n": 43 + }, + { + "page": 103, + "dx": -0.625, + "dy": -0.9375, + "n": 45 + }, + { + "page": 104, + "dx": -0.75, + "dy": -1.0625, + "n": 61 + }, + { + "page": 105, + "dx": -0.5625, + "dy": -0.9375, + "n": 50 + }, + { + "page": 106, + "dx": -0.75, + "dy": -0.8125, + "n": 38 + }, + { + "page": 107, + "dx": -0.625, + "dy": -0.9375, + "n": 47 + }, + { + "page": 108, + "dx": -0.75, + "dy": -1, + "n": 60 + }, + { + "page": 109, + "dx": -0.625, + "dy": -0.8125, + "n": 48 + }, + { + "page": 110, + "dx": -0.8125, + "dy": -1, + "n": 35 + }, + { + "page": 111, + "dx": -0.875, + "dy": -1.0625, + "n": 50 + }, + { + "page": 112, + "dx": -0.5625, + "dy": -1.0625, + "n": 52 + }, + { + "page": 113, + "dx": -1.75, + "dy": 1.875, + "n": 44 + }, + { + "page": 114, + "dx": -0.5, + "dy": -1, + "n": 38 + }, + { + "page": 115, + "dx": -0.625, + "dy": -1, + "n": 43 + }, + { + "page": 116, + "dx": -0.875, + "dy": -1.4375, + "n": 40 + }, + { + "page": 117, + "dx": -0.5, + "dy": -0.875, + "n": 62 + }, + { + "page": 118, + "dx": -0.5, + "dy": -0.8125, + "n": 62 + }, + { + "page": 119, + "dx": -0.5, + "dy": -0.8125, + "n": 53 + }, + { + "page": 120, + "dx": -0.75, + "dy": -0.875, + "n": 59 + }, + { + "page": 121, + "dx": -0.625, + "dy": -0.9375, + "n": 53 + }, + { + "page": 122, + "dx": -0.625, + "dy": -0.875, + "n": 55 + }, + { + "page": 123, + "dx": -0.625, + "dy": -1.0625, + "n": 46 + }, + { + "page": 124, + "dx": -0.75, + "dy": -1.0625, + "n": 61 + }, + { + "page": 125, + "dx": -0.75, + "dy": -1.0625, + "n": 54 + }, + { + "page": 126, + "dx": -0.75, + "dy": -1.0625, + "n": 59 + }, + { + "page": 127, + "dx": -0.5, + "dy": -1.0625, + "n": 48 + }, + { + "page": 128, + "dx": -0.5625, + "dy": -0.875, + "n": 45 + }, + { + "page": 129, + "dx": -0.625, + "dy": -0.9375, + "n": 50 + }, + { + "page": 130, + "dx": -0.9375, + "dy": -1.0625, + "n": 61 + }, + { + "page": 131, + "dx": -0.6875, + "dy": -0.9375, + "n": 60 + }, + { + "page": 132, + "dx": -0.6875, + "dy": -0.9375, + "n": 49 + }, + { + "page": 133, + "dx": -0.5, + "dy": -0.9375, + "n": 47 + }, + { + "page": 134, + "dx": -0.75, + "dy": -1.0625, + "n": 50 + }, + { + "page": 135, + "dx": -0.5625, + "dy": -1.0625, + "n": 58 + }, + { + "page": 136, + "dx": -0.5625, + "dy": -0.8125, + "n": 45 + }, + { + "page": 137, + "dx": -0.625, + "dy": -0.9375, + "n": 55 + }, + { + "page": 138, + "dx": -0.875, + "dy": -1.125, + "n": 46 + }, + { + "page": 139, + "dx": -0.625, + "dy": -0.875, + "n": 51 + }, + { + "page": 140, + "dx": -0.5625, + "dy": -0.9375, + "n": 52 + }, + { + "page": 141, + "dx": -0.5625, + "dy": -0.9375, + "n": 57 + }, + { + "page": 142, + "dx": -0.75, + "dy": -1, + "n": 43 + }, + { + "page": 143, + "dx": -0.6875, + "dy": -0.9375, + "n": 40 + }, + { + "page": 144, + "dx": -0.375, + "dy": -0.75, + "n": 43 + }, + { + "page": 145, + "dx": -0.5, + "dy": -0.9375, + "n": 37 + }, + { + "page": 146, + "dx": -0.75, + "dy": -0.9375, + "n": 52 + }, + { + "page": 147, + "dx": -0.625, + "dy": -0.9375, + "n": 59 + }, + { + "page": 148, + "dx": -0.875, + "dy": -1.1875, + "n": 40 + }, + { + "page": 149, + "dx": -0.625, + "dy": -0.9375, + "n": 52 + }, + { + "page": 150, + "dx": -0.8125, + "dy": -1, + "n": 53 + }, + { + "page": 151, + "dx": -0.8125, + "dy": -0.875, + "n": 35 + }, + { + "page": 152, + "dx": -0.75, + "dy": -1.125, + "n": 42 + }, + { + "page": 153, + "dx": -0.6875, + "dy": -1, + "n": 53 + }, + { + "page": 154, + "dx": -0.75, + "dy": -1.1875, + "n": 49 + }, + { + "page": 155, + "dx": -0.5, + "dy": -1, + "n": 50 + }, + { + "page": 156, + "dx": -1, + "dy": -1.0625, + "n": 67 + }, + { + "page": 157, + "dx": -0.5625, + "dy": -0.9375, + "n": 48 + }, + { + "page": 158, + "dx": -1, + "dy": -1.0625, + "n": 49 + }, + { + "page": 159, + "dx": -0.75, + "dy": -0.9375, + "n": 52 + }, + { + "page": 160, + "dx": -0.8125, + "dy": -0.9375, + "n": 38 + }, + { + "page": 161, + "dx": -0.625, + "dy": -0.9375, + "n": 41 + }, + { + "page": 162, + "dx": -0.75, + "dy": -1.125, + "n": 52 + }, + { + "page": 163, + "dx": -0.625, + "dy": -0.9375, + "n": 50 + }, + { + "page": 164, + "dx": -0.75, + "dy": -0.9375, + "n": 55 + }, + { + "page": 165, + "dx": -0.5, + "dy": -0.9375, + "n": 63 + }, + { + "page": 166, + "dx": -0.75, + "dy": -1.0625, + "n": 51 + }, + { + "page": 167, + "dx": -0.6875, + "dy": -0.9375, + "n": 55 + }, + { + "page": 168, + "dx": -0.625, + "dy": -1, + "n": 50 + }, + { + "page": 169, + "dx": -0.625, + "dy": -0.9375, + "n": 54 + }, + { + "page": 170, + "dx": -0.75, + "dy": -1.125, + "n": 44 + }, + { + "page": 171, + "dx": -0.6875, + "dy": -0.9375, + "n": 45 + }, + { + "page": 172, + "dx": -0.8125, + "dy": -1.125, + "n": 42 + }, + { + "page": 173, + "dx": -0.6875, + "dy": -0.875, + "n": 46 + }, + { + "page": 174, + "dx": -0.8125, + "dy": -1.0625, + "n": 47 + }, + { + "page": 175, + "dx": -0.5625, + "dy": -0.9375, + "n": 45 + }, + { + "page": 176, + "dx": -0.625, + "dy": -0.9375, + "n": 52 + }, + { + "page": 177, + "dx": -0.625, + "dy": -0.8125, + "n": 47 + }, + { + "page": 178, + "dx": -0.6875, + "dy": -1.125, + "n": 46 + }, + { + "page": 179, + "dx": -0.75, + "dy": -0.9375, + "n": 52 + }, + { + "page": 180, + "dx": -0.6875, + "dy": -1, + "n": 59 + }, + { + "page": 181, + "dx": -0.625, + "dy": -0.9375, + "n": 47 + }, + { + "page": 182, + "dx": -0.875, + "dy": -1, + "n": 50 + }, + { + "page": 183, + "dx": -0.625, + "dy": -1.0625, + "n": 45 + }, + { + "page": 184, + "dx": -0.75, + "dy": -0.875, + "n": 52 + }, + { + "page": 185, + "dx": -0.5625, + "dy": -0.875, + "n": 44 + }, + { + "page": 186, + "dx": -0.875, + "dy": -0.9375, + "n": 56 + }, + { + "page": 187, + "dx": -0.5625, + "dy": -1.3125, + "n": 42 + }, + { + "page": 188, + "dx": -0.6875, + "dy": -0.875, + "n": 52 + }, + { + "page": 189, + "dx": -0.75, + "dy": -0.9375, + "n": 56 + }, + { + "page": 190, + "dx": -0.6875, + "dy": -0.8125, + "n": 49 + }, + { + "page": 191, + "dx": -0.6875, + "dy": -1.0625, + "n": 41 + }, + { + "page": 192, + "dx": -0.75, + "dy": -1.0625, + "n": 35 + }, + { + "page": 193, + "dx": -0.5625, + "dy": -0.9375, + "n": 43 + }, + { + "page": 194, + "dx": -0.875, + "dy": -0.875, + "n": 51 + }, + { + "page": 195, + "dx": -0.625, + "dy": -1.0625, + "n": 42 + }, + { + "page": 196, + "dx": -1, + "dy": -1, + "n": 45 + }, + { + "page": 197, + "dx": -0.5, + "dy": -0.9375, + "n": 43 + }, + { + "page": 198, + "dx": -0.75, + "dy": -1, + "n": 47 + }, + { + "page": 199, + "dx": -0.5625, + "dy": -1, + "n": 43 + }, + { + "page": 200, + "dx": -0.875, + "dy": -1, + "n": 63 + }, + { + "page": 201, + "dx": -0.625, + "dy": -0.9375, + "n": 55 + }, + { + "page": 202, + "dx": -0.5625, + "dy": -0.875, + "n": 49 + }, + { + "page": 203, + "dx": -0.625, + "dy": -0.9375, + "n": 48 + }, + { + "page": 204, + "dx": -0.625, + "dy": -1, + "n": 53 + }, + { + "page": 205, + "dx": -0.6875, + "dy": -0.875, + "n": 45 + }, + { + "page": 206, + "dx": -1.1875, + "dy": -1.1875, + "n": 38 + }, + { + "page": 207, + "dx": -0.4375, + "dy": -0.9375, + "n": 39 + }, + { + "page": 208, + "dx": -0.75, + "dy": -1, + "n": 48 + }, + { + "page": 209, + "dx": -0.5, + "dy": -0.8125, + "n": 52 + }, + { + "page": 210, + "dx": -0.625, + "dy": -1, + "n": 44 + }, + { + "page": 211, + "dx": -0.75, + "dy": -0.9375, + "n": 61 + }, + { + "page": 212, + "dx": -0.8125, + "dy": -1.0625, + "n": 51 + }, + { + "page": 213, + "dx": -0.5625, + "dy": -0.875, + "n": 55 + }, + { + "page": 214, + "dx": -0.6875, + "dy": -0.9375, + "n": 57 + }, + { + "page": 215, + "dx": -0.5, + "dy": -0.9375, + "n": 55 + }, + { + "page": 216, + "dx": -0.8125, + "dy": -0.9375, + "n": 45 + }, + { + "page": 217, + "dx": -0.5625, + "dy": -0.9375, + "n": 64 + }, + { + "page": 218, + "dx": -0.625, + "dy": -0.9375, + "n": 51 + }, + { + "page": 219, + "dx": -0.625, + "dy": -0.875, + "n": 52 + }, + { + "page": 220, + "dx": -0.875, + "dy": -1.0625, + "n": 47 + }, + { + "page": 221, + "dx": -0.625, + "dy": -0.9375, + "n": 59 + }, + { + "page": 222, + "dx": -0.75, + "dy": -1.1875, + "n": 59 + }, + { + "page": 223, + "dx": -0.5625, + "dy": -0.9375, + "n": 48 + }, + { + "page": 224, + "dx": -0.9375, + "dy": -1, + "n": 44 + }, + { + "page": 225, + "dx": -0.5625, + "dy": -0.9375, + "n": 46 + }, + { + "page": 226, + "dx": -0.875, + "dy": -1.25, + "n": 45 + }, + { + "page": 227, + "dx": -0.8125, + "dy": -1.0625, + "n": 36 + }, + { + "page": 228, + "dx": -0.6875, + "dy": -1.0625, + "n": 52 + }, + { + "page": 229, + "dx": -0.6875, + "dy": -0.9375, + "n": 37 + }, + { + "page": 230, + "dx": -0.8125, + "dy": -1, + "n": 62 + }, + { + "page": 231, + "dx": -0.5, + "dy": -0.8125, + "n": 57 + }, + { + "page": 232, + "dx": -0.6875, + "dy": -1.0625, + "n": 39 + }, + { + "page": 233, + "dx": -0.6875, + "dy": -0.875, + "n": 45 + }, + { + "page": 234, + "dx": -0.75, + "dy": -1, + "n": 51 + }, + { + "page": 235, + "dx": -0.625, + "dy": -0.9375, + "n": 42 + }, + { + "page": 236, + "dx": -0.875, + "dy": -1.0625, + "n": 63 + }, + { + "page": 237, + "dx": -0.5625, + "dy": -0.875, + "n": 46 + }, + { + "page": 238, + "dx": -0.6875, + "dy": -1.0625, + "n": 60 + }, + { + "page": 239, + "dx": -0.5625, + "dy": -0.9375, + "n": 66 + }, + { + "page": 240, + "dx": -0.5, + "dy": -0.9375, + "n": 38 + }, + { + "page": 241, + "dx": -0.625, + "dy": -1.0625, + "n": 49 + }, + { + "page": 242, + "dx": -0.8125, + "dy": -1.125, + "n": 50 + }, + { + "page": 243, + "dx": -0.75, + "dy": -1.125, + "n": 47 + }, + { + "page": 244, + "dx": -0.5625, + "dy": -0.9375, + "n": 33 + }, + { + "page": 245, + "dx": -0.75, + "dy": -1, + "n": 47 + }, + { + "page": 246, + "dx": -0.6875, + "dy": -1, + "n": 42 + }, + { + "page": 247, + "dx": -0.6875, + "dy": -1, + "n": 51 + }, + { + "page": 248, + "dx": -0.5625, + "dy": -1.0625, + "n": 59 + }, + { + "page": 249, + "dx": -0.5, + "dy": -0.9375, + "n": 42 + }, + { + "page": 250, + "dx": -1, + "dy": -1.1875, + "n": 49 + }, + { + "page": 251, + "dx": -0.5625, + "dy": -0.875, + "n": 65 + }, + { + "page": 252, + "dx": -0.75, + "dy": -0.875, + "n": 64 + }, + { + "page": 253, + "dx": -0.5625, + "dy": -0.9375, + "n": 60 + }, + { + "page": 254, + "dx": -0.8125, + "dy": -1.1875, + "n": 53 + }, + { + "page": 255, + "dx": -0.5, + "dy": -0.9375, + "n": 29 + }, + { + "page": 256, + "dx": -0.75, + "dy": -0.875, + "n": 52 + }, + { + "page": 257, + "dx": -0.875, + "dy": -0.9375, + "n": 42 + }, + { + "page": 258, + "dx": -0.5, + "dy": -0.9375, + "n": 51 + }, + { + "page": 259, + "dx": -0.8125, + "dy": -0.8125, + "n": 45 + }, + { + "page": 260, + "dx": -0.6875, + "dy": -1.0625, + "n": 60 + }, + { + "page": 261, + "dx": -0.75, + "dy": -1.0625, + "n": 50 + }, + { + "page": 262, + "dx": -0.75, + "dy": -1.125, + "n": 48 + }, + { + "page": 263, + "dx": -0.6875, + "dy": -1.0625, + "n": 57 + }, + { + "page": 264, + "dx": -0.75, + "dy": -0.9375, + "n": 42 + }, + { + "page": 265, + "dx": -0.5625, + "dy": -0.9375, + "n": 40 + }, + { + "page": 266, + "dx": -0.875, + "dy": -1.3125, + "n": 45 + }, + { + "page": 267, + "dx": -0.75, + "dy": -1.0625, + "n": 39 + }, + { + "page": 268, + "dx": -0.8125, + "dy": -1.0625, + "n": 51 + }, + { + "page": 269, + "dx": -0.75, + "dy": -0.8125, + "n": 42 + }, + { + "page": 270, + "dx": -0.625, + "dy": -1, + "n": 41 + }, + { + "page": 271, + "dx": -0.625, + "dy": -0.875, + "n": 48 + }, + { + "page": 272, + "dx": -0.625, + "dy": -0.875, + "n": 59 + }, + { + "page": 273, + "dx": -0.5625, + "dy": -0.9375, + "n": 50 + }, + { + "page": 274, + "dx": -1, + "dy": -1, + "n": 58 + }, + { + "page": 275, + "dx": -0.625, + "dy": -0.9375, + "n": 57 + }, + { + "page": 276, + "dx": -0.8125, + "dy": -1, + "n": 42 + }, + { + "page": 277, + "dx": -0.625, + "dy": -0.9375, + "n": 51 + }, + { + "page": 278, + "dx": -0.6875, + "dy": -1, + "n": 48 + }, + { + "page": 279, + "dx": -0.6875, + "dy": -0.9375, + "n": 61 + }, + { + "page": 280, + "dx": -0.625, + "dy": -1, + "n": 46 + }, + { + "page": 281, + "dx": -0.75, + "dy": -0.875, + "n": 53 + }, + { + "page": 282, + "dx": -0.75, + "dy": -1.0625, + "n": 42 + }, + { + "page": 283, + "dx": -0.625, + "dy": -1.0625, + "n": 62 + }, + { + "page": 284, + "dx": -0.75, + "dy": -1.0625, + "n": 55 + }, + { + "page": 285, + "dx": -0.75, + "dy": -1.0625, + "n": 47 + }, + { + "page": 286, + "dx": -0.625, + "dy": -1.0625, + "n": 38 + }, + { + "page": 287, + "dx": -0.5625, + "dy": -0.9375, + "n": 54 + }, + { + "page": 288, + "dx": -0.75, + "dy": -1.0625, + "n": 38 + }, + { + "page": 289, + "dx": -0.5625, + "dy": -0.9375, + "n": 55 + }, + { + "page": 290, + "dx": -0.6875, + "dy": -0.9375, + "n": 43 + }, + { + "page": 291, + "dx": -0.5, + "dy": -0.8125, + "n": 44 + }, + { + "page": 292, + "dx": -0.8125, + "dy": -0.9375, + "n": 43 + }, + { + "page": 293, + "dx": -0.5625, + "dy": -0.875, + "n": 54 + }, + { + "page": 294, + "dx": -0.75, + "dy": -1.0625, + "n": 42 + }, + { + "page": 295, + "dx": -0.5, + "dy": -0.9375, + "n": 51 + }, + { + "page": 296, + "dx": -0.875, + "dy": -0.9375, + "n": 49 + }, + { + "page": 297, + "dx": -0.6875, + "dy": -0.8125, + "n": 54 + }, + { + "page": 298, + "dx": -0.875, + "dy": -1.0625, + "n": 48 + }, + { + "page": 299, + "dx": -0.5, + "dy": -0.9375, + "n": 58 + }, + { + "page": 300, + "dx": -0.625, + "dy": -1.0625, + "n": 46 + }, + { + "page": 301, + "dx": -0.5, + "dy": -1.0625, + "n": 56 + }, + { + "page": 302, + "dx": -0.8125, + "dy": -1, + "n": 57 + }, + { + "page": 303, + "dx": -0.625, + "dy": -0.875, + "n": 48 + }, + { + "page": 304, + "dx": -0.75, + "dy": -1.3125, + "n": 61 + }, + { + "page": 305, + "dx": -0.6875, + "dy": -1.0625, + "n": 41 + }, + { + "page": 306, + "dx": -1.1875, + "dy": -0.9375, + "n": 57 + }, + { + "page": 307, + "dx": -0.625, + "dy": -0.9375, + "n": 45 + }, + { + "page": 308, + "dx": -0.8125, + "dy": -1.1875, + "n": 50 + }, + { + "page": 309, + "dx": -0.8125, + "dy": -1.0625, + "n": 55 + }, + { + "page": 310, + "dx": -0.9375, + "dy": -1.25, + "n": 53 + }, + { + "page": 311, + "dx": -0.5625, + "dy": -1.0625, + "n": 52 + }, + { + "page": 312, + "dx": -0.5625, + "dy": -1.3125, + "n": 37 + }, + { + "page": 313, + "dx": -0.75, + "dy": -1.0625, + "n": 51 + }, + { + "page": 314, + "dx": -1, + "dy": -1.0625, + "n": 55 + }, + { + "page": 315, + "dx": -0.625, + "dy": -0.9375, + "n": 59 + }, + { + "page": 316, + "dx": -0.8125, + "dy": -1.0625, + "n": 57 + }, + { + "page": 317, + "dx": -0.6875, + "dy": -1.0625, + "n": 65 + }, + { + "page": 318, + "dx": -0.6875, + "dy": -0.9375, + "n": 54 + }, + { + "page": 319, + "dx": -0.625, + "dy": -1.0625, + "n": 50 + }, + { + "page": 320, + "dx": -1.1875, + "dy": -1.1875, + "n": 40 + }, + { + "page": 321, + "dx": -0.5625, + "dy": -0.8125, + "n": 57 + }, + { + "page": 322, + "dx": -0.625, + "dy": -1.0625, + "n": 58 + }, + { + "page": 323, + "dx": -0.5, + "dy": -0.8125, + "n": 53 + }, + { + "page": 324, + "dx": -0.75, + "dy": -1.0625, + "n": 63 + }, + { + "page": 325, + "dx": -0.5, + "dy": -1.0625, + "n": 51 + }, + { + "page": 326, + "dx": -0.625, + "dy": -1, + "n": 57 + }, + { + "page": 327, + "dx": -0.8125, + "dy": -1, + "n": 53 + }, + { + "page": 328, + "dx": -0.75, + "dy": -1.1875, + "n": 41 + }, + { + "page": 329, + "dx": -0.6875, + "dy": -1.0625, + "n": 45 + }, + { + "page": 330, + "dx": -0.6875, + "dy": -1.0625, + "n": 36 + }, + { + "page": 331, + "dx": -0.75, + "dy": -0.9375, + "n": 42 + }, + { + "page": 332, + "dx": -0.6875, + "dy": -0.875, + "n": 41 + }, + { + "page": 333, + "dx": -0.6875, + "dy": -0.9375, + "n": 44 + }, + { + "page": 334, + "dx": -0.75, + "dy": -1, + "n": 50 + }, + { + "page": 335, + "dx": -0.6875, + "dy": -1.0625, + "n": 51 + }, + { + "page": 336, + "dx": -0.6875, + "dy": -0.9375, + "n": 60 + }, + { + "page": 337, + "dx": -0.625, + "dy": -0.9375, + "n": 59 + }, + { + "page": 338, + "dx": -0.6875, + "dy": -0.9375, + "n": 55 + }, + { + "page": 339, + "dx": -0.875, + "dy": -1.0625, + "n": 42 + }, + { + "page": 340, + "dx": -0.8125, + "dy": -1.0625, + "n": 47 + }, + { + "page": 341, + "dx": -0.75, + "dy": -0.875, + "n": 36 + }, + { + "page": 342, + "dx": -0.8125, + "dy": -1, + "n": 41 + }, + { + "page": 343, + "dx": -0.5625, + "dy": -0.875, + "n": 59 + }, + { + "page": 344, + "dx": -0.875, + "dy": -0.9375, + "n": 45 + }, + { + "page": 345, + "dx": -0.875, + "dy": -1.0625, + "n": 53 + }, + { + "page": 346, + "dx": -0.625, + "dy": -0.9375, + "n": 56 + }, + { + "page": 347, + "dx": -0.5, + "dy": -0.9375, + "n": 50 + }, + { + "page": 348, + "dx": -0.9375, + "dy": -1, + "n": 50 + }, + { + "page": 349, + "dx": -0.625, + "dy": -0.875, + "n": 57 + }, + { + "page": 350, + "dx": -0.625, + "dy": -1.0625, + "n": 61 + }, + { + "page": 351, + "dx": -0.625, + "dy": -0.9375, + "n": 51 + }, + { + "page": 352, + "dx": -0.75, + "dy": -0.9375, + "n": 53 + }, + { + "page": 353, + "dx": -0.625, + "dy": -0.9375, + "n": 45 + }, + { + "page": 354, + "dx": -0.625, + "dy": -1.1875, + "n": 57 + }, + { + "page": 355, + "dx": -0.8125, + "dy": -1, + "n": 51 + }, + { + "page": 356, + "dx": -0.625, + "dy": -1, + "n": 56 + }, + { + "page": 357, + "dx": -0.75, + "dy": -0.8125, + "n": 53 + }, + { + "page": 358, + "dx": -0.625, + "dy": -0.9375, + "n": 44 + }, + { + "page": 359, + "dx": -0.6875, + "dy": -0.9375, + "n": 52 + }, + { + "page": 360, + "dx": -1.125, + "dy": -1.0625, + "n": 50 + }, + { + "page": 361, + "dx": -0.75, + "dy": -1.0625, + "n": 43 + }, + { + "page": 362, + "dx": -0.625, + "dy": -1, + "n": 48 + }, + { + "page": 363, + "dx": -0.5625, + "dy": -0.9375, + "n": 57 + }, + { + "page": 364, + "dx": -0.625, + "dy": -1, + "n": 54 + }, + { + "page": 365, + "dx": -0.6875, + "dy": -1.0625, + "n": 47 + }, + { + "page": 366, + "dx": -0.625, + "dy": -0.9375, + "n": 44 + }, + { + "page": 367, + "dx": -0.625, + "dy": -0.875, + "n": 49 + }, + { + "page": 368, + "dx": -0.75, + "dy": -1.0625, + "n": 52 + }, + { + "page": 369, + "dx": -0.5, + "dy": -0.9375, + "n": 52 + }, + { + "page": 370, + "dx": -1.375, + "dy": -1.1875, + "n": 46 + }, + { + "page": 371, + "dx": -0.75, + "dy": -1, + "n": 47 + }, + { + "page": 372, + "dx": -0.9375, + "dy": -1.0625, + "n": 57 + }, + { + "page": 373, + "dx": -0.5625, + "dy": -0.875, + "n": 50 + }, + { + "page": 374, + "dx": -0.625, + "dy": -0.9375, + "n": 64 + }, + { + "page": 375, + "dx": -0.625, + "dy": -0.9375, + "n": 59 + }, + { + "page": 376, + "dx": -0.75, + "dy": -1.3125, + "n": 49 + }, + { + "page": 377, + "dx": -0.625, + "dy": -0.9375, + "n": 51 + }, + { + "page": 378, + "dx": -0.6875, + "dy": -1.0625, + "n": 59 + }, + { + "page": 379, + "dx": -0.875, + "dy": -1, + "n": 65 + }, + { + "page": 380, + "dx": -0.875, + "dy": -1.125, + "n": 51 + }, + { + "page": 381, + "dx": -0.625, + "dy": -0.875, + "n": 44 + }, + { + "page": 382, + "dx": -0.8125, + "dy": -1.0625, + "n": 49 + }, + { + "page": 383, + "dx": -0.625, + "dy": -0.9375, + "n": 39 + }, + { + "page": 384, + "dx": -0.75, + "dy": -1, + "n": 61 + }, + { + "page": 385, + "dx": -0.6875, + "dy": -1.0625, + "n": 48 + }, + { + "page": 386, + "dx": -0.5625, + "dy": -0.9375, + "n": 45 + }, + { + "page": 387, + "dx": -0.5625, + "dy": -0.875, + "n": 66 + }, + { + "page": 388, + "dx": -0.8125, + "dy": -1.0625, + "n": 38 + }, + { + "page": 389, + "dx": -0.5625, + "dy": -1.0625, + "n": 57 + }, + { + "page": 390, + "dx": -0.75, + "dy": -0.875, + "n": 39 + }, + { + "page": 391, + "dx": -0.625, + "dy": -0.875, + "n": 47 + }, + { + "page": 392, + "dx": -0.75, + "dy": -1.125, + "n": 59 + }, + { + "page": 393, + "dx": -0.625, + "dy": -0.9375, + "n": 51 + }, + { + "page": 394, + "dx": -0.75, + "dy": -0.9375, + "n": 51 + }, + { + "page": 395, + "dx": -0.625, + "dy": -0.9375, + "n": 48 + }, + { + "page": 396, + "dx": -0.625, + "dy": -0.875, + "n": 38 + }, + { + "page": 397, + "dx": -0.6875, + "dy": -1.0625, + "n": 56 + }, + { + "page": 398, + "dx": -0.625, + "dy": -0.8125, + "n": 44 + }, + { + "page": 399, + "dx": -0.625, + "dy": -0.9375, + "n": 44 + }, + { + "page": 400, + "dx": -0.75, + "dy": -0.9375, + "n": 45 + }, + { + "page": 401, + "dx": -0.75, + "dy": -0.875, + "n": 37 + }, + { + "page": 402, + "dx": -0.625, + "dy": -0.9375, + "n": 56 + }, + { + "page": 403, + "dx": -0.625, + "dy": -0.9375, + "n": 56 + }, + { + "page": 404, + "dx": -0.75, + "dy": -0.9375, + "n": 46 + }, + { + "page": 405, + "dx": -0.5625, + "dy": -0.875, + "n": 60 + }, + { + "page": 406, + "dx": -0.75, + "dy": -0.9375, + "n": 54 + }, + { + "page": 407, + "dx": -0.625, + "dy": -0.9375, + "n": 39 + }, + { + "page": 408, + "dx": -0.75, + "dy": -1, + "n": 46 + }, + { + "page": 409, + "dx": -0.5, + "dy": -0.9375, + "n": 53 + }, + { + "page": 410, + "dx": -0.75, + "dy": -0.875, + "n": 46 + }, + { + "page": 411, + "dx": -0.9375, + "dy": -1.0625, + "n": 33 + }, + { + "page": 412, + "dx": -0.5625, + "dy": -1.0625, + "n": 44 + }, + { + "page": 413, + "dx": -0.6875, + "dy": -0.9375, + "n": 51 + }, + { + "page": 414, + "dx": -0.75, + "dy": -1.3125, + "n": 37 + }, + { + "page": 415, + "dx": -0.75, + "dy": -0.9375, + "n": 53 + }, + { + "page": 416, + "dx": -1, + "dy": -1.0625, + "n": 54 + }, + { + "page": 417, + "dx": -0.625, + "dy": -0.8125, + "n": 48 + }, + { + "page": 418, + "dx": -0.75, + "dy": -1.0625, + "n": 58 + }, + { + "page": 419, + "dx": -0.625, + "dy": -0.9375, + "n": 58 + }, + { + "page": 420, + "dx": -0.75, + "dy": -1.0625, + "n": 68 + }, + { + "page": 421, + "dx": -0.875, + "dy": -1.0625, + "n": 42 + }, + { + "page": 422, + "dx": -0.8125, + "dy": -0.9375, + "n": 52 + }, + { + "page": 423, + "dx": -0.8125, + "dy": -1, + "n": 56 + }, + { + "page": 424, + "dx": -0.6875, + "dy": -1, + "n": 50 + }, + { + "page": 425, + "dx": -0.5625, + "dy": -0.875, + "n": 47 + }, + { + "page": 426, + "dx": -0.625, + "dy": -1, + "n": 40 + }, + { + "page": 427, + "dx": -0.5625, + "dy": -0.9375, + "n": 54 + }, + { + "page": 428, + "dx": -0.75, + "dy": -0.9375, + "n": 28 + }, + { + "page": 429, + "dx": -0.5625, + "dy": -1.1875, + "n": 47 + }, + { + "page": 430, + "dx": -0.9375, + "dy": -0.9375, + "n": 49 + }, + { + "page": 431, + "dx": -0.5625, + "dy": -0.9375, + "n": 34 + }, + { + "page": 432, + "dx": -0.75, + "dy": -1.25, + "n": 59 + }, + { + "page": 433, + "dx": -0.5, + "dy": -1.0625, + "n": 56 + }, + { + "page": 434, + "dx": -0.6875, + "dy": -0.9375, + "n": 38 + }, + { + "page": 435, + "dx": -0.75, + "dy": -0.9375, + "n": 57 + }, + { + "page": 436, + "dx": -0.6875, + "dy": -1.0625, + "n": 50 + }, + { + "page": 437, + "dx": -0.6875, + "dy": -1.0625, + "n": 50 + }, + { + "page": 438, + "dx": -0.8125, + "dy": -0.9375, + "n": 41 + }, + { + "page": 439, + "dx": -0.75, + "dy": -0.875, + "n": 52 + }, + { + "page": 440, + "dx": -0.75, + "dy": -1.25, + "n": 35 + }, + { + "page": 441, + "dx": -0.5, + "dy": -0.9375, + "n": 49 + }, + { + "page": 442, + "dx": -0.9375, + "dy": -1.0625, + "n": 44 + }, + { + "page": 443, + "dx": -0.5625, + "dy": -1, + "n": 58 + }, + { + "page": 444, + "dx": -0.625, + "dy": -0.875, + "n": 51 + }, + { + "page": 445, + "dx": 0, + "dy": -0.8125, + "n": 34 + }, + { + "page": 446, + "dx": -0.3125, + "dy": -0.9375, + "n": 50 + }, + { + "page": 447, + "dx": -0.75, + "dy": -1.0625, + "n": 54 + }, + { + "page": 448, + "dx": -0.875, + "dy": -1.0625, + "n": 52 + }, + { + "page": 449, + "dx": -0.5625, + "dy": -0.875, + "n": 43 + }, + { + "page": 450, + "dx": -0.75, + "dy": -0.9375, + "n": 47 + }, + { + "page": 451, + "dx": -0.0625, + "dy": -0.8125, + "n": 57 + }, + { + "page": 452, + "dx": -0.1875, + "dy": -0.9375, + "n": 40 + }, + { + "page": 453, + "dx": -0.125, + "dy": -0.8125, + "n": 53 + }, + { + "page": 454, + "dx": -0.5625, + "dy": -1.1875, + "n": 55 + }, + { + "page": 455, + "dx": -0.5625, + "dy": -0.9375, + "n": 48 + }, + { + "page": 456, + "dx": -0.75, + "dy": -1.125, + "n": 47 + }, + { + "page": 457, + "dx": -0.6875, + "dy": -0.9375, + "n": 58 + }, + { + "page": 458, + "dx": -0.875, + "dy": -1, + "n": 48 + }, + { + "page": 459, + "dx": -0.625, + "dy": -1.0625, + "n": 58 + }, + { + "page": 460, + "dx": -0.75, + "dy": -1, + "n": 52 + }, + { + "page": 461, + "dx": -0.6875, + "dy": -0.9375, + "n": 44 + }, + { + "page": 462, + "dx": -0.875, + "dy": -1.0625, + "n": 48 + }, + { + "page": 463, + "dx": -0.625, + "dy": -0.8125, + "n": 60 + }, + { + "page": 464, + "dx": -0.75, + "dy": -1.0625, + "n": 45 + }, + { + "page": 465, + "dx": -0.5, + "dy": -1, + "n": 45 + }, + { + "page": 466, + "dx": -1.1875, + "dy": -1.3125, + "n": 42 + }, + { + "page": 467, + "dx": -0.75, + "dy": -1, + "n": 44 + }, + { + "page": 468, + "dx": -0.6875, + "dy": -0.8125, + "n": 38 + }, + { + "page": 469, + "dx": -0.625, + "dy": -0.875, + "n": 57 + }, + { + "page": 470, + "dx": -0.625, + "dy": -1, + "n": 47 + }, + { + "page": 471, + "dx": -0.5, + "dy": -0.9375, + "n": 35 + }, + { + "page": 472, + "dx": -0.75, + "dy": -0.9375, + "n": 30 + }, + { + "page": 473, + "dx": -0.625, + "dy": -0.8125, + "n": 38 + }, + { + "page": 474, + "dx": -0.75, + "dy": -0.9375, + "n": 49 + }, + { + "page": 475, + "dx": -0.625, + "dy": -0.9375, + "n": 43 + }, + { + "page": 476, + "dx": -0.875, + "dy": -1.125, + "n": 55 + }, + { + "page": 477, + "dx": -0.75, + "dy": -0.8125, + "n": 47 + }, + { + "page": 478, + "dx": -0.6875, + "dy": -0.8125, + "n": 60 + }, + { + "page": 479, + "dx": -0.5625, + "dy": -0.9375, + "n": 56 + }, + { + "page": 480, + "dx": -1.3125, + "dy": -1.0625, + "n": 53 + }, + { + "page": 481, + "dx": -0.5625, + "dy": -0.9375, + "n": 51 + }, + { + "page": 482, + "dx": -0.75, + "dy": -1, + "n": 50 + }, + { + "page": 483, + "dx": -0.625, + "dy": -0.9375, + "n": 42 + }, + { + "page": 484, + "dx": -0.75, + "dy": -1, + "n": 52 + }, + { + "page": 485, + "dx": -0.5625, + "dy": -0.8125, + "n": 59 + }, + { + "page": 486, + "dx": -1, + "dy": -1.1875, + "n": 58 + }, + { + "page": 487, + "dx": -0.625, + "dy": -0.9375, + "n": 54 + }, + { + "page": 488, + "dx": -0.75, + "dy": -1.0625, + "n": 48 + }, + { + "page": 489, + "dx": -0.75, + "dy": -1, + "n": 39 + }, + { + "page": 490, + "dx": -0.8125, + "dy": -1.1875, + "n": 47 + }, + { + "page": 491, + "dx": -0.625, + "dy": -0.875, + "n": 50 + }, + { + "page": 492, + "dx": -0.75, + "dy": -0.875, + "n": 46 + }, + { + "page": 493, + "dx": -0.5625, + "dy": -0.9375, + "n": 60 + }, + { + "page": 494, + "dx": -0.6875, + "dy": -1, + "n": 58 + }, + { + "page": 495, + "dx": -0.625, + "dy": -0.875, + "n": 60 + }, + { + "page": 496, + "dx": -0.9375, + "dy": -1, + "n": 43 + }, + { + "page": 497, + "dx": -0.5625, + "dy": -0.875, + "n": 51 + }, + { + "page": 498, + "dx": -0.75, + "dy": -1.0625, + "n": 35 + }, + { + "page": 499, + "dx": -0.125, + "dy": -0.6875, + "n": 52 + }, + { + "page": 500, + "dx": -0.25, + "dy": -0.875, + "n": 60 + }, + { + "page": 501, + "dx": -0.75, + "dy": -1.0625, + "n": 54 + }, + { + "page": 502, + "dx": -0.625, + "dy": -0.875, + "n": 45 + }, + { + "page": 503, + "dx": -0.625, + "dy": -0.875, + "n": 65 + }, + { + "page": 504, + "dx": -0.875, + "dy": -1.0625, + "n": 59 + }, + { + "page": 505, + "dx": -0.625, + "dy": -1, + "n": 45 + }, + { + "page": 506, + "dx": -0.625, + "dy": -1.0625, + "n": 42 + }, + { + "page": 507, + "dx": -0.3125, + "dy": -0.875, + "n": 63 + }, + { + "page": 508, + "dx": -0.125, + "dy": -0.875, + "n": 48 + }, + { + "page": 509, + "dx": -0.5625, + "dy": -0.8125, + "n": 36 + }, + { + "page": 510, + "dx": -0.8125, + "dy": -0.9375, + "n": 42 + }, + { + "page": 511, + "dx": -0.6875, + "dy": -0.9375, + "n": 42 + }, + { + "page": 512, + "dx": -0.875, + "dy": -1.0625, + "n": 51 + }, + { + "page": 513, + "dx": -0.75, + "dy": -1.0625, + "n": 45 + }, + { + "page": 514, + "dx": -0.9375, + "dy": -1.125, + "n": 42 + }, + { + "page": 515, + "dx": -0.5, + "dy": -0.9375, + "n": 50 + }, + { + "page": 516, + "dx": -0.75, + "dy": -1.0625, + "n": 56 + }, + { + "page": 517, + "dx": -0.625, + "dy": -1.0625, + "n": 45 + }, + { + "page": 518, + "dx": -0.75, + "dy": -1.125, + "n": 57 + }, + { + "page": 519, + "dx": -0.5625, + "dy": -1.0625, + "n": 45 + }, + { + "page": 520, + "dx": -0.8125, + "dy": -1, + "n": 53 + }, + { + "page": 521, + "dx": -0.6875, + "dy": -0.9375, + "n": 49 + }, + { + "page": 522, + "dx": -0.6875, + "dy": -1.0625, + "n": 58 + }, + { + "page": 523, + "dx": -0.875, + "dy": -0.9375, + "n": 43 + }, + { + "page": 524, + "dx": -0.625, + "dy": -1.0625, + "n": 56 + }, + { + "page": 525, + "dx": -0.625, + "dy": -0.8125, + "n": 40 + }, + { + "page": 526, + "dx": -0.875, + "dy": -1.1875, + "n": 50 + }, + { + "page": 527, + "dx": -0.5625, + "dy": -0.875, + "n": 67 + }, + { + "page": 528, + "dx": -0.75, + "dy": -1, + "n": 51 + }, + { + "page": 529, + "dx": -0.625, + "dy": -0.8125, + "n": 43 + }, + { + "page": 530, + "dx": -0.9375, + "dy": -1.0625, + "n": 53 + }, + { + "page": 531, + "dx": -0.5, + "dy": -0.8125, + "n": 44 + }, + { + "page": 532, + "dx": -0.8125, + "dy": -0.9375, + "n": 47 + }, + { + "page": 533, + "dx": -0.625, + "dy": -0.9375, + "n": 57 + }, + { + "page": 534, + "dx": -0.875, + "dy": -1.0625, + "n": 28 + }, + { + "page": 535, + "dx": -0.625, + "dy": -0.9375, + "n": 50 + }, + { + "page": 536, + "dx": -0.6875, + "dy": -1.3125, + "n": 55 + }, + { + "page": 537, + "dx": -0.8125, + "dy": -0.9375, + "n": 47 + }, + { + "page": 538, + "dx": -0.8125, + "dy": -1.0625, + "n": 53 + }, + { + "page": 539, + "dx": -0.625, + "dy": -0.75, + "n": 61 + }, + { + "page": 540, + "dx": -0.6875, + "dy": -0.9375, + "n": 49 + }, + { + "page": 541, + "dx": -0.5, + "dy": -0.9375, + "n": 54 + }, + { + "page": 542, + "dx": -0.9375, + "dy": -1.125, + "n": 47 + }, + { + "page": 543, + "dx": -0.75, + "dy": -1, + "n": 50 + }, + { + "page": 544, + "dx": -0.75, + "dy": -1, + "n": 50 + }, + { + "page": 545, + "dx": -0.625, + "dy": -1.0625, + "n": 43 + }, + { + "page": 546, + "dx": -0.625, + "dy": -0.875, + "n": 36 + }, + { + "page": 547, + "dx": -0.5625, + "dy": -0.9375, + "n": 43 + }, + { + "page": 548, + "dx": -0.75, + "dy": -0.9375, + "n": 51 + }, + { + "page": 549, + "dx": -0.125, + "dy": -0.8125, + "n": 55 + }, + { + "page": 550, + "dx": -0.375, + "dy": -0.9375, + "n": 57 + }, + { + "page": 551, + "dx": -0.625, + "dy": -0.9375, + "n": 39 + }, + { + "page": 552, + "dx": -0.5625, + "dy": -1, + "n": 76 + }, + { + "page": 553, + "dx": -0.75, + "dy": -0.9375, + "n": 46 + }, + { + "page": 554, + "dx": -0.5625, + "dy": -0.9375, + "n": 35 + }, + { + "page": 555, + "dx": -0.0625, + "dy": -0.9375, + "n": 59 + }, + { + "page": 556, + "dx": -0.375, + "dy": -0.8125, + "n": 35 + }, + { + "page": 557, + "dx": -0.625, + "dy": -0.9375, + "n": 46 + }, + { + "page": 558, + "dx": -0.9375, + "dy": -1, + "n": 48 + }, + { + "page": 559, + "dx": -0.625, + "dy": -0.9375, + "n": 56 + }, + { + "page": 560, + "dx": -0.625, + "dy": -1.0625, + "n": 40 + }, + { + "page": 561, + "dx": -0.5625, + "dy": -1.125, + "n": 51 + }, + { + "page": 562, + "dx": -0.75, + "dy": -0.6875, + "n": 46 + }, + { + "page": 563, + "dx": -0.5625, + "dy": -0.9375, + "n": 48 + }, + { + "page": 564, + "dx": -1.3125, + "dy": -1.5, + "n": 33 + }, + { + "page": 565, + "dx": -0.5625, + "dy": -1.0625, + "n": 53 + }, + { + "page": 566, + "dx": -0.75, + "dy": -0.9375, + "n": 48 + }, + { + "page": 567, + "dx": -0.75, + "dy": -1.0625, + "n": 50 + }, + { + "page": 568, + "dx": -0.875, + "dy": -1.125, + "n": 50 + }, + { + "page": 569, + "dx": -0.625, + "dy": -1, + "n": 61 + }, + { + "page": 570, + "dx": -0.875, + "dy": -0.9375, + "n": 47 + }, + { + "page": 571, + "dx": -0.625, + "dy": -1, + "n": 50 + }, + { + "page": 572, + "dx": -0.875, + "dy": -1.0625, + "n": 51 + }, + { + "page": 573, + "dx": -0.0625, + "dy": -0.8125, + "n": 49 + }, + { + "page": 574, + "dx": -0.75, + "dy": -1.0625, + "n": 41 + }, + { + "page": 575, + "dx": -0.625, + "dy": -0.875, + "n": 35 + }, + { + "page": 576, + "dx": -0.75, + "dy": -0.9375, + "n": 63 + }, + { + "page": 577, + "dx": -0.6875, + "dy": -0.9375, + "n": 56 + }, + { + "page": 578, + "dx": -0.5625, + "dy": -1.0625, + "n": 55 + }, + { + "page": 579, + "dx": -0.625, + "dy": -1.0625, + "n": 61 + }, + { + "page": 580, + "dx": -0.8125, + "dy": -0.9375, + "n": 39 + }, + { + "page": 581, + "dx": -0.5625, + "dy": -0.9375, + "n": 36 + }, + { + "page": 582, + "dx": -0.875, + "dy": -1.1875, + "n": 30 + }, + { + "page": 583, + "dx": -0.75, + "dy": -1.0625, + "n": 50 + }, + { + "page": 584, + "dx": -0.75, + "dy": -1.0625, + "n": 49 + }, + { + "page": 585, + "dx": -0.25, + "dy": -0.875, + "n": 39 + }, + { + "page": 586, + "dx": -0.375, + "dy": -0.9375, + "n": 41 + }, + { + "page": 587, + "dx": -0.6875, + "dy": -0.9375, + "n": 27 + }, + { + "page": 588, + "dx": -1.0625, + "dy": -1.0625, + "n": 64 + }, + { + "page": 589, + "dx": -0.625, + "dy": -1, + "n": 37 + }, + { + "page": 590, + "dx": -0.9375, + "dy": -1, + "n": 47 + }, + { + "page": 591, + "dx": -0.8125, + "dy": -1.0625, + "n": 26 + }, + { + "page": 592, + "dx": -0.625, + "dy": -0.9375, + "n": 38 + }, + { + "page": 593, + "dx": -0.625, + "dy": -0.9375, + "n": 61 + }, + { + "page": 594, + "dx": -0.5, + "dy": -1.0625, + "n": 45 + }, + { + "page": 595, + "dx": -0.8125, + "dy": -1, + "n": 49 + }, + { + "page": 596, + "dx": -0.875, + "dy": -1.1875, + "n": 36 + }, + { + "page": 597, + "dx": -0.625, + "dy": -1.0625, + "n": 38 + }, + { + "page": 598, + "dx": -0.6875, + "dy": -0.9375, + "n": 34 + }, + { + "page": 599, + "dx": -0.875, + "dy": -1.0625, + "n": 31 + }, + { + "page": 600, + "dx": -0.625, + "dy": -0.9375, + "n": 35 + }, + { + "page": 601, + "dx": -0.6875, + "dy": -1, + "n": 28 + }, + { + "page": 602, + "dx": -0.5, + "dy": -0.9375, + "n": 24 + } + ] +} diff --git a/docs/validation/sessions/2026-08-12T1650-placement-residual-by-hand.jsonl b/docs/validation/sessions/2026-08-12T1650-placement-residual-by-hand.jsonl new file mode 100644 index 00000000..cc83cb1a --- /dev/null +++ b/docs/validation/sessions/2026-08-12T1650-placement-residual-by-hand.jsonl @@ -0,0 +1,62 @@ +{"t":"2026-08-12T16:50:10.790Z","kind":"session","check":"placement-residual-by-hand","title":"Placement by hand — put each rectangle where it belongs, and measure how far our correction still misses","commit":"b674459","on":"darwin node 20.19.4","stepsTotal":9} +{"t":"2026-08-12T18:00:13.759Z","kind":"observation","tool":true,"payload":{"trial":1,"of":60,"i":0,"id":"p340k216","u":[-0.464,-1.577],"from":[-2.75,-1.2],"moves":63,"ms":8063}} +{"t":"2026-08-12T18:00:26.064Z","kind":"observation","tool":true,"payload":{"trial":2,"of":60,"i":1,"id":"p336k300","u":[-1.081,-1.046],"from":[-2.65,-1.41],"moves":70,"ms":12305}} +{"t":"2026-08-12T18:00:34.307Z","kind":"observation","tool":true,"payload":{"trial":3,"of":60,"i":2,"id":"p463k471","u":[-0.632,-0.882],"from":[3,0.17],"moves":172,"ms":8241}} +{"t":"2026-08-12T18:00:43.749Z","kind":"observation","tool":true,"payload":{"trial":4,"of":60,"i":3,"id":"p115k199","u":[-0.372,-0.894],"from":[1.28,-2.71],"moves":150,"ms":9434}} +{"t":"2026-08-12T18:00:50.556Z","kind":"observation","tool":true,"payload":{"trial":5,"of":60,"i":4,"id":"p557k129","u":[-0.697,-0.899],"from":[0.3,-2.98],"moves":66,"ms":6810}} +{"t":"2026-08-12T18:00:58.006Z","kind":"observation","tool":true,"payload":{"trial":6,"of":60,"i":5,"id":"p315k187","u":[-1.49,-1.22],"from":[-2.64,-1.42],"moves":35,"ms":7451}} +{"t":"2026-08-12T18:01:15.830Z","kind":"observation","tool":true,"payload":{"trial":7,"of":60,"i":6,"id":"p180k285","u":[-0.52,-0.89],"from":[1.18,2.76],"moves":115,"ms":17714}} +{"t":"2026-08-12T18:01:23.857Z","kind":"observation","tool":true,"payload":{"trial":8,"of":60,"i":7,"id":"p314k75","u":[-0.31,-1.03],"from":[-2.96,-0.48],"moves":68,"ms":8131}} +{"t":"2026-08-12T18:01:32.877Z","kind":"observation","tool":true,"payload":{"trial":9,"of":60,"i":8,"id":"p337k432","u":[-0.31,-1.35],"from":[0.09,-3],"moves":45,"ms":9019}} +{"t":"2026-08-12T18:02:21.000Z","kind":"observation","tool":true,"payload":{"trial":10,"of":60,"i":9,"id":"p326k205","u":[-0.691,-1.911],"from":[1.57,2.56],"moves":57,"ms":48120}} +{"t":"2026-08-12T18:02:27.969Z","kind":"observation","tool":true,"payload":{"trial":11,"of":60,"i":10,"id":"p583k243","u":[-0.488,-0.864],"from":[1.74,2.44],"moves":107,"ms":6969}} +{"t":"2026-08-12T18:02:35.096Z","kind":"observation","tool":true,"payload":{"trial":12,"of":60,"i":11,"id":"p591k154","u":[-2,-2.32],"from":[-1.95,2.28],"moves":93,"ms":7127}} +{"t":"2026-08-12T18:02:47.567Z","kind":"observation","tool":true,"payload":{"trial":13,"of":60,"i":12,"id":"p95k361","u":[-1.88,-1.26],"from":[-1.23,2.74],"moves":97,"ms":12467}} +{"t":"2026-08-12T18:02:53.218Z","kind":"observation","tool":true,"payload":{"trial":14,"of":60,"i":13,"id":"p326k358","u":[-0.34,-1.1],"from":[-2.89,-0.8],"moves":61,"ms":5647}} +{"t":"2026-08-12T18:03:00.496Z","kind":"observation","tool":true,"payload":{"trial":15,"of":60,"i":14,"id":"p181k58","u":[-0.79,-0.59],"from":[-0.79,-2.89],"moves":50,"ms":7171}} +{"t":"2026-08-12T18:03:09.639Z","kind":"observation","tool":true,"payload":{"trial":16,"of":60,"i":15,"id":"p150k428","u":[-0.66,-0.98],"from":[2.99,-0.18],"moves":89,"ms":9253}} +{"t":"2026-08-12T18:03:21.630Z","kind":"observation","tool":true,"payload":{"trial":17,"of":60,"i":16,"id":"p86k237","u":[-0.65,-0.74],"from":[0.6,-2.94],"moves":75,"ms":11989}} +{"t":"2026-08-12T18:03:31.921Z","kind":"observation","tool":true,"payload":{"trial":18,"of":60,"i":17,"id":"p39k402","u":[-1.19,-1.38],"from":[-0.69,2.92],"moves":100,"ms":10095}} +{"t":"2026-08-12T18:03:36.950Z","kind":"observation","tool":true,"payload":{"trial":19,"of":60,"i":18,"id":"p592k178","u":[-0.37,-0.83],"from":[2.88,-0.83],"moves":65,"ms":5112}} +{"t":"2026-08-12T18:04:14.828Z","kind":"observation","tool":true,"payload":{"trial":20,"of":60,"i":19,"id":"p501k474","u":[-1.04,-1.104],"from":[1.78,2.41],"moves":142,"ms":37977}} +{"t":"2026-08-12T18:04:23.989Z","kind":"observation","tool":true,"payload":{"trial":21,"of":60,"i":20,"id":"p443k324","u":[-0.39,-1.12],"from":[2.66,1.38],"moves":111,"ms":9162}} +{"t":"2026-08-12T18:04:29.564Z","kind":"observation","tool":true,"payload":{"trial":22,"of":60,"i":21,"id":"p340k216","u":[-0.475,-1.573],"from":[-1.8,2.4],"moves":97,"ms":5578}} +{"t":"2026-08-12T18:04:34.712Z","kind":"observation","tool":true,"payload":{"trial":23,"of":60,"i":22,"id":"p414k133","u":[-0.38,-0.967],"from":[3,-0.05],"moves":108,"ms":5145}} +{"t":"2026-08-12T18:04:42.042Z","kind":"observation","tool":true,"payload":{"trial":24,"of":60,"i":23,"id":"p45k14","u":[-0.537,-1.058],"from":[0.54,2.95],"moves":81,"ms":7224}} +{"t":"2026-08-12T18:04:48.766Z","kind":"observation","tool":true,"payload":{"trial":25,"of":60,"i":24,"id":"p531k428","u":[-0.298,-0.737],"from":[-2.69,-1.33],"moves":117,"ms":6831}} +{"t":"2026-08-12T18:04:54.636Z","kind":"observation","tool":true,"payload":{"trial":26,"of":60,"i":25,"id":"p93k377","u":[-0.452,-0.785],"from":[2.35,1.87],"moves":114,"ms":5868}} +{"t":"2026-08-12T18:05:01.942Z","kind":"observation","tool":true,"payload":{"trial":27,"of":60,"i":26,"id":"p180k303","u":[-0.589,-1.233],"from":[1.3,-2.71],"moves":109,"ms":7265}} +{"t":"2026-08-12T18:05:07.867Z","kind":"observation","tool":true,"payload":{"trial":28,"of":60,"i":27,"id":"p336k355","u":[-0.483,-1.029],"from":[1.75,-2.44],"moves":90,"ms":5963}} +{"t":"2026-08-12T18:05:12.665Z","kind":"observation","tool":true,"payload":{"trial":29,"of":60,"i":28,"id":"p146k477","u":[-0.568,-0.476],"from":[-2.44,-1.75],"moves":93,"ms":4797}} +{"t":"2026-08-12T18:05:27.327Z","kind":"observation","tool":true,"payload":{"trial":30,"of":60,"i":29,"id":"p24k85","u":[-0.554,-1.042],"from":[0.05,3],"moves":114,"ms":14655}} +{"t":"2026-08-12T18:05:34.854Z","kind":"observation","tool":true,"payload":{"trial":31,"of":60,"i":30,"id":"p380k492","u":[-2.197,-1.81],"from":[-0.72,2.91],"moves":117,"ms":7422}} +{"t":"2026-08-12T18:05:40.194Z","kind":"observation","tool":true,"payload":{"trial":32,"of":60,"i":31,"id":"p115k25","u":[-0.443,-0.895],"from":[-2.8,1.07],"moves":103,"ms":5446}} +{"t":"2026-08-12T18:05:45.388Z","kind":"observation","tool":true,"payload":{"trial":33,"of":60,"i":32,"id":"p245k425","u":[-0.416,-0.944],"from":[2.73,1.24],"moves":57,"ms":5189}} +{"t":"2026-08-12T18:05:58.891Z","kind":"observation","tool":true,"payload":{"trial":34,"of":60,"i":33,"id":"p531k141","u":[-0.617,-1.386],"from":[-2.85,-0.93],"moves":69,"ms":13504}} +{"t":"2026-08-12T18:06:06.186Z","kind":"observation","tool":true,"payload":{"trial":35,"of":60,"i":34,"id":"p179k373","u":[-0.73,-0.714],"from":[-2.99,-0.28],"moves":50,"ms":7295}} +{"t":"2026-08-12T18:06:18.040Z","kind":"observation","tool":true,"payload":{"trial":36,"of":60,"i":35,"id":"p115k199","u":[-0.372,-0.937],"from":[-1.04,2.81],"moves":97,"ms":11850}} +{"t":"2026-08-12T18:06:23.731Z","kind":"observation","tool":true,"payload":{"trial":37,"of":60,"i":36,"id":"p120k101","u":[-1.25,-1.23],"from":[-3,0.12],"moves":64,"ms":5691}} +{"t":"2026-08-12T18:06:28.734Z","kind":"observation","tool":true,"payload":{"trial":38,"of":60,"i":37,"id":"p218k0","u":[-0.276,-0.395],"from":[1.81,2.39],"moves":124,"ms":4757}} +{"t":"2026-08-12T18:06:35.986Z","kind":"observation","tool":true,"payload":{"trial":39,"of":60,"i":38,"id":"p107k399","u":[-0.081,-0.765],"from":[-2.59,1.51],"moves":113,"ms":7499}} +{"t":"2026-08-12T18:06:41.844Z","kind":"observation","tool":true,"payload":{"trial":40,"of":60,"i":39,"id":"p314k75","u":[-0.321,-1.066],"from":[-2.9,-0.77],"moves":91,"ms":5860}} +{"t":"2026-08-12T18:06:53.815Z","kind":"observation","tool":true,"payload":{"trial":41,"of":60,"i":40,"id":"p423k272","u":[-0.397,-0.984],"from":[-0.24,2.99],"moves":136,"ms":11968}} +{"t":"2026-08-12T18:07:00.432Z","kind":"observation","tool":true,"payload":{"trial":42,"of":60,"i":41,"id":"p552k313","u":[-1.517,-1.295],"from":[0.57,-2.94],"moves":91,"ms":6616}} +{"t":"2026-08-12T18:07:08.455Z","kind":"observation","tool":true,"payload":{"trial":43,"of":60,"i":42,"id":"p443k449","u":[-0.357,-0.949],"from":[2.57,-1.55],"moves":97,"ms":8025}} +{"t":"2026-08-12T18:07:15.201Z","kind":"observation","tool":true,"payload":{"trial":44,"of":60,"i":43,"id":"p8k501","u":[-0.794,-0.909],"from":[2.86,0.92],"moves":100,"ms":6743}} +{"t":"2026-08-12T18:07:20.659Z","kind":"observation","tool":true,"payload":{"trial":45,"of":60,"i":44,"id":"p531k141","u":[-0.583,-1.417],"from":[-2.87,-0.88],"moves":54,"ms":5459}} +{"t":"2026-08-12T18:07:29.154Z","kind":"observation","tool":true,"payload":{"trial":46,"of":60,"i":45,"id":"p464k477","u":[-2.122,-1.109],"from":[1.01,-2.82],"moves":65,"ms":8494}} +{"t":"2026-08-12T18:07:33.427Z","kind":"observation","tool":true,"payload":{"trial":47,"of":60,"i":46,"id":"p179k373","u":[-0.768,-0.646],"from":[2.95,-0.57],"moves":90,"ms":4253}} +{"t":"2026-08-12T18:15:28.758Z","kind":"observation","tool":true,"payload":{"trial":48,"of":60,"i":47,"id":"p591k154","u":[-2.007,-2.269],"from":[1.75,2.44],"moves":69,"ms":475341}} +{"t":"2026-08-12T18:15:35.972Z","kind":"observation","tool":true,"payload":{"trial":49,"of":60,"i":48,"id":"p379k356","u":[-1.417,-1.609],"from":[2.97,-0.43],"moves":74,"ms":7214}} +{"t":"2026-08-12T18:15:42.041Z","kind":"observation","tool":true,"payload":{"trial":50,"of":60,"i":49,"id":"p146k420","u":[-1.235,-1.274],"from":[2.18,-2.06],"moves":150,"ms":6068}} +{"t":"2026-08-12T18:15:47.677Z","kind":"observation","tool":true,"payload":{"trial":51,"of":60,"i":50,"id":"p414k457","u":[-1.726,-2.606],"from":[-3,-0.11],"moves":78,"ms":5631}} +{"t":"2026-08-12T18:16:02.302Z","kind":"observation","tool":true,"payload":{"trial":52,"of":60,"i":51,"id":"p157k39","u":[-1.256,-1.131],"from":[2.77,1.15],"moves":97,"ms":14627}} +{"t":"2026-08-12T18:16:08.798Z","kind":"observation","tool":true,"payload":{"trial":53,"of":60,"i":52,"id":"p282k145","u":[-0.831,-1.266],"from":[0.7,-2.92],"moves":71,"ms":6492}} +{"t":"2026-08-12T18:16:23.011Z","kind":"observation","tool":true,"payload":{"trial":54,"of":60,"i":53,"id":"p443k449","u":[-0.381,-0.88],"from":[-1.22,2.74],"moves":56,"ms":14214}} +{"t":"2026-08-12T18:16:28.954Z","kind":"observation","tool":true,"payload":{"trial":55,"of":60,"i":54,"id":"p336k355","u":[-0.489,-1.036],"from":[0.85,-2.88],"moves":121,"ms":5945}} +{"t":"2026-08-12T18:16:32.958Z","kind":"observation","tool":true,"payload":{"trial":56,"of":60,"i":55,"id":"p93k154","u":[-0.674,-0.661],"from":[1.43,-2.64],"moves":98,"ms":4004}} +{"t":"2026-08-12T18:16:41.490Z","kind":"observation","tool":true,"payload":{"trial":57,"of":60,"i":56,"id":"p423k272","u":[-0.413,-0.991],"from":[2.79,1.09],"moves":203,"ms":8529}} +{"t":"2026-08-12T18:16:48.603Z","kind":"observation","tool":true,"payload":{"trial":58,"of":60,"i":57,"id":"p314k410","u":[-0.46,-0.846],"from":[-2.94,-0.57],"moves":129,"ms":7117}} +{"t":"2026-08-12T18:16:55.032Z","kind":"observation","tool":true,"payload":{"trial":59,"of":60,"i":58,"id":"p223k145","u":[-0.429,-0.805],"from":[2.79,-1.1],"moves":130,"ms":6404}} +{"t":"2026-08-12T18:17:00.954Z","kind":"artifact","name":"mark-placements-23.json","path":"packages/etl/out/mark-placements-23.json"} +{"t":"2026-08-12T18:17:00.955Z","kind":"observation","tool":true,"payload":{"trial":60,"of":60,"i":59,"id":"p157k443","u":[-0.248,-0.844],"from":[3,0.1],"moves":87,"ms":5935}} diff --git a/docs/validation/sessions/2026-08-12T2228-placement-correction-by-eye.jsonl b/docs/validation/sessions/2026-08-12T2228-placement-correction-by-eye.jsonl new file mode 100644 index 00000000..8b8d940f --- /dev/null +++ b/docs/validation/sessions/2026-08-12T2228-placement-correction-by-eye.jsonl @@ -0,0 +1,10 @@ +{"t":"2026-08-12T22:28:02.066Z","kind":"session","check":"placement-correction-by-eye","title":"Placement correction by eye — a hundred forced choices between the corrected box and the one we ship","commit":"5020b41","on":"darwin node 20.19.4","stepsTotal":8} +{"t":"2026-08-13T00:55:32.778Z","kind":"observation","tool":true,"payload":{"trial":1,"of":100,"i":0,"id":"p177k293","choice":1,"ms":893}} +{"t":"2026-08-13T00:55:33.245Z","kind":"observation","tool":true,"payload":{"trial":2,"of":100,"i":1,"id":"p491k284","choice":0,"ms":468}} +{"t":"2026-08-13T00:55:33.790Z","kind":"observation","tool":true,"payload":{"trial":3,"of":100,"i":2,"id":"p372k363","choice":0,"ms":551}} +{"t":"2026-08-13T00:55:34.237Z","kind":"observation","tool":true,"payload":{"trial":4,"of":100,"i":3,"id":"p238k265","choice":1,"ms":446}} +{"t":"2026-08-13T00:55:34.490Z","kind":"observation","tool":true,"payload":{"trial":5,"of":100,"i":4,"id":"p41k88","choice":1,"ms":254}} +{"t":"2026-08-13T00:55:35.419Z","kind":"observation","tool":true,"payload":{"trial":6,"of":100,"i":5,"id":"p11k291","choice":1,"ms":926}} +{"t":"2026-08-13T00:55:38.083Z","kind":"observation","tool":true,"payload":{"trial":7,"of":100,"i":6,"id":"p296k36","choice":1,"ms":2667}} +{"t":"2026-08-13T00:55:39.985Z","kind":"observation","tool":true,"payload":{"trial":8,"of":100,"i":7,"id":"p461k273","choice":1,"ms":1901}} +{"t":"2026-08-13T00:55:41.700Z","kind":"observation","tool":true,"payload":{"trial":9,"of":100,"i":8,"id":"p117k109","choice":1,"ms":1714}} diff --git a/docs/validation/sessions/2026-08-13T2106-placement-what-kind-of-wrong.jsonl b/docs/validation/sessions/2026-08-13T2106-placement-what-kind-of-wrong.jsonl new file mode 100644 index 00000000..d4211309 --- /dev/null +++ b/docs/validation/sessions/2026-08-13T2106-placement-what-kind-of-wrong.jsonl @@ -0,0 +1,62 @@ +{"t":"2026-08-13T21:06:45.323Z","kind":"session","check":"placement-what-kind-of-wrong","title":"What kind of wrong — the marks nothing can place, named one at a time instead of counted","commit":"5020b41","on":"darwin node 20.19.4","stepsTotal":13} +{"t":"2026-08-13T21:45:25.486Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"7:575","page":7,"line":15,"name":"successive dammatan","rule":"ink"}} +{"t":"2026-08-13T21:45:28.960Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"10:509","page":10,"line":15,"name":"hamza","rule":"ink"}} +{"t":"2026-08-13T21:45:31.980Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"19:382","page":19,"line":10,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:45:34.733Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"20:12","page":20,"line":1,"name":"superscript alef","rule":"ink"}} +{"t":"2026-08-13T21:45:35.995Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"20:74","page":20,"line":2,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:45:36.838Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"23:337","page":23,"line":10,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:45:39.982Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"29:502","page":29,"line":14,"name":"shadda","rule":"ink"}} +{"t":"2026-08-13T21:45:41.038Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"39:66","page":39,"line":2,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:45:42.930Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"53:462","page":53,"line":13,"name":"wasla","rule":"ink"}} +{"t":"2026-08-13T21:45:43.837Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"74:340","page":74,"line":10,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:45:44.727Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"84:182","page":84,"line":6,"name":"successive fathatan","rule":"ink"}} +{"t":"2026-08-13T21:45:45.425Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"102:539","page":102,"line":15,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:45:46.834Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"106:215","page":106,"line":8,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:45:48.378Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"108:323","page":108,"line":10,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:45:49.416Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"115:92","page":115,"line":3,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:45:50.308Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"126:305","page":126,"line":9,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:45:50.949Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"135:251","page":135,"line":7,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:45:52.022Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"154:215","page":154,"line":6,"name":"wasla","rule":"ink"}} +{"t":"2026-08-13T21:45:53.097Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"161:21","page":161,"line":1,"name":"damma","rule":"ink"}} +{"t":"2026-08-13T21:45:54.311Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"162:301","page":162,"line":8,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:45:56.450Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"221:481","page":221,"line":15,"name":"damma","rule":"ink"}} +{"t":"2026-08-13T21:45:57.445Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"232:509","page":232,"line":15,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:45:58.683Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"241:308","page":241,"line":8,"name":"small meem","rule":"ink"}} +{"t":"2026-08-13T21:45:59.868Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"249:43","page":249,"line":4,"name":"superscript alef","rule":"ink"}} +{"t":"2026-08-13T21:46:00.966Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"250:414","page":250,"line":12,"name":"wasla","rule":"ink"}} +{"t":"2026-08-13T21:46:01.784Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"252:512","page":252,"line":13,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:02.521Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"256:295","page":256,"line":10,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:03.104Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"267:217","page":267,"line":9,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:46:03.868Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"287:283","page":287,"line":8,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:04.686Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"301:359","page":301,"line":10,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:46:06.844Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"320:74","page":320,"line":3,"name":"hamza","rule":"ink"}} +{"t":"2026-08-13T21:46:07.783Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"323:51","page":323,"line":2,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:08.565Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"338:442","page":338,"line":12,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:46:09.575Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"370:329","page":370,"line":10,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:46:10.284Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"371:164","page":371,"line":5,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:46:11.300Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"383:96","page":383,"line":3,"name":"superscript alef","rule":"ink"}} +{"t":"2026-08-13T21:46:12.521Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"390:70","page":390,"line":2,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:13.680Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"407:280","page":407,"line":8,"name":"hamza","rule":"ink"}} +{"t":"2026-08-13T21:46:15.080Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"420:396","page":420,"line":10,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:16.278Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"423:589","page":423,"line":15,"name":"successive fathatan","rule":"ink"}} +{"t":"2026-08-13T21:46:17.254Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"425:349","page":425,"line":10,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:18.268Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"435:2","page":435,"line":1,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:46:19.472Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"437:270","page":437,"line":8,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:20.263Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"454:441","page":454,"line":11,"name":"damma","rule":"ink"}} +{"t":"2026-08-13T21:46:21.245Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"494:348","page":494,"line":10,"name":"shadda","rule":"ink"}} +{"t":"2026-08-13T21:46:23.418Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"499:337","page":499,"line":10,"name":"damma","rule":"ink"}} +{"t":"2026-08-13T21:46:24.608Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"512:27","page":512,"line":1,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:26.060Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"512:67","page":512,"line":2,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:27.093Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"519:452","page":519,"line":12,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:28.313Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"525:519","page":525,"line":15,"name":"damma","rule":"ink"}} +{"t":"2026-08-13T21:46:29.295Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"535:326","page":535,"line":10,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:30.208Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"538:65","page":538,"line":2,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:31.046Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"546:50","page":546,"line":2,"name":"sukun","rule":"ink"}} +{"t":"2026-08-13T21:46:32.005Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"551:5","page":551,"line":1,"name":"shadda","rule":"ink"}} +{"t":"2026-08-13T21:46:33.063Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"561:445","page":561,"line":13,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:46:33.689Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"570:302","page":570,"line":10,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:35.913Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"584:320","page":584,"line":9,"name":"damma","rule":"ink"}} +{"t":"2026-08-13T21:46:36.935Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"586:356","page":586,"line":13,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:37.834Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"591:36","page":591,"line":3,"name":"kasra","rule":"ink"}} +{"t":"2026-08-13T21:46:38.634Z","kind":"report","tool":true,"payload":{"kind":"looks-right","id":"591:43","page":591,"line":4,"name":"fatha","rule":"ink"}} +{"t":"2026-08-13T21:46:39.975Z","kind":"artifact","name":"mark-report-placed.23.json","path":"packages/etl/out/mark-report-placed.23.json"} diff --git a/docs/validation/sessions/README.md b/docs/validation/sessions/README.md index f3358bab..fa2cd9b5 100644 --- a/docs/validation/sessions/README.md +++ b/docs/validation/sessions/README.md @@ -27,6 +27,14 @@ the status — and a transcript is the working underneath one entry in it, the w decide whether a check passed.** A person reads one when they want to know how a verdict was arrived at. +They are also not the **answers**. A check whose tool hands back a file of what a person +actually said puts that file in [`../rulings/`](../rulings/), named for the same sitting, and +a scorer re-reads it to reproduce the verdict on another machine. A transcript says *a sitting +happened and here is how it went*; a ruling says *here is what was answered*. They pair by +stem — `2026-08-12T1650-placement-residual-by-hand.jsonl` beside +`2026-08-12T1650-placement-residual-by-hand.seed23.json` — and the README next door says why +a seed belongs in the name. + They also carry no score. The session page shows how far through you are and whether your last answer was saved, and nothing else, because at least one check here — `placement-correction-by-eye` — is a blind forced choice whose whole validity rests on diff --git a/package.json b/package.json index 9749dee8..557a88e9 100644 --- a/package.json +++ b/package.json @@ -20,32 +20,37 @@ "test:e2e": "pnpm --filter @hifth/web test:e2e", "etl": "pnpm --filter @hifth/etl etl", "audit:corpus": "node packages/etl/scripts/audit-corpus.mjs", + "audit:sittings": "node packages/etl/scripts/audit-mark-sittings.mjs", "gate:budget": "node scripts/gate-budget.mjs", "gate:notext": "node scripts/gate-notext.mjs", "gate:license": "node scripts/gate-license.mjs", "gate:license-copy": "node scripts/gate-license-copy.mjs", + "gate:notices": "node scripts/gate-notices.mjs", "gate:validation": "node scripts/gate-validation.mjs", "gate:verified-edges": "node scripts/gate-verified-edges.mjs", "gate:edges": "node scripts/gate-edges.mjs", "gate:text-sources": "node scripts/gate-text-sources.mjs", + "gate:scripture": "node scripts/gate-scripture.mjs", "gate:ci-artifacts": "node scripts/gate-ci-artifacts.mjs", "gate:golden-env": "node scripts/gate-golden-env.mjs", "gate:golden-size": "node scripts/gate-golden-size.mjs", "gate:assets": "node scripts/gate-assets.mjs", "gate:pages": "node scripts/gate-pages.mjs", + "gate:boxes": "node scripts/gate-boxes.mjs", "gate:words": "node scripts/gate-words.mjs", "gate:align": "node scripts/gate-align.mjs", "gate:map": "node scripts/gate-map.mjs", "gate:revision-privacy": "node scripts/gate-revision-privacy.mjs", "gate:use-cases": "node scripts/gate-use-cases.mjs", "gate:issues": "node scripts/gate-issues.mjs", + "gate:tasks": "node scripts/gate-tasks.mjs", "gate:decisions": "node scripts/gate-decisions.mjs", "gate:quran-meta": "node scripts/gate-quran-meta.mjs", "gate:tajweed": "node scripts/gate-tajweed.mjs", "gate:i18n": "node scripts/gate-i18n.mjs", "gate:params": "node scripts/gate-params.mjs", "gate:gates": "node scripts/gate-gates.mjs", - "gates": "pnpm gate:text-sources && pnpm gate:notext && pnpm gate:license && pnpm gate:license-copy && pnpm gate:validation && pnpm gate:verified-edges && pnpm gate:edges && pnpm gate:gates && pnpm gate:ci-artifacts && pnpm gate:golden-env && pnpm gate:golden-size && pnpm gate:assets && pnpm gate:pages && pnpm gate:words && pnpm gate:align && pnpm gate:map && pnpm gate:use-cases && pnpm gate:issues && pnpm gate:decisions && pnpm gate:quran-meta && pnpm gate:tajweed && pnpm gate:revision-privacy && pnpm gate:i18n && pnpm gate:params && pnpm gate:budget", + "gates": "pnpm gate:text-sources && pnpm gate:scripture && pnpm gate:notext && pnpm gate:license && pnpm gate:license-copy && pnpm gate:notices && pnpm gate:validation && pnpm gate:verified-edges && pnpm gate:edges && pnpm gate:gates && pnpm gate:ci-artifacts && pnpm gate:golden-env && pnpm gate:golden-size && pnpm gate:assets && pnpm gate:pages && pnpm gate:boxes && pnpm gate:words && pnpm gate:align && pnpm gate:map && pnpm gate:use-cases && pnpm gate:issues && pnpm gate:tasks && pnpm gate:decisions && pnpm gate:quran-meta && pnpm gate:tajweed && pnpm gate:revision-privacy && pnpm gate:i18n && pnpm gate:params && pnpm gate:budget", "align": "node packages/etl/scripts/build-alignment.mjs", "probe:ligature-print": "node packages/etl/scripts/probe-ligature-print.mjs", "probe:word-registration": "node packages/etl/scripts/probe-word-registration.mjs", @@ -58,6 +63,11 @@ "adjudicate:score": "node packages/etl/scripts/score-mark-adjudication.mjs", "nudge:marks": "node packages/etl/scripts/build-mark-nudge.mjs", "nudge:score": "node packages/etl/scripts/score-mark-nudge.mjs", + "report:marks": "node packages/etl/scripts/build-mark-report.mjs", + "report:score": "node packages/etl/scripts/score-mark-report.mjs", + "report:settle": "node packages/etl/scripts/settle-mark-report.mjs", + "sit:index": "node packages/etl/scripts/build-sittings-index.mjs", + "sit:serve": "node packages/etl/scripts/serve-sittings.mjs", "sample:edges": "node packages/etl/scripts/sample-edges.mjs", "guide": "node scripts/build-validation-guide.mjs", "session": "node scripts/session.mjs", @@ -70,8 +80,13 @@ "use-cases:doc": "node scripts/build-use-cases.mjs", "issues": "node scripts/gate-issues.mjs --list", "issues:doc": "node scripts/build-issues-doc.mjs", + "box-sweep": "node scripts/build-box-sweep.mjs", + "tasks": "node scripts/gate-tasks.mjs --list", + "tasks:doc": "node scripts/build-tasks-doc.mjs", "decisions": "node scripts/gate-decisions.mjs --list", - "decisions:doc": "node scripts/build-decisions-doc.mjs" + "decisions:doc": "node scripts/build-decisions-doc.mjs", + "decisions:board": "node scripts/build-decision-board.mjs", + "artifacts": "node scripts/artifact-sweep.mjs" }, "pnpm": { "overrides": { diff --git a/packages/core/src/highlighter.test.ts b/packages/core/src/highlighter.test.ts index e407e10b..a481e453 100644 --- a/packages/core/src/highlighter.test.ts +++ b/packages/core/src/highlighter.test.ts @@ -515,6 +515,32 @@ describe("Highlighter marker swipes", () => { for (const m of marks) expect(m.getAttribute("y1")).toBe(m.getAttribute("y2")); }); + it("splits a long ayah's fused middle box into one line per line (#4, #12)", () => { + // 2:249 as vendored (`verse-256`, page 41): a one-line head, six full-width + // middle lines the print fused into one 218-unit box, and a one-line tail. + // Before the fix the box was drawn as a single fat band — the "blob" a + // reader saw instead of six lines. The line height is not in the ayah (the + // box is taller than any line it contains); the highlighter reads it off the + // page's other polygons, which are one line each. + document.body.innerHTML = ""; + svg = makeInkSvg("M0 6.3h345v36H0Zm0 36h345v218.4H0Zm233.4 218.4H345v36H233.4Z"); + hl = new Highlighter(svg, resolver, 7); + hl.highlight("quran/hafs-kfqc/2:38", "sel", "selection"); + + const marks = [...svg.querySelectorAll<SVGElement>("#hifth-overlay .hl-sel")]; + expect(marks).toHaveLength(8); + // No band is a blob: every stroke is about one line thick, never the whole + // box. The old bug drew one stroke 218 × 0.72 ≈ 157 units wide. + for (const m of marks) { + expect(Number(m.getAttribute("stroke-width"))).toBeLessThan(36 * 1.5); + } + // The stagger runs unbroken across all eight lines, so the wipe still reads + // as one pen crossing the ayah top to bottom. + expect(marks.map((m) => m.style.getPropertyValue("--hl-i"))).toEqual([ + "0", "1", "2", "3", "4", "5", "6", "7", + ]); + }); + it("tags swipes `hl-ink`, which is what the stylesheet keys the pen off", () => { hl.highlight("quran/hafs-kfqc/2:38", "sel", "selection"); const marks = [...svg.querySelectorAll("#hifth-overlay .hl-sel")]; diff --git a/packages/core/src/highlighter.ts b/packages/core/src/highlighter.ts index 8d70672b..e26462e6 100644 --- a/packages/core/src/highlighter.ts +++ b/packages/core/src/highlighter.ts @@ -26,7 +26,7 @@ */ import { TAP_SLOP_PX } from "./gestures.js"; -import { swipesFromPath, swipesFromRects, type Swipe } from "./ink.js"; +import { pageLineHeight, swipesFromPath, swipesFromRects, type Swipe } from "./ink.js"; import type { Resolver } from "./resolver.js"; import { TAJWEED_CLASS_PREFIX, @@ -155,6 +155,13 @@ export class Highlighter { /** The applied skin, and L3's rule lookup for it (spec §8; see `setSkin`). */ private currentSkin: SkinId = "plain"; private skinLookup: TajweedLookup | null = null; + /** + * The page's line height in SVG units, computed once from its polygons and + * cached — the pen needs it to split a merged multi-line rectangle back into + * lines (see ink.ts). `undefined` means "not computed yet"; the computation + * itself may land on `null` (nothing parsed), which is a valid, cached answer. + */ + private lineHeightCache: number | null | undefined; constructor(svg: SVGSVGElement, resolver: Resolver, page: number, opts?: { labelFor?: LabelFor }) { this.svg = svg; @@ -242,6 +249,21 @@ export class Highlighter { return Array.from(this.svg.querySelectorAll<SVGElement>(POLYGON_SELECTOR)); } + /** + * The page's line height, computed from its polygons the first time the pen + * needs it and cached for the life of the page (a page's geometry does not + * change under it). Read through the pen's split; see {@link pageLineHeight}. + */ + private inkLineHeight(): number | undefined { + if (this.lineHeightCache === undefined) { + const ds = this.polygons() + .map((p) => p.getAttribute("d")) + .filter((d): d is string => d !== null); + this.lineHeightCache = pageLineHeight(ds); + } + return this.lineHeightCache ?? undefined; + } + /** * Make polygons a keyboard-operable list (spec §7 a11y). Each carries * `role="button"`, `tabindex="0"` (Home for the first, so the page has one @@ -335,7 +357,9 @@ export class Highlighter { const src = this.svg.querySelector<SVGElement>(`#${cssEscape(id)}`); if (!src) return []; - const swipes = INKED.has(style) ? swipesFromPath(src.getAttribute("d") ?? "") : null; + const swipes = INKED.has(style) + ? swipesFromPath(src.getAttribute("d") ?? "", this.inkLineHeight()) + : null; if (swipes) return this.drawSwipes(swipes, style, group); const clone = src.cloneNode(true) as SVGElement; @@ -365,7 +389,8 @@ export class Highlighter { // One element per swipe rather than one path for the whole ayah: line // heights differ between lines, and stroke-width is per element, so a // single path would have to pick one thickness and be wrong on the rest. - // An ayah spans at most three lines, so this is at most three nodes. + // One node per line the ayah occupies — usually a handful, more for the + // long ayahs the print fuses into one tall box and the pen splits back. return swipes.map((s, i) => { const line = document.createElementNS(SVG_NS, "line"); // Drawn from the RIGHT end to the left — x1 takes the larger x. A line @@ -409,7 +434,10 @@ export class Highlighter { highlightRects(rects: readonly Rect[], style: StyleToken, group: GroupId): void { this.clear(group); if (rects.length === 0) return; - this.drawn.set(group, this.drawSwipes(swipesFromRects(rects), style, group)); + this.drawn.set( + group, + this.drawSwipes(swipesFromRects(rects, this.inkLineHeight()), style, group), + ); } /** Remove every highlight drawn for a group. */ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 556569b2..62787132 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -101,14 +101,7 @@ export { type StageFit, } from "./view.js"; -export { - VERSE_TEXT, - verseTokens, - diffPair, - type DiffClass, - type DiffToken, - type DiffSide, -} from "./verse-text.js"; +export { wordDiff, divergentRuns, type DiffSide, type WordDiff } from "./verse-diff.js"; export { serializeState, diff --git a/packages/core/src/ink.test.ts b/packages/core/src/ink.test.ts index efc78c62..d793544b 100644 --- a/packages/core/src/ink.test.ts +++ b/packages/core/src/ink.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { swipesFromPath } from "./ink.js"; +import { pageLineHeight, swipesFromPath, swipesFromRects } from "./ink.js"; /** * The real thing, from apps/web/public/assets/pages/hafs-kfqc/7.svg — a @@ -8,6 +8,26 @@ import { swipesFromPath } from "./ink.js"; */ const VERSE_45 = "M0 8.5h345v38H0Zm79.5 38H345v38.2H79.5Z"; +/** + * The real 2:249, from page 41 (`verse-256`) — the ayah in the bug report. Its + * middle six lines are all full width, so the print fuses them into one + * 218.4-unit box between a one-line head and a one-line tail. Filled or drawn as + * one swipe, that box is the "blob" a reader sees instead of six lines. + */ +const VERSE_2_249 = "M0 6.3h345v36H0Zm0 36h345v218.4H0Zm233.4 218.4H345v36H233.4Z"; + +/** + * The real 10:44, from page 214 (`verse-1408`) — the ayah in the bug report + * (#16). It is an ordinary two-line ayah, but its short second line closes a + * tenth of a unit off its own left edge: it starts at x 259.7 and its closing + * run lands at 345 − 85.4 = 259.6. That is pure coordinate rounding (the print + * quantises to a tenth), yet a stricter tolerance than the rounding step read + * it as "not a rectangle" and dropped the whole ayah to the box fallback. 110 + * ayat across the corpus miss their close by up to 0.2 for the same reason; + * this was simply the first one reported. + */ +const VERSE_10_44 = "M0 45h345v35.8H0Zm259.7 35.8H345v35.8h-85.4Z"; + describe("swipesFromPath", () => { it("lays one swipe per line, centred in the line box", () => { const swipes = swipesFromPath(VERSE_45); @@ -50,6 +70,20 @@ describe("swipesFromPath", () => { expect(s.width).toBeCloseTo(30 * 0.72, 5); }); + it("accepts a rectangle whose rounded closing edge misses by a tenth (10:44)", () => { + // The print rounds coordinates to a tenth, so a true rectangle's closing run + // can land a fraction off its own left edge. 10:44's tail starts at 259.7 and + // closes at 259.6 — a 0.1 miss that is rounding, not a real corner. It must + // still draw as a marker (two lines), not fall back to a box. + const swipes = swipesFromPath(VERSE_10_44); + expect(swipes).toHaveLength(2); + // The tail line spans x 259.7 → 345, ~85 wide; the centreline sits entirely + // in that right-hand span (both caps past 259.7), which is what "the last two + // words of the line" looks like — proof the rect was read, not discarded. + expect(swipes![1].x1).toBeGreaterThan(259.7); + expect(swipes![1].x2 - swipes![1].x1).toBeGreaterThan(50); + }); + /** * The fallback contract. Each of these returns null so the highlighter clones * the source path instead — a boxy highlight, but never a missing one. Loop @@ -60,6 +94,7 @@ describe("swipesFromPath", () => { ["a genuine polygon", "M0 0L10 5L20 0Z"], ["an arc", "M0 0A10 10 0 0 1 20 0Z"], ["a rect that does not close back to its left edge", "M0 0h100v38H40Z"], + ["a rect that misses its close by more than a rounding step", "M0 0h100v38h-99.5Z"], ["a zero-height run", "M0 0h100v0H0Z"], ["a curve", "M0 0C10 10 20 10 30 0Z"], ["empty", ""], @@ -73,3 +108,78 @@ describe("swipesFromPath", () => { expect(swipesFromPath("M0 0h100v38H0ZM0 38L50 60L100 38Z")).toBeNull(); }); }); + +/** + * The multi-line fix (#4, #12). A four-or-more-line ayah's middle lines are all + * full width, so the print may fuse them into one tall box; drawn as one swipe + * that box is a single fat band down the centre of a paragraph — the "blob". + * Told the page's line height, the pen splits the box back into its lines. + */ +describe("splitting a fused multi-line rectangle", () => { + const LINE = 36; // the modal line height of this print + + it("draws one swipe per line for 2:249, not one blob over the middle", () => { + const swipes = swipesFromPath(VERSE_2_249, LINE)!; + // One head line + six fused middle lines + one tail line. + expect(swipes).toHaveLength(8); + + // The bug was a band as tall as the whole middle box; every band is now + // about one line thick, so none is wider than a line and a half. + for (const s of swipes) expect(s.width).toBeLessThan(LINE * 1.5); + + // The six middle bands are evenly stacked one line apart, filling the box + // from y 42.3 to y 260.7 (218.4 / 6 = 36.4 each). + const middle = swipes.slice(1, 7); + const step = 218.4 / 6; + middle.forEach((s, i) => { + expect(s.y).toBeCloseTo(42.3 + (i + 0.5) * step, 5); + }); + }); + + it("splits an ayah that is nothing but one fused box (no partial line)", () => { + // `verse-3629` on page 431 is a single two-line full-width box, so there is + // no one-line rectangle in the ayah to infer a line height from — which is + // why the height is a page fact passed in, not one read off the ayah. + const swipes = swipesFromPath("M0 0h345v72H0Z", LINE)!; + expect(swipes).toHaveLength(2); + expect(swipes[0].y).toBeCloseTo(18, 5); + expect(swipes[1].y).toBeCloseTo(54, 5); + }); + + it("leaves a normal short ayah untouched — 38 against a 36 line is one line", () => { + // Rounding, not flooring: a line's box is a little taller than the modal + // line, and that must not read as two lines. + expect(swipesFromPath(VERSE_45, LINE)).toHaveLength(2); + }); + + it("without a line height, falls back to one swipe per rectangle (the blob)", () => { + // The old behaviour, kept for callers that cannot supply the height: the + // middle box stays one fat band. This is the state the fix improves on. + const swipes = swipesFromPath(VERSE_2_249)!; + expect(swipes).toHaveLength(3); + expect(swipes[1].width).toBeGreaterThan(LINE * 4); + }); + + it("does not split a word run's per-line bands, which are already one line", () => { + // The word selection arrives as one rectangle per line already, so telling + // the pen the line height must not double them. + const bands = [ + { x: 0, y: 0, width: 100, height: 36 }, + { x: 0, y: 36, width: 100, height: 36 }, + ]; + expect(swipesFromRects(bands, LINE)).toHaveLength(2); + }); +}); + +describe("pageLineHeight", () => { + it("is the most common rectangle height on the page", () => { + // Two ordinary one-line ayahs and 2:249's fused box: 36 wins outright, and + // the 218-tall box cannot outvote it. + const height = pageLineHeight([VERSE_45, "M0 0h345v36H0Z", VERSE_2_249]); + expect(height).toBe(36); + }); + + it("is null when nothing parses, so the pen keeps its one-swipe fallback", () => { + expect(pageLineHeight(["M0 0L10 5L20 0Z", ""])).toBeNull(); + }); +}); diff --git a/packages/core/src/ink.ts b/packages/core/src/ink.ts index 8bb9aaf6..d458bd9e 100644 --- a/packages/core/src/ink.ts +++ b/packages/core/src/ink.ts @@ -16,9 +16,23 @@ * roughly constant width along the middle of the line, softly capped at both * ends, and it does not reach the top and bottom of the line box — that is why * you can still see the ascenders and descenders of the lines above and below. - * So: one swipe per rectangle, centred vertically, `BAND` of the line height, + * So: one swipe per *line*, centred vertically, `BAND` of the line height, * round caps. * + * ## One line, not one rectangle + * + * "Per line" is not the same as "per rectangle", and an ayah that runs four or + * more lines is where they part. When several of an ayah's lines are full-width + * and sit directly on top of one another, the upstream optimiser is free to + * write them as a *single* tall rectangle — 2:249's middle spans six lines as + * one 218-unit-high box — and a few ayahs are nothing but one such box. Drawn + * as one swipe that is a single fat band down the centre of a paragraph: the + * "blob" a reader sees instead of a line each. So the pen is told the page's + * line height and splits any rectangle taller than a line back into the lines + * the optimiser fused, one swipe apiece. A caller that does not know the line + * height (or a rectangle already one line tall) gets exactly one swipe, as + * before — the split can only ever *add* the lines that were collapsed. + * * This is deliberately a *parser with a fallback*, not an assumption. Three * pages are vendored today and all 22 of their polygons are rect runs, but Loop * 4b vendors 601 more from the same upstream, and a corpus is not a contract. @@ -94,7 +108,7 @@ const RECT_RE = * happens to look plausible on some pages, so it is asserted by test rather * than left to inspection. */ -function rectsFromPath(d: string): InkRect[] | null { +export function rectsFromPath(d: string): InkRect[] | null { const subs = d.trim().split(/(?=[Mm])/).filter(Boolean); if (subs.length === 0) return null; @@ -126,9 +140,16 @@ function rectsFromPath(d: string): InkRect[] | null { const x1 = h1Cmd === "H" ? h1 : x0 + h1; const y1 = vCmd === "V" ? v : y0 + v; // The closing run must return to the rectangle's left edge for this to be a - // rectangle at all. If it does not, the shape is something else. + // rectangle at all — but only *close* to it. The print rounds every + // coordinate to a tenth, so a true rectangle's closing edge can miss its own + // left edge by up to ~0.2 through rounding alone; measured across the whole + // corpus, 110 ayah boxes miss by that much and none by more. A genuine + // non-rectangle — a slanted quadrilateral written in this same grammar — + // misses by whole units, not a fifth of one, so 0.3 admits the rounding and + // still rejects the real thing. This was 0.01 once, which dropped all 110 of + // those ayat to the box fallback; 10:44 was the first one anybody reported. const x2 = h2Cmd === "H" ? h2 : x1 + h2; - if (Math.abs(x2 - x0) > 0.01) return null; + if (Math.abs(x2 - x0) > 0.3) return null; const width = Math.abs(x1 - x0); const height = Math.abs(y1 - y0); @@ -153,26 +174,77 @@ function rectsFromPath(d: string): InkRect[] | null { * end of a line — collapses to a single point, which a round cap renders as a * dot of exactly the band's diameter. That is what a pen does when you tap it. */ -export function swipesFromPath(d: string): Swipe[] | null { +export function swipesFromPath(d: string, lineHeight?: number): Swipe[] | null { const rects = rectsFromPath(d); - return rects ? swipesFromRects(rects) : null; + return rects ? swipesFromRects(rects, lineHeight) : null; +} + +/** One marker stroke down the centre of one rectangle. */ +function bandOf({ x, y, width: w, height: h }: InkRect): Swipe { + const width = h * BAND; + const half = width / 2; + const x1 = x + half; + const x2 = x + w - half; + return x2 < x1 + ? { x1: x + w / 2, x2: x + w / 2, y: y + h / 2, width } + : { x1, x2, y: y + h / 2, width }; } /** - * The pen itself: one marker stroke per rectangle, wherever the rectangles came - * from — a parsed ayah polygon, or a word run's per-line bands. + * The pen itself: one marker stroke per line, wherever the rectangles came from + * — a parsed ayah polygon, or a word run's per-line bands. + * + * `lineHeight` is what turns "per rectangle" into "per line" (see this file's + * header): a rectangle the print fused out of several stacked full-width lines + * is split back into `round(height / lineHeight)` bands of equal height, so a + * six-line ayah reads as six passes of a pen and not one slab. Rounding, not + * flooring, because a line's box is not exactly the modal height — 38 against a + * 36 line is still one line, and only a rectangle past 1.5 lines rounds to two. + * Omit `lineHeight` (or pass a rectangle already one line tall) and the result + * is one swipe, unchanged — the split can only add the lines that were merged. * * Total, not partial: rectangles are already the shape this file understands, so * unlike `swipesFromPath` there is nothing here that can fail to be recognised. */ -export function swipesFromRects(rects: readonly InkRect[]): Swipe[] { - return rects.map(({ x, y, width: w, height: h }) => { - const width = h * BAND; - const half = width / 2; - const x1 = x + half; - const x2 = x + w - half; - return x2 < x1 - ? { x1: x + w / 2, x2: x + w / 2, y: y + h / 2, width } - : { x1, x2, y: y + h / 2, width }; +export function swipesFromRects(rects: readonly InkRect[], lineHeight?: number): Swipe[] { + return rects.flatMap((rect) => { + const lines = + lineHeight && lineHeight > 0 ? Math.max(1, Math.round(rect.height / lineHeight)) : 1; + if (lines === 1) return [bandOf(rect)]; + const h = rect.height / lines; + return Array.from({ length: lines }, (_, i) => + bandOf({ x: rect.x, y: rect.y + i * h, width: rect.width, height: h }), + ); }); } + +/** + * The page's line height, read off its own geometry: the most common rectangle + * height across every ayah polygon on the page. Nearly every rectangle is a + * single line, so the mode is that line — the merged multi-line boxes and the + * short end-of-ayah tails are both far outnumbered and cannot outvote it. Null + * only if nothing parsed, in which case the pen falls back to one swipe each. + * + * Measured per page rather than assumed, because line height is a fact about a + * print and this app is built to take a second print it has never seen. + */ +export function pageLineHeight(paths: Iterable<string>): number | null { + const counts = new Map<number, number>(); + for (const d of paths) { + const rects = rectsFromPath(d); + if (!rects) continue; + for (const r of rects) { + const k = Math.round(r.height); + counts.set(k, (counts.get(k) ?? 0) + 1); + } + } + let best: number | null = null; + let bestCount = 0; + for (const [h, n] of counts) { + if (n > bestCount) { + best = h; + bestCount = n; + } + } + return best; +} diff --git a/packages/core/src/verse-diff.test.ts b/packages/core/src/verse-diff.test.ts new file mode 100644 index 00000000..6bc689ba --- /dev/null +++ b/packages/core/src/verse-diff.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "vitest"; +import type { Edge } from "./adjacency.js"; +import { divergentRuns, wordDiff } from "./verse-diff.js"; + +/** The 2:48 ↔ 2:123 edge as it ships, which is the pair the old table existed for. */ +const EDGE: Edge = { + type: "mutashabih", + to: "quran/hafs-kfqc/2:123", + page: 19, + dir: { dSurah: 0, dPage: 12, sameJuz: true }, + span: { from: [1, 13] }, + toSpan: { from: [1, 13] }, +}; + +describe("wordDiff", () => { + it("reads both sides' shared run off the edge", () => { + const d = wordDiff(EDGE, "quran/hafs-kfqc/2:48"); + expect(d).toEqual({ + from: { key: "2:48", page: 7, shared: [1, 13] }, + to: { key: "2:123", page: 19, shared: [1, 13] }, + }); + }); + + it("derives the source page by undoing the edge's own page delta", () => { + // 19 − 12 = 7, and 2:48 is printed on 7. The edge is self-describing, so + // this cannot drift out of step with the page it names. + expect(wordDiff(EDGE, "2:48")?.from.page).toBe(7); + expect(wordDiff({ ...EDGE, page: 100, dir: { dSurah: 0, dPage: -5 } }, "2:48")?.from.page).toBe( + 105, + ); + }); + + it("strips an edition prefix and a word anchor from the source key", () => { + expect(wordDiff(EDGE, "quran/hafs-kfqc/2:48#w3-7")?.from.key).toBe("2:48"); + }); + + it("declines an edge that names no words on either side", () => { + // The common case, not a failure: 452 of 2,996 look-alike edges match in + // more than one place and so name none of them. The caller falls back. + const { span: _span, ...noSpan } = EDGE; + const { toSpan: _toSpan, ...noToSpan } = EDGE; + expect(wordDiff(noSpan, "2:48")).toBeNull(); + expect(wordDiff(noToSpan, "2:48")).toBeNull(); + }); + + it("declines an inverted range rather than painting it backwards", () => { + expect(wordDiff({ ...EDGE, span: { from: [9, 4] } }, "2:48")).toBeNull(); + }); +}); + +describe("divergentRuns", () => { + it("names the tail when the two ayahs share their opening", () => { + // 2:48 is 23 words on its page and shares its first 13 — so 14..23 differ. + expect(divergentRuns({ from: 1, to: 23 }, [1, 13])).toEqual([[14, 23]]); + expect(divergentRuns({ from: 1, to: 22 }, [1, 13])).toEqual([[14, 22]]); + }); + + it("names both ends when the shared run is in the middle", () => { + expect(divergentRuns({ from: 1, to: 20 }, [5, 15])).toEqual([ + [1, 4], + [16, 20], + ]); + }); + + it("names nothing when the whole of what is here is shared", () => { + expect(divergentRuns({ from: 1, to: 13 }, [1, 13])).toEqual([]); + }); + + it("clamps to what this page holds, for an ayah that runs onto the next", () => { + // The shared run is recorded over the whole ayah; only 1..8 is printed here. + expect(divergentRuns({ from: 1, to: 8 }, [1, 13])).toEqual([]); + // And where the ayah *starts* on this page part-way through its numbering. + expect(divergentRuns({ from: 9, to: 20 }, [1, 13])).toEqual([[14, 20]]); + }); +}); diff --git a/packages/core/src/verse-diff.ts b/packages/core/src/verse-diff.ts new file mode 100644 index 00000000..3e46c754 --- /dev/null +++ b/packages/core/src/verse-diff.ts @@ -0,0 +1,102 @@ +/** + * Which words two look-alike ayahs share, and which ones they do not. + * + * WHAT THIS REPLACED, AND WHY. Until now the "why are these two confusable" + * panel read from a hand-typed table of twelve ayahs' text, carried in this + * package as source. Three things were wrong with that. It was running scripture + * in a code repository, which is the shape takedown notices are routinely scoped + * to. It was typed in a plainer spelling than the mus'haf the reader is looking + * at, so the panel showed different letters from the page underneath it. And it + * covered twelve ayahs out of six thousand. + * + * None of that was necessary, because the corpus already ships the answer. Every + * look-alike edge carries the target's page and the matching word run on *both* + * sides — `span` and `toSpan`, in the print's own word numbering, the same + * numbering the word boxes use. So the shared stretch is a lookup, not a + * judgement, and what diverges is simply what is left over at either end: + * + * 2:48 → 2:123 page 19 span [1,13] toSpan [1,13] + * 2:48 words 1..23 on page 7 → 1–13 shared, 14–23 differ + * 2:123 words 1..22 on page 19 → 1–13 shared, 14–22 differ + * + * which is exactly what the typed table's per-token 0/1/2 classes encoded, for + * 2,544 pairs rather than twelve. The panel draws the page's own ink through + * those boxes, so the reader compares the printing rather than a transcription + * of it. + * + * This module holds no text and reaches no network: it is arithmetic on word + * indices. Fetching the pages and painting the ink is the view's job. + */ +import type { Edge } from "./adjacency.js"; +import type { WordSpanRange } from "./words.js"; + +/** `"quran/hafs-kfqc/2:48#w3-7"` → `"2:48"`. */ +function bareKey(key: string): string { + const tail = key.slice(key.lastIndexOf("/") + 1); + const hash = tail.indexOf("#"); + return hash === -1 ? tail : tail.slice(0, hash); +} + +/** One side of the comparison: an ayah, the page it is printed on, and the run it shares. */ +export interface DiffSide { + /** Bare `"2:48"`, ready for {@link WordIndex}. */ + readonly key: string; + /** The page whose word shard and artwork this side is drawn from. */ + readonly page: number; + /** Inclusive print word indices the two ayahs have in common. */ + readonly shared: readonly [number, number]; +} + +/** The two sides of a look-alike comparison. */ +export interface WordDiff { + readonly from: DiffSide; + readonly to: DiffSide; +} + +/** + * The comparison an edge describes, or `null` when it describes none. + * + * `null` is the common case and not a failure: `build-adjacency.mjs` emits a + * span only where the shared run occurs in exactly one place on *both* sides, + * because naming one of several occurrences would be a guess. 2,544 of 2,996 + * look-alike edges carry one; the rest make no claim about where they match, and + * a caller shows its plain note instead — the same fallback the twelve-ayah + * table left in place for every pair it did not cover. + * + * The source page is read off `dir.dPage`, which is the target's page minus the + * source's, rather than passed in. The edge is self-describing that way, and the + * arithmetic cannot drift out of step with the page the edge actually names. + */ +export function wordDiff(edge: Edge, fromKey: string): WordDiff | null { + if (!edge.span || !edge.toSpan) return null; + const from = edge.span.from; + const to = edge.toSpan.from; + if (from[1] < from[0] || to[1] < to[0]) return null; + return { + from: { key: bareKey(fromKey), page: edge.page - edge.dir.dPage, shared: from }, + to: { key: bareKey(edge.to), page: edge.page, shared: to }, + }; +} + +/** + * The runs of an ayah that are *not* shared, given everything of it that is on + * this page. + * + * Returned as ranges rather than as a set of indices because the caller paints + * them with {@link WordIndex.bandsFor}, which collapses a run to one rectangle + * per line of the print and wants the run whole. + * + * Both ends can be empty — two ayahs that differ only in their tail share + * everything before it — and an ayah that continues onto the next page is + * clamped to what this page holds, so the leftover named here is the leftover + * the reader can actually see. + */ +export function divergentRuns( + present: WordSpanRange, + shared: readonly [number, number], +): Array<readonly [number, number]> { + const runs: Array<readonly [number, number]> = []; + if (shared[0] > present.from) runs.push([present.from, Math.min(shared[0] - 1, present.to)]); + if (shared[1] < present.to) runs.push([Math.max(shared[1] + 1, present.from), present.to]); + return runs; +} diff --git a/packages/core/src/verse-text.test.ts b/packages/core/src/verse-text.test.ts deleted file mode 100644 index e36ecd11..00000000 --- a/packages/core/src/verse-text.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { diffPair, VERSE_TEXT, verseTokens } from "./verse-text.js"; - -describe("verse-text · fixture", () => { - it("carries the 12 mock ayahs", () => { - expect(Object.keys(VERSE_TEXT)).toHaveLength(12); - for (const k of ["2:40", "2:48", "2:123", "82:19"]) { - expect(VERSE_TEXT[k]).toBeDefined(); - } - }); - - it("2:48 vs 2:123 carries the شفاعة/عدل swap as class-1/2 tokens", () => { - // The signature diff: 2:48 marks شَفَاعَةٌ divergent, 2:123 marks عَدْلٌ. - const t48 = VERSE_TEXT["2:48"]!; - const t123 = VERSE_TEXT["2:123"]!; - expect(t48.some((tok) => tok.text.includes("شَفَاعَةٌ") && tok.cls !== 0)).toBe(true); - expect(t123.some((tok) => tok.text.includes("عَدْلٌ") && tok.cls !== 0)).toBe(true); - }); -}); - -describe("verse-text · lookup", () => { - it("verseTokens accepts bare and canonical keys", () => { - expect(verseTokens("2:48")).toBe(VERSE_TEXT["2:48"]); - expect(verseTokens("quran/hafs-kfqc/2:48")).toBe(VERSE_TEXT["2:48"]); - expect(verseTokens("quran/hafs-kfqc/9:99")).toBeNull(); - }); -}); - -describe("verse-text · diffPair", () => { - it("pairs two vendored ayahs into from/to sides", () => { - const d = diffPair("quran/hafs-kfqc/2:48", "quran/hafs-kfqc/2:123"); - expect(d).not.toBeNull(); - expect(d!.from.tokens).toBe(VERSE_TEXT["2:48"]); - expect(d!.to.tokens).toBe(VERSE_TEXT["2:123"]); - }); - - it("returns null when either ayah has no vendored text", () => { - expect(diffPair("2:48", "9:99")).toBeNull(); - expect(diffPair("9:99", "2:48")).toBeNull(); - }); -}); diff --git a/packages/core/src/verse-text.ts b/packages/core/src/verse-text.ts deleted file mode 100644 index a021d700..00000000 --- a/packages/core/src/verse-text.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * L3 verse-text token table for the diff view (spec §3 diff), keyed by bare - * `surah:ayah`. Each token carries a diff class: 0 = common/unchanged, while - * 1 and 2 mark the two divergent readings being compared. This is Loop-3 demo - * data — the mock's hand-verified tokens — that Loop 4's ETL will replace with - * a real per-ayah token source. The Arabic text is copied EXACTLY, including - * all harakat/diacritics: it is scripture and must not be normalized. - */ - -export type DiffClass = 0 | 1 | 2; - -export interface DiffToken { - readonly text: string; - readonly cls: DiffClass; -} - -export const VERSE_TEXT: Readonly<Record<string, readonly DiffToken[]>> = { - '2:40': [ - { text: 'يَا بَنِي إِسْرَائِيلَ اذْكُرُوا نِعْمَتِيَ الَّتِي أَنْعَمْتُ عَلَيْكُمْ', cls: 0 }, - { text: 'وَأَوْفُوا بِعَهْدِي أُوفِ بِعَهْدِكُمْ وَإِيَّايَ فَارْهَبُونِ', cls: 1 }, - ], - '2:47': [ - { text: 'يَا بَنِي إِسْرَائِيلَ اذْكُرُوا نِعْمَتِيَ الَّتِي أَنْعَمْتُ عَلَيْكُمْ', cls: 0 }, - { text: 'وَأَنِّي فَضَّلْتُكُمْ عَلَى الْعَالَمِينَ', cls: 2 }, - ], - '2:122': [ - { text: 'يَا بَنِي إِسْرَائِيلَ اذْكُرُوا نِعْمَتِيَ الَّتِي أَنْعَمْتُ عَلَيْكُمْ', cls: 0 }, - { text: 'وَأَنِّي فَضَّلْتُكُمْ عَلَى الْعَالَمِينَ', cls: 2 }, - ], - '2:48': [ - { text: 'وَاتَّقُوا يَوْمًا لَا تَجْزِي نَفْسٌ عَنْ نَفْسٍ شَيْئًا وَلَا يُقْبَلُ مِنْهَا', cls: 0 }, - { text: 'شَفَاعَةٌ', cls: 1 }, - { text: 'وَلَا', cls: 0 }, - { text: 'يُؤْخَذُ مِنْهَا عَدْلٌ', cls: 2 }, - { text: 'وَلَا هُمْ يُنْصَرُونَ', cls: 0 }, - ], - '2:123': [ - { text: 'وَاتَّقُوا يَوْمًا لَا تَجْزِي نَفْسٌ عَنْ نَفْسٍ شَيْئًا وَلَا يُقْبَلُ مِنْهَا', cls: 0 }, - { text: 'عَدْلٌ', cls: 2 }, - { text: 'وَلَا', cls: 0 }, - { text: 'تَنْفَعُهَا شَفَاعَةٌ', cls: 1 }, - { text: 'وَلَا هُمْ يُنْصَرُونَ', cls: 0 }, - ], - '2:58': [ - { text: 'وَإِذْ', cls: 0 }, - { text: 'قُلْنَا ادْخُلُوا', cls: 1 }, - { text: 'هَذِهِ الْقَرْيَةَ', cls: 0 }, - { text: 'فَكُلُوا', cls: 2 }, - { text: 'مِنْهَا حَيْثُ شِئْتُمْ', cls: 0 }, - { text: 'رَغَدًا وَادْخُلُوا الْبَابَ سُجَّدًا وَقُولُوا حِطَّةٌ', cls: 1 }, - { text: 'نَغْفِرْ لَكُمْ', cls: 0 }, - { text: 'خَطَايَاكُمْ', cls: 2 }, - { text: 'وَسَنَزِيدُ الْمُحْسِنِينَ', cls: 0 }, - ], - '7:161': [ - { text: 'وَإِذْ', cls: 0 }, - { text: 'قِيلَ لَهُمُ اسْكُنُوا', cls: 1 }, - { text: 'هَذِهِ الْقَرْيَةَ', cls: 0 }, - { text: 'وَكُلُوا', cls: 2 }, - { text: 'مِنْهَا حَيْثُ شِئْتُمْ', cls: 0 }, - { text: 'وَقُولُوا حِطَّةٌ وَادْخُلُوا الْبَابَ سُجَّدًا', cls: 1 }, - { text: 'نَغْفِرْ لَكُمْ', cls: 0 }, - { text: 'خَطِيئَاتِكُمْ', cls: 2 }, - { text: 'سَنَزِيدُ الْمُحْسِنِينَ', cls: 0 }, - ], - '2:60': [ - { text: 'فَقُلْنَا', cls: 1 }, - { text: 'اضْرِبْ بِعَصَاكَ الْحَجَرَ', cls: 0 }, - { text: 'فَانْفَجَرَتْ', cls: 2 }, - { text: 'مِنْهُ اثْنَتَا عَشْرَةَ عَيْنًا', cls: 0 }, - ], - '7:160': [ - { text: 'أَنِ', cls: 1 }, - { text: 'اضْرِبْ بِعَصَاكَ الْحَجَرَ', cls: 0 }, - { text: 'فَانْبَجَسَتْ', cls: 2 }, - { text: 'مِنْهُ اثْنَتَا عَشْرَةَ عَيْنًا', cls: 0 }, - ], - '2:45': [ - { text: 'وَاسْتَعِينُوا بِالصَّبْرِ وَالصَّلَاةِ', cls: 0 }, - { text: 'وَإِنَّهَا لَكَبِيرَةٌ إِلَّا عَلَى الْخَاشِعِينَ', cls: 1 }, - ], - '2:153': [ - { text: 'يَا أَيُّهَا الَّذِينَ آمَنُوا', cls: 2 }, - { text: 'اسْتَعِينُوا بِالصَّبْرِ وَالصَّلَاةِ', cls: 0 }, - { text: 'إِنَّ اللَّهَ مَعَ الصَّابِرِينَ', cls: 1 }, - ], - '82:19': [ - { text: 'يَوْمَ لَا تَمْلِكُ نَفْسٌ لِنَفْسٍ شَيْئًا وَالْأَمْرُ يَوْمَئِذٍ لِلَّهِ', cls: 0 }, - ], -}; - -/** - * Look up the diff-token array for an ayah. Accepts either a bare `"2:48"` key - * or a canonical `"quran/hafs-kfqc/2:48"` key — everything up to and including - * the last `/` is stripped before lookup. Returns `null` if the key is absent. - */ -export function verseTokens(key: string): readonly DiffToken[] | null { - const bare = key.slice(key.lastIndexOf('/') + 1); - return VERSE_TEXT[bare] ?? null; -} - -/** One side of a diff: the ayah's key and its pre-classified tokens. */ -export interface DiffSide { - readonly key: string; - readonly tokens: readonly DiffToken[]; -} - -/** - * The two rows of a token diff (spec §3): the source ayah ("here") and the hop - * target. Each token is already class-tagged in the fixture (the mock does no - * runtime alignment — the divergent readings are pre-marked), so this just pairs - * the two token arrays. Returns `null` when either ayah has no vendored text, so - * a caller can fall back to the plain note rather than render an empty diff. - */ -export function diffPair(fromKey: string, toKey: string): { from: DiffSide; to: DiffSide } | null { - const from = verseTokens(fromKey); - const to = verseTokens(toKey); - if (!from || !to) return null; - return { from: { key: fromKey, tokens: from }, to: { key: toKey, tokens: to } }; -} diff --git a/packages/etl/data/pages/PROVENANCE.md b/packages/etl/data/pages/PROVENANCE.md index 82e81991..48ffad2b 100644 --- a/packages/etl/data/pages/PROVENANCE.md +++ b/packages/etl/data/pages/PROVENANCE.md @@ -112,3 +112,45 @@ extraction and must reproduce it byte-identically. confused. `.claude/skills/mushaf-reference/SKILL.md` carries the rest — which published references can stand in for paper, and which are a different qira'a or a different revision and will make a correct pair look wrong. +- **The marks on these pages are drawn about a unit out of place, and every page + but four now carries its own correction — measured 2026-08-12.** The word + geometry above transfers cleanly; the *diacritic* boxes carried onto this frame + by the same fit do not. `probe-mark-ink.mjs` scores each mark's rectangle against + the ink under it and against three deliberately wrong placements, and as shipped + a claimed placement loses to a wrong one. Over **30,000 marks on all 604 pages**: + separation **−0.234**, only **13.50%** of marks beating their own null, 96.41% + sitting further than 0.75 units from their best fit, median overlap with the ink + **0.017** against an achievable 0.909. The error is a shift of **0.81 across and + 1.06 down** on marks 5.6 × 3.6, the same direction everywhere. + + **Taking each page's own displacement out is most of the repair, and measurably + not all of it.** Corrected, separation goes −0.234 → **+0.281**, marks beating + their null 13.5% → **75.7%**, blank rectangles 1.31% → 0.18%, median overlap + 0.017 → 0.492. But **19.49%** of marks are still further than 0.75 units off, and + the corrected median overlap is 0.492 against the 0.909 a per-mark best fit + reaches — so a single translation per page is a good model of this error and not + a complete one. Whatever remains is not a page-level shift. + + **Coverage: 600 pages of 604.** The probe discards any page that drew too few + marks to fit, and pages **1, 2, 603 and 604** — the short ornamental frames — + never clear that floor. They have no measured correction, and a page with no row + was never looked at rather than found to be right. + + **The first pass covered 40 pages, and the difference is instructive.** Those 40 + turned out to be representative in the *centre* — their mean correction is + indistinguishable from the other 560 (t = −0.1 across, −1.2 down) — but they badly + understated the *spread*: page-to-page sd 0.107/0.097 against 0.172/0.165 over the + rest, and scoring the same 40 pages twice from different samples disagrees by + about as much as those pages differ from each other. So nearly all the variation + visible in the first pass was measurement noise, which is why the sitting built on + it could confirm the correction's direction and never its size. + + A reader confirmed that direction by hand: of 60 marks placed, **59 landed nearer + the corrected rectangle** than the one the app draws today (98.3%, interval + 91.1–99.7). All 60 came from those same 40 pages, because a by-eye trial can only + be built for a page that has a proposed move — so the by-hand result is still + untested on a page nobody had measured. The unresolved remainder — a further tenth + of a unit, which does not clear nothing once the pages rather than the placements + are counted — is **not** applied. `docs/design/mark-registration.md` §⑦ and §⑧ + carry the reasoning, `docs/validation/rulings/` the answers the reader gave and + the displacements they were given about. diff --git a/packages/etl/scripts/audit-mark-sittings.mjs b/packages/etl/scripts/audit-mark-sittings.mjs new file mode 100644 index 00000000..800de202 --- /dev/null +++ b/packages/etl/scripts/audit-mark-sittings.mjs @@ -0,0 +1,202 @@ +#!/usr/bin/env node +/** + * Are the sittings on disk the ones somebody should actually sit? + * + * The rebuild step of a sitting review ends with an instruction and no command: + * *confirm the deal did not move — same number of parts, same total, and no mark + * that has been answered coming back round again.* Everything before it in that + * skill is a script. That one sentence was a person squinting at sixteen files, + * which means in practice it was a person not squinting at sixteen files, and the + * two ways it goes wrong are both silent: + * + * - A rebuild is **forgotten**, or run with one hand-over left out of the list. + * The parts still open, still count down, still bank answers — and re-ask + * questions somebody already answered. The reader cannot tell; the pages look + * exactly like pages they have not seen. + * - A rebuild is run against **different measurements**. Every rectangle in every + * part is then drawn from displacements that are not the ones on disk, so every + * answer is about a picture nobody will be able to reconstruct afterwards. This + * is the worse one, because the answers are wrong rather than merely wasted, and + * nothing downstream can detect it. + * + * Both are exactly checkable, because a built sitting says what it was built from. + * The identity is not decoration: the reader's place is stored under a key made of + * the measurements' fingerprint, the set, the slice and the seed, and the answered + * set is folded into the slice for that reason. So a part that disagrees with the + * tree announces itself, if anything asks. + * + * This asks. It reads no answers of its own — the one reading of *answered* lives in + * lib/answered.mjs and both this and the builder import it, because two readings of + * that word drift and the drift shows up as a mark one of them drops and the other + * counts, with neither obviously wrong. + * + * It is an audit, not a gate. It cannot run in CI: the sittings are build products in + * `out/`, the answers accumulate on the machine doing the serving, and a check that + * passes in CI by being unable to look is worse than no check. + * + * node scripts/audit-mark-sittings.mjs --answered out/mark-answers.jsonl,out/handed.json + * pnpm audit:sittings -- --answered out/mark-answers.jsonl + */ +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { answeredKey, fingerprint, readAnswered } from "./lib/answered.mjs"; +import { readSitting } from "./lib/sitting-file.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ETL = join(HERE, ".."); + +const argv = process.argv.slice(2); +const arg = (k, d) => { + const i = argv.indexOf(k); + return i >= 0 && argv[i + 1] ? argv[i + 1] : d; +}; + +const dir = arg("--dir", join(ETL, "out")); +const rowsPath = arg("--rows", join(ETL, "out", "mark-rows.line-tilt.json")); + +/** + * The same flag the builder takes, and it is required to be the same list. + * + * Defaulting it would be the one mistake worth avoiding here. An auditor that + * quietly reads the running log and nothing else calls a sitting stale whenever a + * hand-over exists, or calls it fresh whenever the rebuild used a file this did not + * — and both verdicts are confident. So it is given, exactly as the rebuild was + * given it, and the report prints what it read so a wrong list is visible rather + * than inferred. + */ +const answeredPaths = String(arg("--answered", "")) + .split(",") + .map((p) => p.trim()) + .filter(Boolean); + +const problems = []; +const notes = []; + +if (!existsSync(dir)) { + console.error(`audit:sittings — FAIL: no ${dir.replace(`${ETL}/`, "")} to look in. Nothing has been built.`); + process.exit(1); +} + +/* What the rectangles were measured from, as it stands right now. */ +let rowsFpNow = null; +if (existsSync(rowsPath)) rowsFpNow = fingerprint(readFileSync(rowsPath, "utf8")); +else notes.push(`the rows file ${rowsPath.replace(`${ETL}/`, "")} is not here, so nothing can be said about which measurements the parts were drawn from`); + +const answered = answeredPaths.length ? readAnswered(answeredPaths) : new Set(); +const wantKey = answeredKey(answered); + +/** + * Read the built sittings back. + * + * The reading itself is in lib/sitting-file.mjs, shared with the front door, for the + * reason given there: two readers of the same two literals disagree eventually, and + * the disagreement is a part one of them cannot see. It hands faults back rather than + * printing them, so they land in this run's problem list and fail the audit. + */ +const sittings = readdirSync(dir) + .filter((n) => n.endsWith(".html")) + .map((n) => readSitting(join(dir, n))) + .filter(Boolean); + +for (const s of sittings) { + for (const fault of s.faults) problems.push(`${s.file.replace(`${ETL}/`, "")}: ${fault}`); +} + +if (!sittings.length) { + console.error(`audit:sittings — FAIL: no built sittings in ${dir.replace(`${ETL}/`, "")}.`); + process.exit(1); +} + +/** + * A **deal** is one shuffle dealt out into parts: the same set, the same seed, the + * same number of parts. Two deals in the same directory are not a fault — the bands + * are five separate small sittings on purpose, and an old deal left behind is + * evidence, not litter. What is a fault is a *gap inside* one deal, so they are + * grouped first and every check below is asked of a deal rather than of a folder. + */ +const deals = new Map(); +for (const s of sittings) { + const { set, seed, part, band } = s.head; + if (!part) { + notes.push(`${s.file.replace(`${ETL}/`, "")} is a single sitting rather than part of a deal${band ? ` (the ${band} band)` : ""} — counted, not audited for coverage`); + continue; + } + const of = Number(part.split("/")[1]); + const key = `${set} · seed ${seed} · ${of} parts`; + if (!deals.has(key)) deals.set(key, []); + deals.get(key).push(s); +} + +for (const [key, parts] of deals) { + const say = (msg) => problems.push(`${key}: ${msg}`); + const of = Number(parts[0].head.part.split("/")[1]); + + /* ① every part of the deal is here, once. */ + const byN = new Map(); + for (const p of parts) { + const n = Number(p.head.part.split("/")[0]); + if (byN.has(n)) say(`part ${n} of ${of} is built twice, as ${byN.get(n).file.replace(`${ETL}/`, "")} and ${p.file.replace(`${ETL}/`, "")} — a reader handed both answers the same marks twice`); + else byN.set(n, p); + } + const missing = []; + for (let n = 1; n <= of; n += 1) if (!byN.has(n)) missing.push(n); + if (missing.length) say(`part${missing.length > 1 ? "s" : ""} ${missing.join(", ")} of ${of} ${missing.length > 1 ? "are" : "is"} not built — those marks are in nobody's sitting`); + + /* ② one measurement across the whole deal, and it is the one on disk. */ + const fps = new Set(parts.map((p) => p.head.rowsFingerprint)); + if (fps.size > 1) say(`the parts were drawn from ${fps.size} different measurements (${[...fps].join(", ")}) — they are not one deal, and their answers cannot be settled together`); + else if (rowsFpNow && !fps.has(rowsFpNow)) say(`built from measurements ${[...fps][0]}, but the rows file on disk is ${rowsFpNow} — every rectangle in every part is about a mark that is not where these parts say it is. Rebuild before anyone sits.`); + + /* ③ the deal knows about every answer that has been given. */ + const keys = new Set(parts.map((p) => String(p.head.slice).replace(/^-p\d+of\d+/, ""))); + if (keys.size > 1) say(`the parts disagree about which answers had been given when they were built — some were rebuilt and some were not`); + else if ([...keys][0] !== wantKey) { + const had = [...keys][0] || "(none)"; + say(`built against a different set of answers than the one on disk (part says ${had}, the answers say ${wantKey || "(none)"}) — the rebuild has not been run since somebody last answered${answeredPaths.length ? "" : ", or --answered was not given the files the rebuild was given"}`); + } + + /* ④ nothing that has been answered comes back round. */ + const seen = new Map(); + let reasked = 0; + for (const p of parts) { + for (const id of p.ids) { + if (answered.has(id)) reasked += 1; + const where = seen.get(id); + if (where) say(`${id} is asked in part ${where} and again in part ${p.head.part} — one mark, two answers, and nothing downstream can tell they are the same question`); + else seen.set(id, p.head.part); + } + } + if (reasked) say(`${reasked} mark${reasked > 1 ? "s" : ""} already carrying a standing answer ${reasked > 1 ? "are" : "is"} asked again`); + + /* ⑤ the arithmetic the reader is shown adds up. */ + const shown = parts.reduce((n, p) => n + (p.head.shown ?? p.ids.length), 0); + const pool = new Set(parts.map((p) => p.head.pool)); + const population = new Set(parts.map((p) => p.head.population)); + const already = new Set(parts.map((p) => p.head.alreadyAnswered)); + if (pool.size > 1 || population.size > 1) say(`the parts disagree about how big the job is`); + else { + const [poolN] = [...pool]; + const [popN] = [...population]; + const [answeredN] = [...already]; + if (!missing.length && shown !== poolN) say(`the parts hold ${shown} marks between them but claim to be dealing out ${poolN} — ${Math.abs(poolN - shown)} ${shown < poolN ? "in nobody's sitting" : "counted twice"}`); + if (poolN + answeredN !== popN) say(`${poolN} left plus ${answeredN} answered is not ${popN} — the count a reader is shown does not describe this population`); + // More answers were dropped at build time than this run can find. The rebuild was + // given a hand-over that `--answered` was not, and every count below is short. + if (answeredN > answered.size) say(`built after ${answeredN} answers had been given, but only ${answered.size} can be read back — a file the rebuild used is not in --answered`); + notes.push(`${key}: ${shown} marks across ${byN.size} parts, ${answeredN} already answered, ${popN} in all`); + } +} + +const label = "audit:sittings"; +if (answeredPaths.length) console.log(`${label} — answers read from ${answeredPaths.map((p) => p.replace(`${ETL}/`, "")).join(", ")} (${answered.size} standing)`); +else console.log(`${label} — no --answered given, so every part is expected to hold the whole population`); +if (rowsFpNow) console.log(`${label} — measurements on disk: ${rowsPath.replace(`${ETL}/`, "")} (${rowsFpNow})`); +for (const n of notes) console.log(`${label} — ${n}`); + +if (problems.length) { + console.error(""); + for (const p of problems) console.error(`${label} — FAIL: ${p}`); + process.exit(1); +} +console.log(`${label} — the deal did not move.`); diff --git a/packages/etl/scripts/audit-mark-sittings.test.mjs b/packages/etl/scripts/audit-mark-sittings.test.mjs new file mode 100644 index 00000000..5845273d --- /dev/null +++ b/packages/etl/scripts/audit-mark-sittings.test.mjs @@ -0,0 +1,187 @@ +/** + * The instruction that had no command behind it. + * + * A sitting review ends by asking somebody to confirm the deal did not move — same + * number of parts, same total, nothing already answered coming round again. Sixteen + * files, by eye, at the end of an hour of work, and the two ways it goes wrong are + * both invisible from inside a part: a rebuild that never happened looks exactly like + * a rebuild that did, and a rebuild against the wrong measurements looks like a + * perfectly ordinary sitting whose answers are all about rectangles nobody drew. + * + * So these are the failures the auditor exists to catch, each one built on purpose. + * A test that only proves the clean case passes proves nothing about an auditor: the + * one that always says yes passes it too. + */ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { answeredKey, fingerprint } from "./lib/answered.mjs"; + +const SCRIPT = new URL("./audit-mark-sittings.mjs", import.meta.url).pathname; + +const ROWS = '{"rows":[]}'; +const ROWS_FP = fingerprint(ROWS); + +let dir; + +/** + * A built sitting, reduced to the two things a built sitting says about itself. + * + * The real file is a megabyte of cropped page artwork around these two lines. The + * artwork is not what is being audited and forging it would only mean the fixtures + * rot whenever the page changes, so what these hold is the shape the auditor + * actually reads — which is also a claim about the builder, and the reason the + * builder emits both literals on one line each. + */ +function sitting(name, head, ids) { + const full = { + built: "mark-report", + rows: "out/mark-rows.line-tilt.json", + rowsFingerprint: ROWS_FP, + set: "fallback", + seed: 23, + band: null, + ...head, + }; + const cards = ids.map((id) => ({ id, page: Number(id.split(":")[0]) })); + writeFileSync( + join(dir, name), + ["<!doctype html><html><body><script>", `const HEAD = ${JSON.stringify(full)};`, `const CARDS = ${JSON.stringify(cards)};`, "</script></body></html>"].join("\n"), + ); +} + +/** Two parts of four marks, one answer standing, exactly as a fresh rebuild leaves it. */ +function deal({ answered = [], parts = 2 } = {}) { + const pool = [["1:1", "1:2"], ["2:1", "2:2"]]; + const key = answeredKey(new Set(answered)); + for (let n = 1; n <= parts; n += 1) { + sitting(`sit.part-${n}.html`, { + slice: `-p${n}of2${key}`, + part: `${n}/2`, + pool: 4, + population: 4 + answered.length, + alreadyAnswered: answered.length, + shown: 2, + }, pool[n - 1]); + } +} + +function audit(answeredFile) { + const args = [SCRIPT, "--dir", dir, "--rows", join(dir, "rows.json")]; + if (answeredFile) args.push("--answered", join(dir, answeredFile)); + try { + return { code: 0, out: execFileSync(process.execPath, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) }; + } catch (e) { + return { code: e.status, out: `${e.stdout}${e.stderr}` }; + } +} + +function answers(name, ids) { + writeFileSync(join(dir, name), ids.map((id) => JSON.stringify({ kind: "placement", id })).join("\n") + "\n"); +} + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "hifth-sittings-")); + writeFileSync(join(dir, "rows.json"), ROWS); +}); + +afterEach(() => rmSync(dir, { recursive: true, force: true })); + +describe("a deal that did not move", () => { + it("passes, and says what the deal is", () => { + deal(); + const r = audit(); + expect(r.code).toBe(0); + expect(r.out).toContain("the deal did not move"); + expect(r.out).toContain("4 marks across 2 parts"); + }); + + it("passes with an answer standing, once the rebuild has dropped it", () => { + answers("log.jsonl", ["9:9"]); + deal({ answered: ["9:9"] }); + const r = audit("log.jsonl"); + expect(r.code).toBe(0); + expect(r.out).toContain("1 already answered"); + }); +}); + +describe("the rebuild that never happened", () => { + it("fails when an answer exists that no part knows about", () => { + deal(); + answers("log.jsonl", ["9:9"]); + const r = audit("log.jsonl"); + expect(r.code).toBe(1); + expect(r.out).toMatch(/built against a different set of answers/); + }); + + it("fails when a mark already answered is asked again", () => { + answers("log.jsonl", ["1:1"]); + deal({ answered: ["1:1"] }); + const r = audit("log.jsonl"); + expect(r.code).toBe(1); + expect(r.out).toMatch(/already carrying a standing answer is asked again/); + }); + + it("fails when only some of the parts were rebuilt", () => { + answers("log.jsonl", ["9:9"]); + deal({ answered: ["9:9"] }); + sitting("sit.part-2.html", { slice: "-p2of2", part: "2/2", pool: 4, population: 5, alreadyAnswered: 1, shown: 2 }, ["2:1", "2:2"]); + const r = audit("log.jsonl"); + expect(r.code).toBe(1); + expect(r.out).toMatch(/disagree about which answers had been given/); + }); +}); + +describe("the rebuild against the wrong measurements", () => { + it("fails, and says the rectangles are not where the parts say", () => { + deal(); + sitting("sit.part-1.html", { rowsFingerprint: "deadbeef", slice: "-p1of2", part: "1/2", pool: 4, population: 4, alreadyAnswered: 0, shown: 2 }, ["1:1", "1:2"]); + sitting("sit.part-2.html", { rowsFingerprint: "deadbeef", slice: "-p2of2", part: "2/2", pool: 4, population: 4, alreadyAnswered: 0, shown: 2 }, ["2:1", "2:2"]); + const r = audit(); + expect(r.code).toBe(1); + expect(r.out).toMatch(/Rebuild before anyone sits/); + }); +}); + +describe("a deal with a hole in it", () => { + it("fails when a part is not built at all", () => { + deal({ parts: 1 }); + const r = audit(); + expect(r.code).toBe(1); + expect(r.out).toMatch(/part 2 of 2 is not built — those marks are in nobody's sitting/); + }); + + it("fails when one mark is dealt into two parts", () => { + deal(); + sitting("sit.part-2.html", { slice: "-p2of2", part: "2/2", pool: 4, population: 4, alreadyAnswered: 0, shown: 2 }, ["1:1", "2:2"]); + const r = audit(); + expect(r.code).toBe(1); + expect(r.out).toMatch(/1:1 is asked in part 1\/2 and again in part 2\/2/); + }); + + it("fails when the parts hold fewer marks than they claim to be dealing out", () => { + deal(); + sitting("sit.part-2.html", { slice: "-p2of2", part: "2/2", pool: 4, population: 4, alreadyAnswered: 0, shown: 1 }, ["2:1"]); + const r = audit(); + expect(r.code).toBe(1); + expect(r.out).toMatch(/hold 3 marks between them but claim to be dealing out 4/); + }); +}); + +describe("what it will not do", () => { + it("does not treat a band sitting as an incomplete deal", () => { + deal(); + sitting("sit.band.html", { slice: "", part: null, band: "0.55,0.65", pool: 9, population: 9, alreadyAnswered: 0, shown: 9 }, ["7:1"]); + const r = audit(); + expect(r.code).toBe(0); + expect(r.out).toContain("counted, not audited for coverage"); + }); + + it("says nothing has been built rather than passing on an empty folder", () => { + const r = audit(); + expect(r.code).toBe(1); + expect(r.out).toMatch(/no built sittings/); + }); +}); diff --git a/packages/etl/scripts/build-adjacency.mjs b/packages/etl/scripts/build-adjacency.mjs index 48ede54c..e392a6fd 100644 --- a/packages/etl/scripts/build-adjacency.mjs +++ b/packages/etl/scripts/build-adjacency.mjs @@ -19,7 +19,9 @@ * (dPage from the ayah-page table, sameJuz from the juz table) and, where the * pair shares one unambiguous phrase, the print word range on each side (see * Pass 2½) → one shard per surah, all 114 written (empty shards included so - * the app's loader never 404s). + * the app's loader never 404s), plus a NOTICE.txt naming BOTH upstreams — the + * spans make this tree a derivative of the GPL'd corpus as well as of the + * mutashabihat dataset, which the write block below says at length. * * Loop 2's `buildShards` compiler is retired here — this script emits * spec-shape shards directly (the adjacency.ts comment foretold it). @@ -42,7 +44,7 @@ import { juzOf, TOTAL_AYAHS, } from "@hifth/core"; -import { wordsByAyah, sharedRuns } from "./morphology.mjs"; +import { wordsByAyah, sharedRuns, copyrightBlock } from "./morphology.mjs"; import { openAlignment } from "./lib/segmentation.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -357,6 +359,62 @@ const OUT_DIR = join(ASSETS, "adj", EDITION); rmSync(OUT_DIR, { recursive: true, force: true }); mkdirSync(OUT_DIR, { recursive: true }); +// This tree has two parents and the notice has to say both, because the way to +// get this wrong twice is to fix it once. The pairings are Waqar144's and owe +// attribution; the spans — which words of an ayah a pairing shares — are +// computed by `sharedRuns` out of the GPL'd Quranic Arabic Corpus morphology, +// so under the same reading of "derivative" the root shards get, these shards +// carry GPL terms forward and owe the corpus's copyright notice as well. A +// notice that discharged only the GPL half and dropped Waqar144 would be the +// identical one-parent error this file shipped for the whole life of the spans +// feature, pointed the other way. +// +// It was that long-lived because the spans arrived as a FEATURE. Nobody touched +// a licence file, so nothing prompted anybody to look at what a sibling build +// step writes beside its output — build-roots.mjs and build-tajweed.mjs both +// emit a NOTICE.txt and this one never joined them. `gate:notices` is what now +// notices; the comment is here because the gate can only check what is declared. +// +// A third parent was named on 2026-09-01: `sameJuz` comes from core's juz +// table, which is Tanzil's structural metadata under CC BY. gate:notices cannot +// see that read, because it does not follow imports into @hifth/core — the +// declaration there names Tanzil by hand, and this paragraph is the notice's +// half of it. +// +// The corpus block is `copyrightBlock()`'s, the same call build-roots.mjs makes, +// so the two trees reproduce one quotation rather than two copies of it. +writeFileSync( + join(OUT_DIR, "NOTICE.txt"), + [ + "Mutashabihat pairings derived from Waqar144/Quran_Mutashabihat_Data", + " https://github.com/Waqar144/Quran_Mutashabihat_Data", + " commit f35f6d5d6e7d07f44e6a652d868b298fcd12e318", + " Free to use as you see fit; the author asks that the project be credited,", + " and credits in turn the work of Qari Idrees Al-Asim, rahimahu Allah.", + "", + "The word ranges on each edge (the span / toSpan fields) are computed from", + "the Quranic Arabic Corpus morphology (version 0.4), which is GPL-licensed,", + "so these files carry GPL-3.0 terms forward as well as the terms above. The", + "corresponding source is packages/etl/scripts/build-adjacency.mjs and the", + "pinned input packages/etl/data/roots/quranic-corpus-morphology-0.4.txt,", + "both in the repository named in the app's colophon.", + "", + "The sameJuz flag on an edge is computed from the juz table in @hifth/core,", + "which is derived from the Tanzil Quran metadata (quran-data.xml,", + "(C) 2008-2009 Tanzil.info, CC BY). Attribution: Tanzil Project,", + "https://tanzil.net. No Tanzil bytes ship; the flag is a boolean.", + "", + "These files contain ayah numbers, word indices and edge metadata only —", + "no Quran text.", + "", + "Generated by packages/etl/scripts/build-adjacency.mjs; see SOURCES.md and", + "LICENSES.md for how the three sets of terms compose.", + "", + copyrightBlock(), + "", + ].join("\n"), +); + const GZ_LIMIT = 50 * 1024; // PLAN budget: every shard <50KB gzipped let totalEdges = 0; let coveredAyahs = 0; diff --git a/packages/etl/scripts/build-mark-adjudication.mjs b/packages/etl/scripts/build-mark-adjudication.mjs index 527dc26b..0c905489 100644 --- a/packages/etl/scripts/build-mark-adjudication.mjs +++ b/packages/etl/scripts/build-mark-adjudication.mjs @@ -26,14 +26,27 @@ * ## Running it * * node packages/etl/scripts/build-mark-adjudication.mjs [--count 100] [--seed 11] + * … [--shift <file>] [--exclude <file,file>] [--pages <n>] * * Then open the page it names, work it, and save the ruling it offers at the * end. Score it with `score-mark-adjudication.mjs`. + * + * ## Why it can be narrowed to a set of pages + * + * A trial cannot be built for a page with no proposed move, so for as long as the + * measurement covered forty pages, every question this page could ask came from + * one of the forty the correction was fitted to. A hundred of those is a very + * precise answer about training pages, and it is the same limitation that turned + * out to be the largest one over on the placing session — which is where + * `selectPages` was written, and this is the second consumer it was written for. + * `--exclude` holds out the pages an earlier fit or an earlier sitting used and + * `--pages` says how many of the rest to keep; both default to nothing, which is + * exactly the behaviour every session banked before them. */ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { planSession, readers, windowFor } from "./lib/adjudication.mjs"; +import { planSession, readers, selectPages, windowFor } from "./lib/adjudication.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const ETL = join(HERE, ".."); @@ -51,9 +64,43 @@ const outPath = arg("--out", join(ETL, "out", "mark-adjudication.html")); /** How wide a panel is drawn, in pixels. Big enough that a unit is not a hair. */ const PANEL = Number(arg("--panel", 420)); +/** + * Which pages this session may ask about: hold some out, keep this many of what + * is left. + * + * `--spread` is passed through rather than fixed because the two sessions want + * different things from the same function. The placing session wants the ends of + * the range, since leverage on the correction's *size* is the only thing that + * makes a residual mean anything. This one only ever asks which of two rectangles + * is closer, so nothing here is estimated from how far apart the proposed moves + * are — what it wants is pages nobody has looked at, spread across the print + * rather than bunched at its extremes. Hence `even` by default, and the flag for + * anyone who wants to argue with that. + */ +const excludePath = arg("--exclude", null); +const pagesWanted = Number(arg("--pages", 0)); +const spread = arg("--spread", "even"); + const shiftText = readFileSync(shiftPath, "utf8"); const shift = JSON.parse(shiftText); +const excludeFiles = excludePath ? excludePath.split(",").filter(Boolean) : []; +const excluded = excludeFiles.flatMap((f) => JSON.parse(readFileSync(f, "utf8")).shifts.map((s) => s.page)); +const chosen = selectPages({ shifts: shift.shifts, exclude: excluded, pages: pagesWanted, spread }); +const byPage = new Map(shift.shifts.map((s) => [s.page, s])); +const shifts = chosen.map((p) => byPage.get(p)); +if (excludePath || pagesWanted > 0) { + if (!chosen.length) { + process.stderr.write(`no pages left after excluding ${excluded.length} of ${shift.shifts.length}\n`); + process.exit(2); + } + const heldOut = excluded.filter((p) => byPage.has(p)).length; + process.stderr.write( + `pages: ${chosen.length} of ${shift.shifts.length} measured` + + `${heldOut ? `, ${heldOut} held out` : ""} · ${spread}\n`, + ); +} + /** * A fingerprint of the displacements this session was built from. * @@ -120,7 +167,7 @@ function panel(trial, shapes, slot) { } const t0 = Date.now(); -const { trials, skippedForInk, pages } = planSession({ seed, count, shifts: shift.shifts }); +const { trials, skippedForInk, pages } = planSession({ seed, count, shifts }); const inks = new Map(); for (const p of new Set(trials.map((t) => t.page))) inks.set(p, readers.inkFor(p)); @@ -151,6 +198,33 @@ const head = { pages, shiftRan: shift.ran, shiftFingerprint: fingerprint(shiftText), + /** + * Which pages this build was allowed to ask about, and how that list was + * arrived at. Same field, same shape and the same load-bearing part as the + * placing session's: `of` is what the scorer replays. + * + * The scorer rebuilds every trial from the seed and the displacements. Narrow + * the displacements in one and not the other and each trial index lands on a + * different mark — the answers still resolve, the rates still print, and every + * one of them is about a question nobody was asked. There is no error to read, + * which is why the list travels rather than the recipe: replaying cannot drift + * the way re-running a selection can, and a later improvement to how pages are + * chosen must not re-score an hour somebody already sat. + * + * `heldOutFrom` is provenance rather than machinery, and it is the thing worth + * being able to check afterwards: it names the file whose pages were removed, + * which is the whole difference between a session that tests the correction and + * one that admires it. + */ + select: { + of: chosen, + /** The list in eight characters, so it can go in the browser's resume key. */ + fp: fingerprint(chosen.join(",")), + strategy: pagesWanted > 0 ? spread : "all", + heldOutFrom: excludeFiles.map((f) => f.split("/").pop()), + heldOut: excluded.length, + measured: shift.shifts.length, + }, /** * What this build of the page asked for, so a ruling can say which questions it * was in a position to answer. @@ -270,7 +344,18 @@ that built this page.</p> <script id="head" type="application/json">${JSON.stringify(head)}</script> <script> const HEAD = JSON.parse(document.getElementById("head").textContent); -const KEY = "hifth.adjudication." + HEAD.seed + "." + HEAD.shiftFingerprint; +// The pages are in the key alongside the seed and the displacements, because two +// builds that agree on those two and disagree on which pages they drew from are +// different sessions with the same name. Sharing a key means the second resumes +// into the first's answers and staples them onto a trial list nobody was shown. +const KEY = "hifth.adjudication." + HEAD.seed + "." + HEAD.shiftFingerprint + "." + HEAD.select.fp; +// And in the file name, for the milder version of the same problem. Two narrowed +// sittings under one seed both arrive in a downloads folder called the same thing, +// where the browser quietly appends a number and the person is left holding two +// files that differ in a way nothing on the outside of them shows. A sitting that +// took every measured page keeps the old plain name, because it is the old thing. +const NAME = "mark-ruling-" + HEAD.seed + + (HEAD.select.strategy === "all" ? "" : "." + HEAD.select.fp) + ".json"; const cards = [...document.querySelectorAll(".trial")]; const work = document.getElementById("work"); const intro = document.getElementById("intro"); @@ -362,7 +447,7 @@ function bank() { if (!SINK || banked) return; banked = true; const el = document.getElementById("banked"); - SINK.artifact("mark-ruling-" + HEAD.seed + ".json", + SINK.artifact(NAME, { ...HEAD, finished: new Date().toISOString(), answers }) .then((r) => { el.hidden = false; @@ -395,7 +480,7 @@ document.getElementById("save").onclick = () => { { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); - a.download = "mark-ruling-" + HEAD.seed + ".json"; + a.download = NAME; a.click(); URL.revokeObjectURL(a.href); }; @@ -407,8 +492,24 @@ else if (at >= cards.length && answers.length) render(); mkdirSync(dirname(outPath), { recursive: true }); writeFileSync(outPath, html); +/** + * What this build chose, in the shape `--exclude` reads — the same companion the + * placing page writes, for the same two reasons. + * + * A later session has to be able to hold these pages out, and the only way to + * hold out a set of pages is to have them in a file. And it is a record in its + * own right: which pages an hour of somebody's attention was spent on, readable + * without opening a megabyte of HTML that is mostly ink. + */ +const pagesPath = outPath.replace(/\.html$/, "") + ".pages.json"; +writeFileSync( + pagesPath, + `${JSON.stringify({ built: head.built, seed, of: shiftPath.split("/").pop(), shifts }, null, 2)}\n`, +); + process.stdout.write( `${trials.length} trials over ${pages} pages · seed ${seed} · displacements ${shift.ran} (${head.shiftFingerprint})\n` + `${skippedForInk} marks passed over for too little ink under the corrected box\n` + - `${(html.length / 1e6).toFixed(1)} MB in ${((Date.now() - t0) / 1000).toFixed(1)}s → ${outPath}\n`, + `${(html.length / 1e6).toFixed(1)} MB in ${((Date.now() - t0) / 1000).toFixed(1)}s → ${outPath}\n` + + `the ${chosen.length} pages it was built over → ${pagesPath}\n`, ); diff --git a/packages/etl/scripts/build-mark-nudge.mjs b/packages/etl/scripts/build-mark-nudge.mjs index c72fb613..2bbe9afe 100644 --- a/packages/etl/scripts/build-mark-nudge.mjs +++ b/packages/etl/scripts/build-mark-nudge.mjs @@ -44,7 +44,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { planNudge, readers, windowFor } from "./lib/adjudication.mjs"; +import { planNudge, readers, selectPages, windowFor } from "./lib/adjudication.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const ETL = join(HERE, ".."); @@ -62,9 +62,62 @@ const outPath = arg("--out", join(ETL, "out", "mark-nudge.html")); /** How wide the panel is drawn, in pixels. One page unit is then ~21 px. */ const PANEL = Number(arg("--panel", 460)); +/** + * A file of displacements whose pages this session must **not** ask about, and + * how many of what is left to ask about. + * + * Both default to nothing, which reproduces the original behaviour: every page + * with a measured correction is fair game. Giving `--exclude` the file the + * correction was fitted on is what makes a sitting out-of-sample, and it is the + * one thing the first sitting could not do — there was nothing held out, because + * a trial cannot be built for a page with no proposed move and only forty pages + * had one. + */ +const excludePath = arg("--exclude", null); +const pagesWanted = Number(arg("--pages", 0)); +const spread = arg("--spread", "extremes"); + +/** + * Who is placing these rectangles. Metadata everywhere except two places, and + * both of those matter. + * + * It deliberately does **not** reach the trial plan. The one measurement a + * second person can make that a first cannot is *do two hands agree*, and that + * question only has an answer if both hands were shown the same marks in the + * same order from the same starting points. So a second reader is the identical + * build under a different name: agreement within about a twentieth of a unit + * says the leftover distance is a fact about the print, and a larger gap says it + * is a fact about whoever placed the boxes — in which case no global residual + * should be applied to three hundred thousand rectangles at all. + * + * Where it does reach: the browser's resume key, so two people working the same + * file on one machine do not resume into each other's answers, which would look + * exactly like one person being unusually consistent; and the name of the file + * the page hands back, so the second download does not arrive looking like a + * duplicate of the first. + */ +const reader = arg("--reader", "A"); + const shiftText = readFileSync(shiftPath, "utf8"); const shift = JSON.parse(shiftText); +const excludeFiles = excludePath ? excludePath.split(",").filter(Boolean) : []; +const excluded = excludeFiles.flatMap((f) => JSON.parse(readFileSync(f, "utf8")).shifts.map((s) => s.page)); +const chosen = selectPages({ shifts: shift.shifts, exclude: excluded, pages: pagesWanted, spread }); +const byPage = new Map(shift.shifts.map((s) => [s.page, s])); +const shifts = chosen.map((p) => byPage.get(p)); +if (excludePath || pagesWanted > 0) { + if (!chosen.length) { + process.stderr.write(`no pages left after excluding ${excluded.length} of ${shift.shifts.length}\n`); + process.exit(2); + } + const heldOut = excluded.filter((p) => byPage.has(p)).length; + process.stderr.write( + `pages: ${chosen.length} of ${shift.shifts.length} measured` + + `${heldOut ? `, ${heldOut} held out` : ""} · ${spread}\n`, + ); +} + /** * A fingerprint of the displacements this session was built from — the same * guard the forced choice carries, for the same reason. The scorer rebuilds the @@ -83,6 +136,105 @@ function fingerprint(s) { const n2 = (v) => Math.round(v * 100) / 100; const esc = (s) => String(s).replace(/[<>&"]/g, (c) => ({ "<": "<", ">": ">", "&": "&", '"': """ })[c]); +/** + * Which one, in words. + * + * Small numbers spelled out because they are read mid-sentence — "the 2nd of 3" + * is a table cell and this is a sentence somebody reads once, quickly, before + * looking at the ink. + */ +const NTH = ["", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth"]; +const HOW_MANY = ["", "one", "two", "three", "four", "five", "six", "seven", "eight"]; +const nth = (n) => NTH[n] ?? `${n}th`; +const howMany = (n) => HOW_MANY[n] ?? String(n); + +/** + * How soft the wash over the host letters is, in page units: the blur, and how + * far it is grown past the letter outline before blurring. + * + * These two numbers are the whole safety argument, so they are named rather than + * buried. The correction being measured is about one unit; the wash's edge is a + * gradient a good deal wider than that, so a wash drawn a unit out of place looks + * exactly like one drawn correctly. Shrink them and this stops being true. + */ +const WASH_BLUR = 0.9; +const WASH_GROW = 1.6; + +/** + * A soft wash over the letters the mark was drawn on — which ones, and nothing + * about where. + * + * ## Why it is a wash and not a tint + * + * The first version of this filled the host letters in a second colour, and it + * was wrong in a way worth writing down, because it looked obviously safe. + * + * The ink in the panel is the **shipped** page — about fifteen paths, one per + * line, so there is no way to pick a word out of it. The letter outlines come + * from the **ligature corpus** instead, a different vendored drawing of the same + * print, carried onto our frame by the page's recorded fit. That fit is exactly + * what places the mark rectangle. So the gap between a crisply-drawn corpus + * letter and the shipped ink underneath it *is the correction*, drawn on the + * screen at about twenty-five pixels, in every panel, for an hour. A reader would + * not even have to notice they were copying it. + * + * A wash cannot say that. It is grown past the outline and then blurred, so its + * edge is a gradient several times wider than the correction; the same wash is + * produced whether the fit is right or a unit out. What survives is the only part + * we wanted — *these letters, not those* — and what is destroyed is the part that + * would have been an answer key. + * + * It is painted under the ink rather than over it, so the print the reader is + * judging against stays exactly as black and as crisp as it was. + * + * ## The mark itself + * + * Never washed, never traced, never drawn. Its position is the entire + * measurement. It stays as the print set it, indistinguishable from its + * neighbours, which is the condition under which a placement means anything. + */ +function hostWash(lig, fit, i) { + if (!lig || !lig.letters.length) return ""; + const ds = lig.letters.map((d) => `<path d="${esc(d)}"/>`).join(""); + // The filter sits on the outer group and the frame change on the inner one, so + // the blur and the growth are both in page units — the units the correction is + // measured in, and therefore the only units in which "wider than the thing it + // has to hide" means anything. + return ( + `<filter id="soft${i}" x="-20%" y="-20%" width="140%" height="140%">` + + `<feGaussianBlur stdDeviation="${WASH_BLUR}"/></filter>` + + `<g class="host" filter="url(#soft${i})" opacity=".55">` + + `<g transform="translate(${fit.tx} ${fit.ty}) scale(${fit.sx} ${fit.sy})"` + + ` fill="var(--host)" stroke="var(--host)" stroke-width="${n2(WASH_GROW / fit.sx)}"` + + ` stroke-linejoin="round">${ds}</g></g>` + ); +} + +/** + * The sentence that says which mark, above the panel. + * + * Three parts, and each is there because without it a real card in this session + * is ambiguous: the mark's name, the letters it sits on, and — only when those + * letters carry more than one mark of that name — which of them, counting from + * the right, because that is the direction the letters are read in. + * + * The rank is counted over the washed letters and says so, rather than over the + * whole word. A word can run off the edge of the crop, and "the third of three" + * over letters the reader cannot all see is worse than saying nothing at all. + * + * A mark the corpus drew under a word but inside no ligature gets no letters and + * says so. That is rare and it is better said than papered over: a reader told + * "the fatha on ذ" when nothing is washed would spend the trial hunting for the + * wash rather than placing the box. + */ +function whichOne(t) { + const on = t.lig && t.lig.text ? ` on <bdi>${esc(t.lig.text)}</bdi>` : ""; + const scope = t.lig ? "on those letters" : "in this word"; + const rank = t.of > 1 ? ` — the ${nth(t.nth)} of ${howMany(t.of)} ${scope}, counting from the right` : ""; + const lost = t.lig ? "" : " — this one sits under the word rather than on a letter, so nothing is washed"; + return `<p class="who">the <b>${esc(t.name)}</b>${on}${rank}${lost}</p>`; +} + /** The page's own outlines, clipped to the window. Ink and nothing else. */ function inkPaths(shapes, vx, vy, side) { const parts = []; @@ -110,7 +262,7 @@ function inkPaths(shapes, vx, vy, side) { } const t0 = Date.now(); -const { trials, repeats, skippedForInk, pages } = planNudge({ seed, count, shifts: shift.shifts }); +const { trials, repeats, skippedForInk, pages } = planNudge({ seed, count, shifts }); const inks = new Map(); for (const p of new Set(trials.map((t) => t.page))) inks.set(p, readers.inkFor(p)); @@ -137,7 +289,8 @@ const cards = trials data-x="${n2(x)}" data-y="${n2(y)}" data-w="${n2(w)}" data-h="${n2(h)}" data-vx="${n2(vx)}" data-vy="${n2(vy)}" data-side="${n2(side)}" data-sx="${n2(sx)}" data-sy="${n2(sy)}" hidden> -<svg viewBox="${n2(vx)} ${n2(vy)} ${n2(side)} ${n2(side)}" width="${PANEL}" height="${PANEL}" aria-hidden="true">${inkPaths(inks.get(t.page), vx, vy, side)}<rect class="hit" x="${n2(x + sx)}" y="${n2(y + sy)}" width="${n2(w)}" height="${n2(h)}" fill="none" stroke="var(--box)" stroke-width="${n2(side / 300)}"/></svg> +${whichOne(t)} +<svg viewBox="${n2(vx)} ${n2(vy)} ${n2(side)} ${n2(side)}" width="${PANEL}" height="${PANEL}" aria-hidden="true">${hostWash(t.lig, t.fit, t.i)}${inkPaths(inks.get(t.page), vx, vy, side)}<rect class="hit" x="${n2(x + sx)}" y="${n2(y + sy)}" width="${n2(w)}" height="${n2(h)}" fill="none" stroke="var(--box)" stroke-width="${n2(side / 300)}"/></svg> <p class="where">page ${t.page} · ${t.surah}:${t.aya}</p> </section>`; }) @@ -147,11 +300,45 @@ const head = { built: new Date().toISOString(), kind: "nudge", seed, + reader, count: trials.length, repeats, pages, shiftRan: shift.ran, shiftFingerprint: fingerprint(shiftText), + /** + * Which pages this build was allowed to draw from, and how that list was + * arrived at. + * + * `of` is the list itself, and it is the load-bearing field: the scorer + * rebuilds the trials from the seed and the displacements, so if the builder + * narrowed those displacements and the scorer did not, every trial index would + * point at a different mark and the residuals would be a subtraction of the + * wrong numbers — silently, with no error to read. Replaying a recorded list + * cannot drift the way re-running a selection can: a later change to how pages + * are chosen re-scores nothing that was already answered. + * + * The rest is provenance, not machinery. `heldOutFrom` names the file whose + * pages were removed, which is what makes a sitting out-of-sample and is the + * single most important thing to be able to check afterwards. + */ + select: { + of: chosen, + /** + * The list, in eight characters, so it can go in the browser's resume key. + * + * Two builds from the same displacements with the same seed and different + * pages are different sessions, and without this they would share a key: the + * second would resume into the first's answers and staple them onto a trial + * list they were never given. That is the same failure `shows` was added to + * this key to prevent, arriving by a second route. + */ + fp: fingerprint(chosen.join(",")), + strategy: pagesWanted > 0 ? spread : "all", + heldOutFrom: excludeFiles.map((f) => f.split("/").pop()), + heldOut: excluded.length, + measured: shift.shifts.length, + }, /** * What this build put on the screen. The forced-choice page carries the same * field for the same reason: a reading that assumes a question was asked when @@ -159,22 +346,49 @@ const head = { * feature. */ asks: ["place", "size"], + /** + * What a card put in front of the reader. It is part of the browser's resume + * key, below, and that is the point of writing it down: a placement made on a + * card that never said which mark it meant is not the same measurement as one + * made on a card that did, and a rebuild that changes the question has to + * start the session over rather than quietly stack the two on top of each + * other. The scorer reads it for the same reason the forced-choice page's + * `asks` is read — so a reading cannot assume a question the page had no way + * to put. + */ + shows: ["ink", "box", "which-mark"], }; const html = `<!doctype html><meta charset="utf-8"><title>Hifth — put the box where it goes + + +
    +

    Is this rectangle on the right mark?

    + +

    The orange rectangle is where the app would draw this mark. Around it is the +ink the mus'haf actually prints there, at whatever size your screen gives it. The letters under +the heading are the ones the print drew this mark on — that is the mark the rectangle should be +sitting on, and when a crop holds several of the same name, the line beside them says which. +These marks were drawn at random, so most of them should look right — saying so is a real +answer, and it is the one you should expect to give most often. When something is wrong, more +than one thing can be. If the rectangle is on the wrong mark, tap the mark it should be on: the +rectangle moves onto that ink and takes its size from it, which is quicker and steadier than +pushing it there by hand. Tapping it again takes it back, and a mark printed in two pieces takes +two taps.

    +

    + +
    +
    +
    + +
    + + +
    + +

    +
    + + + + + +
    + +
      +

      +
      + + +
      +
      +
      + +
      + +
      +
      + + +
      + + +

      + +
      + + +
      +
      + + + + +`; + +mkdirSync(dirname(out), { recursive: true }); +writeFileSync(out, html); +console.log( + `${HEAD.pool} of ${HEAD.of} marks in the ${set} set` + + (HEAD.alreadyAnswered ? ` · ${HEAD.alreadyAnswered} already answered, left out` : "") + + ` · ${cards.length} drawn · displacements ${rowsFp}`, +); +console.log(` drawn by ${Object.entries(HEAD.drawnBy).map(([k, v]) => `${k} ${v}`).join(", ")}`); + +/** + * What this many marks can and cannot find — printed before the sitting rather than + * discovered after it. With n answers and nothing wrong in any of them, the rule of + * three puts the upper bound on the true rate at about 3/n; a sitting of 60 that + * comes back clean has shown the failure rate is under roughly 5%, and has shown + * nothing whatever about whether it is 1% or 0.01%. + * + * This is here because a clean sitting is exactly the result somebody will want to + * quote as "we checked it", and the honest version of that sentence has a number in + * it. The instrument that will be over-read is the one that has to say so itself. + */ +console.log(` a clean sitting of ${cards.length} bounds the failure rate at about ${((3 / cards.length) * 100).toFixed(1)}% — not at zero`); +console.log(`wrote ${out.replace(`${ROOT}/`, "")}`); diff --git a/packages/etl/scripts/build-mark-report.test.mjs b/packages/etl/scripts/build-mark-report.test.mjs new file mode 100644 index 00000000..bc8b30b9 --- /dev/null +++ b/packages/etl/scripts/build-mark-report.test.mjs @@ -0,0 +1,1075 @@ +/** + * The arithmetic behind the words "we looked at all of them". + * + * Somebody asked to validate the remaining marks until we are at 100%, and the only + * way a person gets to say that at the end is if the files they sat really were the + * population — every mark in exactly one of them. That is a property of how the + * pages are cut, it is invisible from inside any single page, and it fails silently: + * sixteen files that between them miss forty marks look exactly like sixteen files + * that do not, and the shortfall is discovered — if ever — by somebody re-deriving + * it months later, after the claim has been written into a register. + * + * So the partition is checked here rather than by eye, and it is checked by reading + * the ids back out of the built pages rather than by re-running the selection: the + * artifact a person sits is the thing the claim is about. Re-running the chooser and + * comparing it against itself would pass for a builder that writes empty files. + * + * The band tests are here for a different reason. A band is how the sitting plan + * proposes to spend forty hours on the marks the machine is least sure of, so a band + * that quietly included a mark from outside it, or that overlapped its neighbour, + * would corrupt the one comparison the whole plan turns on. + */ +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { markPageFile, marksOf } from "./lib/marks.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SCRIPT = join(HERE, "build-mark-report.mjs"); + +const RADIUS = 3; +const IOU = 0.55; + +/** + * Sixty marks on one line of page 1 — one line so the line-tilt correction clears + * its twenty-mark floor and the refused marks are drawn by a fit that actually ran, + * and page 1 because the builder reads that page's real ink and a made-up page + * number has none. The spread of `iouBest` is deliberate: it puts marks in every + * band the sitting plan cuts, so a band test has something to be wrong about. + * + * The index and the name are the print's own, not invented, because the builder now + * looks up which letters each mark was drawn on in order to caption it, and refuses + * a row whose name does not match the mark that index names. A fixture of sixty + * hand-written kasras would fail that check on the first card — correctly, which is + * the point of the check, and uselessly, which is why the fixture reads them. The + * geometry stays synthetic: the boxes are what puts the marks on one line and in + * every band, and nothing here is measuring where they really are. + */ +const REAL = existsSync(markPageFile(1)) ? marksOf(1).slice(0, 60) : []; +const ROWS = REAL.map((m, k) => ({ + page: 1, + k: m.k, + line: 1, + name: m.name, + box: [180 - k * 2.5, 40, 5.6, 3.6], + ink: 0.1, + dx: k % 17 === 0 ? RADIUS : 0.5, + dy: -0.25, + iou0: 0, + iouBest: 0.4 + (k % 12) * 0.05, + phi0: 0, + nullPhi: 0, +})); + +/** + * The ligature corpus is a 380 MB cache that is not in the repo, and the builder + * cannot caption a mark without it. Skipping says so out loud rather than failing + * on a machine that was never going to be able to run this. + */ +const haveCorpus = ROWS.length === 60; + +const atEdge = (r) => Math.abs(Math.abs(r.dx) - RADIUS) < 1e-6 || Math.abs(Math.abs(r.dy) - RADIUS) < 1e-6; +const isPlaced = (r) => r.iouBest >= IOU && !atEdge(r); +const id = (r) => `${r.page}:${r.k}`; + +let dir; +let rowsPath; +let nth = 0; + +beforeAll(() => { + dir = mkdtempSync(join(tmpdir(), "hifth-build-")); + rowsPath = join(dir, "rows.json"); + writeFileSync(rowsPath, JSON.stringify(ROWS)); +}); +afterAll(() => rmSync(dir, { recursive: true, force: true })); + +/** Build one page and hand back what a reader would be shown, read off the page. */ +function build(...flags) { + const out = join(dir, `page-${(nth += 1)}.html`); + execFileSync(process.execPath, [SCRIPT, "--rows", rowsPath, "--out", out, ...flags], { + encoding: "utf8", + stdio: "pipe", + }); + const html = readFileSync(out, "utf8"); + return { + html, + ids: [...html.matchAll(/"id":"(\d+:\d+)"/g)].map((m) => m[1]), + head: JSON.parse(/^const HEAD = (\{.*\});$/m.exec(html)[1]), + cards: JSON.parse(/^const CARDS = (\[.*\]);$/m.exec(html)[1]), + }; +} + +describe.skipIf(!haveCorpus)("cutting a population into parts somebody can finish", () => { + it("makes the parts add up to the population exactly", () => { + const want = ROWS.filter((r) => !isPlaced(r)).map(id); + const seen = new Map(); + let cards = 0; + for (let n = 1; n <= 5; n += 1) { + const got = build("--set", "fallback", "--part", `${n}/5`); + cards += got.ids.length; + for (const one of got.ids) seen.set(one, n); + } + // Three separate claims, because they fail differently: nothing shown twice, + // nothing left out, and no mark shown that was never in the population. + expect(cards).toBe(seen.size); + expect([...seen.keys()].sort()).toEqual([...want].sort()); + expect(seen.size).toBe(want.length); + }); + + it("gives each part its own place to keep answers", () => { + // Same fingerprint, same set, same seed — only the part differs. If that did not + // reach the storage key, part 2 would open showing part 1's answers already + // given, and the reader would page past marks nobody ever looked at. + const a = build("--set", "fallback", "--part", "1/5"); + const b = build("--set", "fallback", "--part", "2/5"); + expect(a.head.slice).not.toBe(b.head.slice); + }); + + it("leaves the key alone when no part and no band were asked for", () => { + // A sitting banked before these flags existed is keyed without them. Adding a + // flag must not orphan somebody's finished half-hour. + expect(build("--set", "fallback", "--count", "5").head.slice).toBe(""); + }); + + it("puts the slice into the storage key", () => { + // The key is assembled at runtime from the head, so the tests above compare + // slices rather than keys. This is the other half: that the slice reaches the + // key at all. Rebuilding the formula here instead would go on passing happily + // the day somebody drops it, which is the only failure worth catching. + expect(build("--set", "fallback", "--count", "5").html).toMatch(/const KEY = .*HEAD\.slice/); + }); + + it("refuses a part that is not one of m", () => { + expect(() => build("--set", "fallback", "--part", "6/5")).toThrow(); + expect(() => build("--set", "fallback", "--part", "0/5")).toThrow(); + }); +}); + +describe.skipIf(!haveCorpus)("drawing one band of confidence at a time", () => { + it("shows only marks whose match falls inside the band", () => { + const got = build("--set", "placed", "--band", "0.65,0.75", "--count", "100"); + const inside = new Set( + ROWS.filter((r) => isPlaced(r) && r.iouBest >= 0.65 && r.iouBest < 0.75).map(id), + ); + expect(got.ids.length).toBeGreaterThan(0); + for (const one of got.ids) expect(inside.has(one)).toBe(true); + }); + + it("does not let one band hand a mark to the next", () => { + const lower = build("--set", "placed", "--band", "0.65,0.75", "--count", "100"); + const upper = build("--set", "placed", "--band", "0.75,0.85", "--count", "100"); + const shared = lower.ids.filter((one) => upper.ids.includes(one)); + expect(shared).toEqual([]); + }); + + it("keeps two bands' answers apart", () => { + const lower = build("--set", "placed", "--band", "0.65,0.75", "--count", "5"); + const upper = build("--set", "placed", "--band", "0.75,0.85", "--count", "5"); + expect(lower.head.slice).not.toBe(upper.head.slice); + }); + + it("stops rather than build a sitting with nothing in it", () => { + // An empty band is a typo almost every time. Writing the page anyway produces a + // file that opens, says "that is all of them", and banks a transcript claiming a + // clean run over zero marks. + expect(() => build("--set", "placed", "--band", "0.01,0.02")).toThrow(); + }); + + it("refuses a band that runs backwards", () => { + expect(() => build("--set", "placed", "--band", "0.8,0.6")).toThrow(); + }); +}); + +/** + * The count only comes down if answering actually takes a mark out of the pool. + * + * Somebody asked how to submit answers one at a time and see the number left fall, + * and the two halves are separable: the submitting is a server, but the falling is + * this. Without it the sixteen parts are rebuilt from the whole population every + * time and a reader who has answered two hundred marks is handed back exactly what + * they started with — which is both dispiriting and, worse, indistinguishable from + * a rebuild that lost their work. + */ +describe.skipIf(!haveCorpus)("taking answered marks out of what is left", () => { + const answersAt = (name, evs) => { + const p = join(dir, name); + writeFileSync(p, JSON.stringify({ said: evs })); + return p; + }; + + it("does not show a mark somebody has already answered", () => { + const before = build("--set", "fallback", "--count", "100"); + const gone = before.ids.slice(0, 4); + const after = build( + "--set", "fallback", "--count", "100", + "--answered", answersAt("a1.json", gone.map((one) => ({ kind: "looks-right", id: one }))), + ); + for (const one of gone) expect(after.ids).not.toContain(one); + expect(after.ids.length).toBe(before.ids.length - gone.length); + }); + + it("counts a retracted answer as no answer at all", () => { + // Said and then unsaid is not an answer, and the marks a reader took back are + // the hard ones — precisely the ones it would be worst to quietly bury. The + // running log keeps both statements because it is appended to, never rewritten, + // so this arithmetic is the only thing standing between the two readings. + const before = build("--set", "fallback", "--count", "100"); + const one = before.ids[0]; + const p = join(dir, "a2.jsonl"); + writeFileSync( + p, + [ + JSON.stringify({ kind: "report", payload: { kind: "placement", id: one } }), + JSON.stringify({ kind: "report", payload: { kind: "retracted", id: one, was: "placement" } }), + ].join("\n") + "\n", + ); + expect(build("--set", "fallback", "--count", "100", "--answered", p).ids).toContain(one); + }); + + it("gives a shrunken sitting its own place to keep answers", () => { + // The kept position is an index into the cards. Drop marks and leave the key + // alone and that index points into a set it was never measured against — a + // reader who reached card ninety of a hundred and seventeen reopens a rebuilt + // sitting of eighty and is told it is finished, having never seen most of it. + const before = build("--set", "fallback", "--count", "100"); + const after = build( + "--set", "fallback", "--count", "100", + "--answered", answersAt("a3.json", [{ kind: "looks-right", id: before.ids[0] }]), + ); + expect(after.head.slice).not.toBe(before.head.slice); + expect(before.head.slice).toBe(""); + }); + + it("says how big the population was before the answered ones came out", () => { + // "80 of 117" is unreadable a month later without the number it came down from, + // and a transcript that cannot say which pass of the population it belongs to + // cannot be pooled with the one before it. + const before = build("--set", "fallback", "--count", "100"); + const after = build( + "--set", "fallback", "--count", "100", + "--answered", answersAt("a4.json", before.ids.slice(0, 3).map((one) => ({ kind: "looks-right", id: one }))), + ); + expect(after.head.alreadyAnswered).toBe(3); + expect(after.head.population).toBe(after.head.pool + 3); + }); + + it("stops rather than build a sitting of marks that were all answered", () => { + const all = build("--set", "fallback", "--count", "1000"); + const p = answersAt("a5.json", all.ids.map((one) => ({ kind: "looks-right", id: one }))); + expect(() => build("--set", "fallback", "--count", "1000", "--answered", p)).toThrow(); + }); +}); + +/** + * Answers survive a reload; for one sitting the drawn rectangle did not, and neither + * did the reader's place. Twenty-five carefully nudged marks opened again at the + * rectangle they shipped with, on card one, with nothing on the page saying the + * corrections were still held — so the only reasonable thing to do was the work + * again, and a second pass appends rather than replaces. + * + * The page's own load path cannot be exercised here: it wants a document and a store, + * and this file builds HTML with node. So these read the script the builder emits and + * hold it to the two things that were missing. It is a coarse test for a defect that + * cost an evening, and a coarse test that would have caught it is worth having. + */ +describe.skipIf(!haveCorpus)("giving a reader back the sitting they left", () => { + it("puts every standing answer back on its rectangle before the first draw", () => { + const { html } = build("--set", "fallback", "--count", "20"); + const replay = html.indexOf("for (const id of new Set(said.map("); + expect(replay).toBeGreaterThan(-1); + // Before the first draw, or the reader still sees one untouched page and has no + // way to know a second one is coming. + expect(replay).toBeLessThan(html.lastIndexOf("\nrender();")); + }); + + it("keeps where the reader was standing, not only how far they got", () => { + const { html } = build("--set", "fallback", "--count", "20"); + // Two different questions, and only the high-water mark used to survive. + expect(html).toContain("keepAt(at);"); + expect(html).toContain("Math.min(keptAt(), DECK.length - 1)"); + }); + + /** + * A reader banked an hour of answers, watched the count under the card stay where + * it was, banked again, and watched it stay again — and said so twice. Nothing was + * lost either time; the deal is fixed when the page is built and the count was of + * the deal. But an instrument that cannot show a reader their own work is one they + * stop trusting, and it takes forty hours of theirs on trust. + * + * So the two lists are separate now, and these are the three things that have to + * stay true about them together. + */ + it("takes the handed-over marks off the deck and leaves the transcript whole", () => { + const { html } = build("--set", "fallback", "--count", "20"); + // The deal is still the deal, and what is left is derived from it. + expect(html).toContain("let DECK = CARDS.filter(function (c) { return !GONE.has(c.id); });"); + // Retiring is what handing over does, on every path a file leaves the page by. + expect(html).toContain("const went = retire();"); + // It survives a reload, or the retired marks come back and the reader is told + // their work did not count after all — the same failure, one refresh later. + expect(html).toContain("keepGone([...GONE]);"); + }); + + it("never lets a shrinking deck shrink the file that gets written", () => { + const { html } = build("--set", "fallback", "--count", "20"); + // Every hand-over writes the whole transcript under one name, so a later write + // that had lost answers would silently destroy an earlier one that had them. + // That is why what comes back from the session is filtered against the deal. + expect(html).toContain("const mine = new Set(CARDS.map(function (c) { return c.id; }));"); + // And why retiring touches the deck and the place, and nothing else. + const retire = html.slice(html.indexOf("function retire()"), html.indexOf("function handOver(")); + expect(retire).not.toContain("said ="); + expect(retire).not.toContain("keep(said)"); + }); + + /** + * The rest of this file reads the emitted script as text. This one runs it, because + * the thing that can be wrong here is arithmetic on two indices that shift under + * each other — where the reader is standing, and how far they had got — and no + * amount of string-matching can tell you a subtraction is right. + * + * Five marks, and the reader is placed somewhere different in each: the point is + * that the position follows the card the reader was on rather than being reset, and + * that a mark passed in silence stays owed while an answered one goes. + */ + it("moves the reader with the deck instead of resetting them", () => { + const { html } = build("--set", "fallback", "--count", "20"); + const src = html.slice(html.indexOf("function retire()"), html.indexOf("function handOver(")); + const noop = () => {}; + const run = (deck, at, seen, answers) => + new Function( + "DECK", "GONE", "at", "seen", "said", "keepGone", "keepSeen", "keepAt", "render", + `${src}\nconst w = retire(); return { went: w, left: DECK.map((c) => c.id), at, seen };`, + )(deck.map((id) => ({ id })), new Set(), at, seen, answers.map((id) => ({ id })), noop, noop, noop, noop); + const all = ["a", "b", "c", "d", "e"]; + + // The reported case: one answered, standing on the next. It becomes card one of + // four, and none of the four has been looked at — which is the truth. + expect(run(all, 1, 1, ["a"])).toEqual({ went: 1, left: ["b", "c", "d", "e"], at: 0, seen: 0 }); + // Two go from either side of where they stand; they stay on the same card, and + // the one they passed in silence is still counted as looked at and still owed. + expect(run(all, 3, 3, ["a", "c"])).toEqual({ went: 2, left: ["b", "d", "e"], at: 1, seen: 1 }); + // Nothing survives at or after where they stood, so the sitting is over. Past the + // end rather than wrapped to the start: there is genuinely no next card. + expect(run(all, 3, 4, ["d", "e"])).toEqual({ went: 2, left: ["a", "b", "c"], at: 3, seen: 3 }); + // Handing over an empty sitting, and handing the same one over twice, both do + // nothing — which is what lets the button stay pressable without a reader having + // to remember whether they already pressed it. + expect(run(all, 2, 2, [])).toEqual({ went: 0, left: all, at: 2, seen: 2 }); + expect(run(["b", "c"], 0, 0, ["a"])).toEqual({ went: 0, left: ["b", "c"], at: 0, seen: 0 }); + }); + + /** + * The count under the card and the count in the file are different questions, and + * for one sitting they were answered with the same variable. The screen wants what + * is left, so it shrinks; the file wants the whole sitting, and it is the + * denominator of the only rate this instrument produces — one sitting came back + * saying nineteen marks had been looked at when a hundred and fifteen had been + * answered, and the scorer duly printed two thousand per cent. + * + * The invariant, and it is the whole of the fix: what the file gets can only ever + * go up. The test runs retiring rather than reading it, because both halves of the + * sum move at once and in opposite directions. + */ + it("writes the whole sitting into the file even as the deck shrinks under it", () => { + const { html } = build("--set", "fallback", "--count", "20"); + const over = html.slice(html.indexOf("function handOver("), html.indexOf("function handOver(") + 900); + expect(over).toContain("seen: GONE.size + seen"); + + const src = html.slice(html.indexOf("function retire()"), html.indexOf("function handOver(")); + const noop = () => {}; + // Retiring in place, so the two halves of the sum can be read after each round the + // way a hand-over reads them: what has been given away, plus what is still here + // and has been passed. + const round = (state, answers) => + new Function( + "DECK", "GONE", "at", "seen", "said", "keepGone", "keepSeen", "keepAt", "render", + `${src}\nretire(); return { deck: DECK.map((c) => c.id), GONE, at, seen };`, + )(state.deck.map((id) => ({ id })), state.GONE, state.at, state.seen, + answers.map((id) => ({ id })), noop, noop, noop, noop); + const filed = (s) => s.GONE.size + s.seen; + + let s = { deck: ["a", "b", "c", "d", "e"], GONE: new Set(), at: 5, seen: 5 }; + expect(filed(s)).toBe(5); + // Two handed over. The deck loses them and the screen's count follows it down — + // but all five have still been looked at, and that is what the file must carry. + s = round(s, ["a", "c"]); + expect(s.seen).toBe(3); + expect(filed(s)).toBe(5); + // And again, with the answers of the first round still standing in the transcript, + // because the transcript is never retired with the deck. Nothing is counted twice. + s = round(s, ["a", "c", "d"]); + expect(s.seen).toBe(2); + expect(filed(s)).toBe(5); + }); +}); + +/** + * A crop of print holds several marks, often several of the same name, and a page + * that says only "fatha" has not asked a question anybody can answer — the reader + * is shown a rectangle and left to guess which of four fathas it was supposed to be + * on. The letters are the answer, and the rank settles it when the letters alone + * cannot. This is what the person sitting the first fallback page said was missing, + * so it is checked here rather than left to be noticed again. + */ +describe.skipIf(!haveCorpus)("saying which mark is being asked about", () => { + it("names the letters the print drew each mark on", () => { + const got = build("--set", "fallback", "--count", "20"); + expect(got.cards.length).toBeGreaterThan(0); + for (const c of got.cards) { + // One or the other, always: the letters when the print drew the mark inside a + // ligature, and the word when it drew it under a word but on no letter of its + // own. A card with neither names nothing and is the failure this test exists + // for; a card with both is normal. + expect(c.on || c.word).toBeTruthy(); + } + }); + + it("tells two marks of one name on one ligature apart", () => { + const got = build("--set", "fallback", "--count", "20"); + for (const c of got.cards) { + expect(c.of).toBeGreaterThanOrEqual(1); + expect(c.nth).toBeGreaterThanOrEqual(1); + // The rank has to be sayable: "the fourth of two" is worse than saying nothing, + // because a reader who counts will conclude the page is broken and stop. + expect(c.nth).toBeLessThanOrEqual(c.of); + } + }); + + it("puts the mark under discussion in the middle of both windows", () => { + // The row's rectangle and the drawn one are a correction apart, and framing on + // the first while drawing the second walks the mark off toward the edge — + // furthest exactly when the correction is largest, which is when a reader most + // needs to see it. Looking closer magnified that rather than fixing it. + const got = build("--set", "fallback", "--count", "20"); + for (const c of got.cards) { + const cx = c.at[0] + c.at[2] / 2; + const cy = c.at[1] + c.at[3] / 2; + for (const vb of [c.vb, c.near]) { + expect(Math.abs(vb[0] + vb[2] / 2 - cx)).toBeLessThan(0.02); + expect(Math.abs(vb[1] + vb[3] / 2 - cy)).toBeLessThan(0.02); + } + } + }); + + it("keeps the drawn rectangle a correction away from the row's own", () => { + // The guard on the test above: if `at` ever collapsed onto the row's rectangle, + // the centring assertion would pass trivially and stop meaning anything. These + // are the refused marks, so every one of them carries a real correction. + const got = build("--set", "fallback", "--count", "20"); + const moved = got.cards.filter((c) => Math.abs(c.at[0] - c.box[0]) + Math.abs(c.at[1] - c.box[1]) > 0.01); + expect(moved.length).toBe(got.cards.length); + }); + + it("refuses to caption a row it cannot match to the print", () => { + // The index walks the print in document order, so rows built from a different + // revision line up off by one and every card is captioned with its neighbour's + // letters — silently, and in the direction that makes a correct placement look + // wrong. Better to stop than to mislead a whole sitting. + const bad = join(dir, "rows-mismatched.json"); + writeFileSync(bad, JSON.stringify(ROWS.map((r) => ({ ...r, name: "no-such-mark" })))); + expect(() => + execFileSync(process.execPath, [SCRIPT, "--rows", bad, "--out", join(dir, "bad.html"), "--set", "fallback"], { + encoding: "utf8", + stdio: "pipe", + }), + ).toThrow(); + }); +}); + +/** + * The page is a JavaScript template literal, all of it — style, markup and script. + * + * A backtick anywhere inside it ends the string early, and the file then fails to + * parse somewhere else entirely, with a message about the wrong line. It has broken + * this file three times, twice inside a comment that was never meant to be code. + * Two assertions cost nothing and say the trap out loud to whoever trips it next. + */ +describe.skipIf(!haveCorpus)("the trap the page is built in", () => { + it("emits no backtick and no interpolation of its own", () => { + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).not.toContain("`"); + expect(html).not.toContain("${"); + }); +}); + +/** + * Two letters — move on, and say the print is odd without being able to say how — + * and the guards that keep them from firing when they are just letters. + * + * The risk in a bare letter shortcut is not that it fails to fire; a reader finds + * that out immediately. It is that it fires while somebody is typing a sentence + * about why a mark is odd, and deals them away from the card they were describing. + * So the guards are the part worth pinning, not the binding. + * + * The claim these tests hold is narrower than the one the first of them held. It was + * *no key records anything*, which was the wrong line: what makes a binding dangerous + * is not recording, it is recording while the card leaves, because then the slip goes + * off the screen with the evidence of itself. So the rule pinned here is that no key + * both records and moves — the odd-print key stays on the card, lights the button it + * pressed, and undoes on a second press. + */ +describe.skipIf(!haveCorpus)("answering and moving on without reaching for a button", () => { + const page = () => build("--set", "fallback", "--count", "5").html; + const handler = (html) => { + const i = html.indexOf('document.addEventListener("keydown"'); + expect(i).toBeGreaterThan(-1); + return html.slice(i, html.indexOf("});", i)); + }; + // What it does, with what it says about itself taken out. The comments here name + // the very things the assertions below say must not be called, and a test that + // cannot tell a mention from a call fails on a paragraph. + const code = (s) => s.split("\n").filter((l) => !l.trim().startsWith("//")).join("\n"); + + it("moves to the next card and nowhere else", () => { + const h = handler(page()); + expect(h).toContain("go(1);"); + expect(h).not.toContain("go(-1)"); + }); + + it("keeps out of the note box, where these are letters somebody is typing", () => { + const h = handler(page()); + expect(h).toContain('$("ask").open'); + expect(h).toContain('tag === "TEXTAREA"'); + expect(h).toContain('tag === "INPUT"'); + expect(h).toContain("isContentEditable"); + }); + + it("gives the key up to the browser and does not run away when it is held", () => { + const h = handler(page()); + expect(h).toContain("e.repeat"); + expect(h).toContain("e.ctrlKey"); + expect(h).toContain("e.metaKey"); + expect(h).toContain("e.altKey"); + }); + + it("presses the reader's own button rather than writing an answer of its own", () => { + const h = code(handler(page())); + expect(h).toContain('$("why-" + r[0]).click()'); + // Not a second path to the transcript. The button is the only one, so a key + // cannot answer in a way pressing it never would. + expect(h).not.toContain("say("); + expect(h).not.toContain("toggle("); + }); + + it("does not let the card leave on the key that records", () => { + const h = code(handler(page())); + const loop = h.slice(h.indexOf("for (const r of REASONS)")); + expect(loop.length).toBeGreaterThan(0); + expect(loop).not.toContain("go("); + // And it opens what it just wrote into, or the press looks like it missed. + expect(loop).toContain("oddOpen = true"); + }); + + it("keeps every letter in one place, so nothing can bind a second one quietly", () => { + expect(page().split("keydown").length - 1).toBe(1); + }); + + it("gives a key to the vaguest reason only, and prints it on the button", () => { + const html = page(); + const rows = html.slice(html.indexOf("const REASONS = ["), html.indexOf("];", html.indexOf("const REASONS = ["))); + // A row with a key carries three quoted strings; one without carries two. + const keyed = rows.split("\n").filter((l) => (l.match(/"/g) || []).length === 6); + expect(keyed).toHaveLength(1); + expect(keyed[0]).toContain('"unsure"'); + expect(keyed[0]).toContain('"o"'); + expect(html).toContain('r[2] ? "" + r[2] + "" : ""'); + }); + + it("says which key, where there is a keyboard to press it on", () => { + const html = page(); + expect(html).toContain("n"); + expect(html).toContain("@media (pointer: fine)"); + // Not scoped to the button strip any more, or the chip's own key would not show. + expect(html).not.toContain(".dock kbd"); + }); +}); + +/** + * Nothing drawn on the paper is themed. + * + * The crop is a photograph of print and stays white in both themes on purpose, but + * the two rectangle colours were in the re-themed set, so dark mode lightened them + * and then drew them on that unchanged white: 5.05:1 fell to 2.49:1 and 4.89:1 to + * 1.70:1. That is not invisible — it is worse than invisible, because a reader can + * still make out something rectangular, believe they have looked, and affirm. The + * finding runs in the direction that reads as success, which is why it gets a test + * rather than a comment. + */ +const NEVER_THEMED = ["paper", "ink", "ours-line", "ours-wash", "yours-line", "yours-wash"]; + +const channels = (hex) => + [1, 3, 5].map((i) => { + const c = parseInt(hex.slice(i, i + 2), 16) / 255; + return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; + }); +const luminance = (hex) => { + const [r, g, b] = channels(hex); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +}; +const contrast = (a, b) => { + const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); + return (hi + 0.05) / (lo + 0.05); +}; + +describe.skipIf(!haveCorpus)("keeping the rectangles legible on paper that never changes", () => { + const tokensIn = (css) => { + const found = {}; + for (const m of css.matchAll(/--([a-z-]+):\s*(#[0-9a-f]{6,8})/gi)) found[m[1]] = m[2]; + return found; + }; + const sheets = () => { + const { html } = build("--set", "fallback", "--count", "5"); + return { + light: tokensIn(/:root \{([\s\S]*?)\n\}/.exec(html)[1]), + // Lazy to the first close at column zero — the selector inside it is indented, + // so its own brace cannot end the match early. + dark: tokensIn(/@media \(prefers-color-scheme: dark\) \{([\s\S]*?)\n\}/.exec(html)[1]), + }; + }; + + it("re-themes nothing that is drawn on the crop", () => { + const { light, dark } = sheets(); + // Every one of them defined once, in the theme-independent block... + for (const name of NEVER_THEMED) expect(light[name]).toMatch(/^#[0-9a-f]{6,8}$/i); + // ...and named nowhere in the dark one. This is the whole invariant. + for (const name of NEVER_THEMED) expect(dark[name]).toBeUndefined(); + }); + + it("still re-themes the chrome, which is the reason the two sets exist", () => { + // The guard on the test above. If the dark block ever emptied out, that test + // would pass forever while the page stopped having a dark theme at all. + const { dark } = sheets(); + expect(Object.keys(dark).length).toBeGreaterThan(0); + for (const name of ["field", "text", "ours", "yours"]) expect(dark[name]).toBeTruthy(); + }); + + it("draws both rectangles at 3:1 or better against the paper", () => { + const { light } = sheets(); + // 3:1 is the threshold for a graphical object rather than for text, and these + // are graphical objects: a hairline rectangle around a mark, not a label. + expect(contrast(light["ours-line"], light.paper)).toBeGreaterThanOrEqual(3); + expect(contrast(light["yours-line"], light.paper)).toBeGreaterThanOrEqual(3); + }); + + it("tells the two rectangles apart by something other than hue", () => { + // Ours and the reader's are orange and green, which is the one pair a red-green + // reader cannot separate. The dash carries the distinction on its own, and it + // survives anybody re-picking the palette later. + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toMatch(/rect\.mine \{ stroke-dasharray:/); + }); +}); + +/** + * The crop is parsed once per card, not once per pointer frame. + * + * The drag handler used to rebuild the stage from the card's path data on every + * `pointermove` — 2.0 KB on the smallest crop, 23.2 KB on the largest. A correction + * that stutters is one the reader gives up on and affirms instead, which turns a + * performance defect into a wrong answer in the transcript. + * + * This reads the emitted script rather than running it, in the same spirit as the + * replay tests above: coarse, and it catches the regression that matters. + */ +describe.skipIf(!haveCorpus)("parsing the crop once per card", () => { + it("never rebuilds the stage while drawing a frame", () => { + const { html } = build("--set", "fallback", "--count", "5"); + const paint = /\nfunction paint\(\) \{\n([\s\S]*?)\n\}\n/.exec(html); + expect(paint).toBeTruthy(); + expect(paint[1]).not.toContain("innerHTML"); + }); + + it("writes the crop into the page in exactly one place, and that place is mount", () => { + const { html } = build("--set", "fallback", "--count", "5"); + const script = html.slice(html.indexOf("function mount(")); + expect([...script.matchAll(/c\.svg/g)].length).toBe(1); + const mount = /\nfunction mount\(c\) \{\n([\s\S]*?)\n\}\n/.exec(html); + expect(mount[1]).toContain("c.svg"); + }); + + it("measures the rectangle's line in screen pixels, not in page units", () => { + // The old stroke width was the viewBox over 220, which is 1.64px only while the + // stage is exactly as wide as the column. Anything that sizes the stage + // differently — a dock, a landscape phone — makes line weight a function of each + // card's shape, and the thinnest cards are the ones hardest to judge. + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toContain('vector-effect="non-scaling-stroke"'); + expect(html).toMatch(/rect\.grab, svg\.stage rect\.mine \{ stroke-width:/); + }); +}); + +/** + * Saying which mark, in words a reader can check against the print. + * + * The person who sat the first fallback page said they could not tell which of four + * fathas the rectangle was supposed to be on. The card has always carried the word + * the mark sits in and thrown it away whenever it also had the letters, which is the + * common case; and the sentence under the letters named the mark in only one of its + * three branches, so a reader was told "the second of four on these letters" without + * ever being told the second *what*. + */ +describe.skipIf(!haveCorpus)("naming the mark and the word it sits in", () => { + it("carries a longer containing word on every card that names single letters", () => { + // This is a fact about the data, and it is what makes the fix possible: if the + // word were only ever the letters again, appending it would add nothing and this + // would be the test that noticed. + const got = build("--set", "fallback", "--count", "40"); + const single = got.cards.filter((c) => c.on && [...c.on].length === 1); + expect(single.length).toBeGreaterThan(0); + for (const c of single) { + expect(c.word).toBeTruthy(); + expect([...c.word].length).toBeGreaterThan([...c.on].length); + } + }); + + it("says the mark's name whichever of the three things it has to say", () => { + const { html } = build("--set", "fallback", "--count", "5"); + const identify = /\nfunction identify\(([\s\S]*?)\n\}\n/.exec(html); + expect(identify).toBeTruthy(); + // No letters at all · one of several · the only one. Three branches, three names. + expect([...identify[1].matchAll(/c\.name/g)].length).toBeGreaterThanOrEqual(3); + }); + + it("puts the word on the card in its own direction", () => { + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toMatch(/\.ident \.word \{ direction: rtl;/); + expect(html).toContain('w.setAttribute("lang", "ar")'); + }); +}); + +/** + * The instructions get out of the way without making any answer cost more. + * + * The explanation was about 275px tall and never went away, which put the affirm + * button below the fold on a 393px phone for the whole sitting. It cannot simply be + * deleted — the first card needs it — and the fault buttons must not go behind a + * disclosure either, because adding a tap to reporting a fault while affirming stays + * free biases the exact ratio the sitting exists to measure. So: two ledes and a + * quiet toggle, and the short one appears once the reader has demonstrably read the + * long one. + */ +describe.skipIf(!haveCorpus)("getting the explanation off the fold", () => { + it("carries both a short lede and the full one", () => { + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toMatch(/id="brief"/); + expect(html).toMatch(/id="full"/); + expect(html).toMatch(/id="ledeSwap"/); + }); + + it("remembers which one the reader asked for, and lets them ask for either", () => { + const { html } = build("--set", "fallback", "--count", "5"); + // Three states, not two. A boolean derived from progress alone would re-collapse + // the long version on the very next answer, one tap after the reader asked for it. + expect(html).toContain("KEY + \"-read\""); + expect(html).toMatch(/chose === null \? seen > 0 : chose === "1"/); + }); + + it("keeps the number of taps an answer costs where it was", () => { + // The whole design constraint in one assertion. Shortening the explanation must + // not be paid for by hiding the fault buttons: a fault that costs a tap more than + // an affirmation biases the one ratio these sittings exist to measure, and it + // biases it toward agreement. All five stay on the card, none behind a disclosure. + const { html } = build("--set", "fallback", "--count", "5"); + const acts = /
      ([\s\S]*?)<\/div>/.exec(html); + expect(acts).toBeTruthy(); + for (const id of ["off", "shape", "draw", "print", "skip"]) { + expect(acts[1]).toContain(`id="${id}"`); + } + expect(html).not.toMatch(/
      { + const bounds = (html) => ({ + min: Number(/const STEP_MIN = ([\d.]+);/.exec(html)[1]), + max: Number(/const STEP_MAX = ([\d.]+);/.exec(html)[1]), + }); + + it("offers a range, and the slider agrees with the clamp about its ends", () => { + const { html } = build("--set", "fallback", "--count", "5"); + const { min, max } = bounds(html); + expect(min).toBe(0.01); + expect(max).toBe(0.5); + const input = //.exec(html); + expect(input).toBeTruthy(); + expect(input[0]).toContain(`min="${min}"`); + expect(input[0]).toContain(`max="${max}"`); + // Hundredths, so every position is a number that reads cleanly in the transcript. + expect(input[0]).toContain('step="0.01"'); + // And the two-position swap is gone rather than left beside its replacement. + expect(html).not.toContain("Finer steps"); + expect(html).not.toContain("stepx"); + }); + + it("moves the rectangle by whatever the reader chose", () => { + const { html } = build("--set", "fallback", "--count", "5"); + const nudge = /\nfunction nudge\(([\s\S]*?)\n\}\n/.exec(html); + expect(nudge).toBeTruthy(); + expect(nudge[1]).toContain("const s = step;"); + }); + + it("remembers the size across a reload, and refuses a stored value that is not one", () => { + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toContain('KEY + "-step"'); + const { min, max } = bounds(html); + const src = /\nfunction okStep\(v\) \{[^\n]*\n/.exec(html); + expect(src).toBeTruthy(); + const okStep = new Function("STEP_MIN", "STEP_MAX", `${src[0]}\nreturn okStep;`)(min, max); + // A press of nothing is not a smaller press, it is a control that has stopped + // working — and a reader would have no way to tell which they were looking at. + expect(okStep(NaN)).toBe(max); + expect(okStep(0)).toBe(max); + expect(okStep(-0.2)).toBe(max); + expect(okStep(max + 0.01)).toBe(max); + expect(okStep(min)).toBe(min); + expect(okStep(0.07)).toBe(0.07); + // Hundredths out, whatever the browser hands in: the number goes into the + // transcript as the size of the answer, and 0.30000000000000004 is not a size + // anybody chose. + expect(okStep(0.1 + 0.2)).toBe(0.3); + }); +}); + +/** + * Pointing at the mark instead of pushing a rectangle onto it. + * + * The first sitting measured what the by-hand loop costs: 127 of the 128 rectangles + * a reader looked at were moved, over 226 separate presses, a median of 2.009 units + * of travel and a worst of 7.052. Every one of those presses is the reader doing + * arithmetic — deciding not only which mark is meant, which only a person can do, + * but also how far across and how far down, which the print already knows. + * + * So the ink of the window is cut into pieces, each piece is drawn as its own shape + * with a number on it, and a tap puts the rectangle round whatever the reader has + * pointed at. The two halves of that are tested apart: that the pieces exist and are + * numbered, and that the geometry a tap produces is the extent of the ink chosen. + */ +describe.skipIf(!haveCorpus)("pointing at the ink the rectangle belongs on", () => { + it("cuts every card's window into pieces a finger can point at", () => { + // A card with no pieces is one where the gesture silently does nothing, and the + // reader has no way to tell that from a tap that missed. There is no fallback + // for it, so it must not happen. + const got = build("--set", "fallback", "--count", "20"); + expect(got.cards.length).toBeGreaterThan(0); + for (const c of got.cards) { + expect(Array.isArray(c.pieces)).toBe(true); + expect(c.pieces.length).toBeGreaterThan(0); + for (const b of c.pieces) { + expect(b.length).toBe(4); + for (const v of b) expect(Number.isFinite(v)).toBe(true); + expect(b[2]).toBeGreaterThan(0); + expect(b[3]).toBeGreaterThan(0); + } + } + }); + + it("numbers the ink it draws, so a tap can say which piece it landed on", () => { + // The tap looks its answer up by number, so a piece that is measured but never + // drawn is unreachable and a shape drawn under a number nothing measured moves + // the rectangle somewhere arbitrary. The two lists have to be the same list. + const got = build("--set", "fallback", "--count", "20"); + for (const c of got.cards) { + const drawn = new Set([...c.svg.matchAll(/data-p="(\d+)"/g)].map((m) => Number(m[1]))); + expect(drawn.size).toBe(c.pieces.length); + for (let i = 0; i < c.pieces.length; i += 1) expect(drawn.has(i)).toBe(true); + } + }); + + it("takes the extent of everything pointed at, not of the last thing pointed at", () => { + // Two pieces is the normal case rather than the edge: a tanween is two strokes, + // and a reader who pointed at one half of one has not yet said where the mark is. + const { html } = build("--set", "fallback", "--count", "5"); + const src = /\nfunction unionOf\(c, ids\) \{[\s\S]*?\n\}\n/.exec(html); + expect(src).toBeTruthy(); + const unionOf = new Function(`${src[0]}\nreturn unionOf;`)(); + const c = { pieces: [[10, 10, 2, 2], [14, 11, 1, 3]] }; + expect(unionOf(c, [0])).toEqual([10, 10, 2, 2]); + expect(unionOf(c, [0, 1])).toEqual([10, 10, 5, 4]); + }); + + it("forgives a fingertip, and gives the smaller of two pieces to the finger", () => { + // A haraka is a stroke a few screen pixels wide at the framing that shows the + // whole word. Exact hit-testing would mean stabbing at it and missing every + // time; and where the mark sits over the letter it belongs to, both answer, so + // the smaller has to win or the gesture always returns the letter. + const { html } = build("--set", "fallback", "--count", "5"); + const src = /\nfunction pieceAt\(c, p\) \{[\s\S]*?\n\}\n/.exec(html); + expect(src).toBeTruthy(); + // A window 30 units across, so the slack the page allows is exactly one unit. + const pieceAt = new Function("framing", `${src[0]}\nreturn pieceAt;`)(() => [0, 0, 30, 30]); + const c = { pieces: [[0, 0, 10, 10], [4, 4, 1, 1]] }; + expect(pieceAt(c, [4.5, 4.5])).toBe(1); + expect(pieceAt(c, [9, 9])).toBe(0); + expect(pieceAt(c, [20, 20])).toBe(null); + expect(pieceAt({ pieces: [] }, [0, 0])).toBe(null); + }); + + it("does not reach past the ink the finger is actually on", () => { + // Smaller-wins used to be the only rule, applied flatly across everything + // within slack — so a finger squarely inside a large piece was answered with a + // small piece it had merely come near, and the rectangle jumped onto ink the + // reader could see they had not touched. + // + // Ranking landed-on above came-near is what fixes it, and it is measurable + // rather than a matter of taste: over two built sittings, aiming at the dead + // centre of a piece returned a different piece on 3.7% of aims at the wide + // framing before and 1.7% after, and taps on blank paper that snapped past + // nearer ink to something smaller further away — 2.3% of them — stopped + // entirely. Slack still forgives a near miss; it just no longer overrules a + // hit. + const { html } = build("--set", "fallback", "--count", "5"); + const src = /\nfunction pieceAt\(c, p\) \{[\s\S]*?\n\}\n/.exec(html); + const pieceAt = new Function("framing", `${src[0]}\nreturn pieceAt;`)(() => [0, 0, 30, 30]); + + // Inside the letter, four-fifths of a unit clear of the mark: the letter. + expect(pieceAt({ pieces: [[0, 0, 10, 10], [4, 4, 1, 1]] }, [5.8, 4.5])).toBe(0); + // On neither, but a fifth of a unit from the large one and twice that from the + // small one: the near one, not the small one. + expect(pieceAt({ pieces: [[0, 0, 4, 4], [4.6, 1.9, 0.2, 0.2]] }, [4.2, 2])).toBe(0); + // And a near miss on the only ink there still reaches it — the forgiveness is + // what makes a mark a few screen pixels wide tappable at all. + expect(pieceAt({ pieces: [[0, 0, 1, 1], [5, 0, 1, 1]] }, [1.5, 0.5])).toBe(0); + }); + + it("tells a tap apart from a drag with one number, in one unit, and no clock", () => { + // There were two boundaries and the gap between them banked placements nobody + // made. A gesture counted as a tap only if it also finished inside 600ms, and + // separately a drag banked a placement only once it had moved more than 0.05 + // page units — so a finger resting on the rectangle with a few pixels of + // tremor was past the clock and far past the floor, on the gesture a reader + // makes most: reaching for the ink under the rectangle. A slow tap is still a + // tap, and only distance can say whether a finger stayed still. + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toContain("const TAP_PX = 10;"); + const up = /stage\.addEventListener\("pointerup", function \(e\) \{([\s\S]*?)\n\}\);\n/.exec(html); + expect(up).toBeTruthy(); + expect(up[1]).toContain("TAP_PX"); + // No clock in the decision, and no second floor under it. + expect(up[1]).not.toContain("Date.now()"); + expect(up[1]).not.toMatch(/Math\.abs\(moved\[[01]\]\) > 0/); + }); + + it("says so when a tap reaches no ink at all", () => { + // Repainting and saying nothing is indistinguishable, from the reader's side, + // from a tap the page never received — so a finger a hair off the stroke and a + // dead control look the same, and after two of those a reader stops pointing + // at ink and goes back to the nudge pad. The answers then stop being about the + // ink, which is the thing the sitting is trying to measure. + const { html } = build("--set", "fallback", "--count", "5"); + const up = /stage\.addEventListener\("pointerup", function \(e\) \{([\s\S]*?)\n\}\);\n/.exec(html); + expect(up[1]).toContain("hint(true)"); + const hint = /\nfunction hint\(miss\) \{([\s\S]*?)\n\}\n/.exec(html); + expect(hint).toBeTruthy(); + expect(hint[1]).toContain("no ink where you tapped"); + }); + + it("never lets a tap leave a rectangle too small to see", () => { + // The same floor a press is held to. An ink piece can be a single dot, and a + // rectangle the size of a dot is one the reader cannot find again to correct. + const { html } = build("--set", "fallback", "--count", "5"); + const snap = /\nfunction snap\(c\) \{([\s\S]*?)\n\}\n/.exec(html); + expect(snap).toBeTruthy(); + expect(snap[1]).toContain("Math.max(FLOOR,"); + }); + + it("says which answers came from the ink and which came from a hand", () => { + // The words are the same either way, because the reader is saying the same + // thing. Who measured it is not the same: choosing which ink is judgement, and + // the extent of that ink is arithmetic this page did. Pooling the two into one + // distribution would report a tightness that says nothing about whether a reader + // can see a misplaced rectangle — so a press joining the burst must disown the + // ink, or a hand-finished answer would be banked claiming the print placed it. + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toContain('how: "ink"'); + const flush = /\nfunction flush\(\) \{([\s\S]*?)\n\}\n/.exec(html); + expect(flush[1]).toContain("const ink = pendInk;"); + expect(flush[1]).toContain("pendInk = null;"); + const nudge = /\nfunction nudge\(([\s\S]*?)\n\}\n/.exec(html); + expect(nudge[1]).toContain("pendInk = null;"); + }); + + it("tints the ink the reader chose in a colour the paper never re-themes", () => { + // What is lit is the reader's own statement, not our guess at where the mark is + // — that guess is the unknown the sitting exists to measure and showing it would + // be handing over the answer with the question. But a statement the reader + // cannot see is one they cannot check, and it is drawn on paper that stays white + // in both themes, so it joins the set that is never re-themed. + const { html } = build("--set", "fallback", "--count", "5"); + const rule = /svg\.stage path\.on \{ fill: var\(--([a-z-]+)\); \}/.exec(html); + expect(rule).toBeTruthy(); + expect(NEVER_THEMED).toContain(rule[1]); + }); + + it("rebuilds what was pointed at from the transcript, rather than remembering it", () => { + // The rectangle is already rebuilt from the answers standing for a card rather + // than from anything held in the page, so the tint has to come from the same + // place. Remembered separately, the two part company the first time a reader + // takes something back: ink still lit under a rectangle that has gone home. + const { html } = build("--set", "fallback", "--count", "5"); + const redraw = /\nfunction redraw\(id\) \{([\s\S]*?)\n\}\n/.exec(html); + expect(redraw).toBeTruthy(); + expect(redraw[1]).toMatch(/move && move\.ink/); + expect(redraw[1]).toContain("c.picked ="); + }); +}); + +/** + * The gesture the sitting is mostly made of, which a phone was reading as zoom. + * + * Somebody sitting on a phone reported that pressing the same nudge button twice + * magnified the page. That is the browser's double-tap-to-zoom, and it fires on two + * taps anywhere near each other in time — the same arrow twice, or two arrows a + * centimetre apart, which between them is most of what placing a rectangle consists + * of. The cost is not the magnification: it is that the card walks off the screen + * mid-answer and the next press lands somewhere the reader did not aim, so the + * transcript records a placement nobody made. + * + * `manipulation` is the ordinary behaviour minus that one gesture. It buys back the + * control and costs a gesture that was never available on the print anyway, because + * the stage has carried `pan-y` since the drag was wired and `pan-y` already + * excludes double-tap zoom. Pinch still works everywhere. + */ +describe.skipIf(!haveCorpus)("two taps in a row are two taps, not a zoom", () => { + it("declines double-tap zoom for the whole page", () => { + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toMatch(/body \{[^}]*touch-action: manipulation;/); + }); + + it("puts it on the page rather than on the buttons", () => { + // Two taps on *adjacent* buttons trigger it as readily as two on one button, so + // a rule per button would leave the commonest case — left, then up — unfixed. + // One declaration, and the test above says where it is. + const { html } = build("--set", "fallback", "--count", "5"); + expect([...html.matchAll(/touch-action: manipulation/g)]).toHaveLength(1); + }); + + it("leaves the stage's stricter rules to win, as they must", () => { + // touch-action is intersected down the ancestor chain rather than inherited, so + // the stage keeps its pan-y and its none. If this ever became the only rule on + // the page, a drag on the print would start scrolling it instead. + const { html } = build("--set", "fallback", "--count", "5"); + expect(html).toMatch(/svg\.stage \{[^}]*touch-action: pan-y;/); + expect(html).toMatch(/svg\.stage\.drawing \{[^}]*touch-action: none;/); + }); +}); diff --git a/packages/etl/scripts/build-roots.mjs b/packages/etl/scripts/build-roots.mjs index ae63d255..c54660c7 100644 --- a/packages/etl/scripts/build-roots.mjs +++ b/packages/etl/scripts/build-roots.mjs @@ -67,6 +67,7 @@ import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { toAbsoluteAyah, TOTAL_AYAHS } from "@hifth/core"; import { openAlignment } from "./lib/segmentation.mjs"; +import { copyrightBlock } from "./morphology.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const REPO = join(HERE, "..", "..", ".."); @@ -82,10 +83,24 @@ const BUCKETS = 32; const GZ_LIMIT = 50 * 1024; /* ------------------------------------------------------------------ */ -/* Buckwalter → Arabic (corpus.quran.com/java/buckwalter.jsp, verbatim).*/ +/* Buckwalter → Arabic. */ +/* */ +/* The scheme is Tim Buckwalter's — a one-character-per-letter Arabic */ +/* transliteration published with the LDC's Arabic morphological */ +/* analyser and in general use since. The rows below the letters are its */ +/* "XML-safe" variants plus the extra codepoints the Quranic Arabic */ +/* Corpus needs for a mus'haf's pause, madd and small-letter marks, */ +/* which its own transliteration page sets out. */ +/* */ +/* This comment used to cite that page and say "verbatim", which read as */ +/* if a table had been copied out of the corpus rather than as a */ +/* statement about which scheme is in use. What is here is a character */ +/* correspondence — the alphabet on one side and the alphabet on the */ +/* other — and it has to match theirs exactly or the roots come out */ +/* wrong. See the corpus's entry in SOURCES.md for its terms. */ /* ------------------------------------------------------------------ */ -/** The corpus's own transliteration table — roots and lemmas arrive in it. */ +/** The scheme the corpus writes its roots and lemmas in, read back to Arabic. */ const BUCKWALTER = { "'": "ء", ">": "أ", "&": "ؤ", "<": "إ", "}": "ئ", A: "ا", b: "ب", p: "ة", t: "ت", v: "ث", @@ -261,18 +276,17 @@ mkdirSync(join(OUT_DIR, "root"), { recursive: true }); // this file". These shards are exactly that — every root↔ayah pair in the // corpus — so the notice ships *with the assets*, not only in the repo's // SOURCES.md, and the app's root lens carries the visible credit + link. -// Copied from the source file's own header so it can never drift from it. -// Verbatim, from the file's opening sentinel through the closing one, so the -// notice can never drift from the source it is quoting. -const HEAD = MORPHOLOGY.split("\n").slice(0, 40); -const rule = HEAD.findLastIndex((l) => l.startsWith("#====")); -if (rule < 4) throw new Error("copyright block not found at the head of the morphology file"); +// The block is read out of the source file itself by `copyrightBlock()`, which +// build-adjacency.mjs also calls: two trees derived from this corpus reproduce +// one quotation, and a quotation with two readers is a quotation that drifts. +// The bytes below are unchanged by that extraction — the function is the same +// find-the-last-rule walk that lived here, moved to where both callers are. writeFileSync( join(OUT_DIR, "NOTICE.txt"), `Derived from the Quranic Arabic Corpus morphology (version 0.4).\n` + `These files contain root and lemma annotation only — no Quran text.\n` + `Generated by packages/etl/scripts/build-roots.mjs; see SOURCES.md.\n\n` + - `${HEAD.slice(0, rule + 1).join("\n")}\n`, + `${copyrightBlock()}\n`, ); /** One entry per line: compact like a data file, diffable like source. */ diff --git a/packages/etl/scripts/build-sittings-index.mjs b/packages/etl/scripts/build-sittings-index.mjs new file mode 100644 index 00000000..f1e10a8a --- /dev/null +++ b/packages/etl/scripts/build-sittings-index.mjs @@ -0,0 +1,729 @@ +/** + * The front door to the sittings, with counts it cannot get wrong. + * + * ── Why this exists ────────────────────────────────────────────────────── + * + * There was a front door already, and it was hand-written. On 2026-08-15 it was + * telling a reader that 26 marks were answered and 1,851 were left, cut into + * sixteen sittings of about 116. Every one of those numbers was false: 167 were + * answered, 1,710 were left, and the sittings behind the very links on that page + * held 106 or 107 apiece. The page had not moved since the day it was typed, and + * the sittings behind it had been rebuilt twice. + * + * That is not a typo, it is the shape of the mistake. A page whose numbers are + * typed is right on the day it is typed and drifts silently every day after, + * and this one drifts in the direction that costs the most: a reader who is told + * there are 1,851 marks left budgets sixteen hours against a number that is a day + * and a hundred and forty-one marks out of date, and nothing on the screen ever + * admits it. The count is the only reason to open this page rather than the + * sittings directly. + * + * ── Where the numbers come from ────────────────────────────────────────── + * + * Every sitting already carries its own census. `build-mark-report.mjs` writes a + * HEAD block into each emitted page recording the population it was drawn from, + * how many marks were already answered and so left out, how many it is showing, + * and the fingerprint of the deal. This reads those blocks back — through the one + * reading in `lib/sitting-file.mjs`, shared with the auditor — and adds them up. + * + * So the page cannot disagree with the sittings it links to, because it is built + * out of them. If a sitting is missing from disk it does not appear here — a link + * to a page that is not there is worse than no link. + * + * The one thing it does NOT read out of them is the prose describing each band of + * confidence. Those are editorial — "barely accepted", "where nine marks in ten + * live" — and belong to whoever is explaining the plan, not to the data. + * + * ── Why the page then works all of it out again ────────────────────────── + * + * Everything above is true at the moment of the build and only then, and the + * things that change afterwards are precisely the things a reader came to find + * out. A sitting gets finished — and it kept its place in the list looking exactly + * as unfinished as the fifteen beside it, because being finished is a fact about + * the answers and the page had counted those once. A deal gets re-dealt, and the + * page went on describing parts that no longer held those marks. The only cure + * for either was somebody remembering to run this script again, and forgetting is + * how the hand-written page it replaced went wrong in the first place. Automating + * the regeneration would only move the forgetting. + * + * So the page asks, on every open, and works the whole thing out again: which + * sittings are on the disk now and what each is asking about, and everything this + * machine has heard anybody answer. Not a patch over the baked numbers — the same + * functions that drew the tiles are run again on the new listing, so no version of + * the page is half of one census and half of another. Finished sittings fold away + * as they are finished, the totals fall as the answers arrive, and a re-deal + * underneath an open tab says so instead of being invisible. + * + * That is also why the baked half exists rather than an empty frame: a page opened + * off the filesystem, or served by something that only hands out files, keeps + * every number it was written with. Nothing listening is a normal state. + * + * Neither the rule for *which answers still stand* nor any of the counting and + * drawing is restated for the browser. Those are `lib/answered.mjs` and + * `lib/sittings-view.mjs`, both written closed over nothing for this reason, and + * their own source text is inlined into the page. Two runtimes, one reading. + * + * ── The stamp, and the address ─────────────────────────────────────────── + * + * The page says when it was built and against which deal. That is what lets the + * next person spot in one glance what nobody spotted for a day: a front door older + * than the rooms behind it. + * + * It also knows the one address it is meant to be reached at, and says so when it + * is being read at another spelling of the same machine. A browser keeps a + * reader's place per address, compared as text, so the name and the number are two + * different memories of the same sitting — a trap that has already cost this + * project an hour of somebody's confidence. + * + * Usage: + * node packages/etl/scripts/build-sittings-index.mjs [--dir packages/etl/out] + * node packages/etl/scripts/build-sittings-index.mjs --answered out/mark-answers.jsonl + */ +import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { readAnswered, standingIds } from "./lib/answered.mjs"; +import { readSitting } from "./lib/sitting-file.mjs"; +import { + Word, + bandCopy, + bandsLine, + bandTile, + carryOn, + census, + doneCount, + esc, + howLong, + isDone, + num, + partTile, + sizeOf, + word, +} from "./lib/sittings-view.mjs"; +import { canonicalAddress } from "./lib/tailnet.mjs"; + +const arg = (name, fallback) => { + const i = process.argv.indexOf(name); + return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback; +}; + +const DIR = resolve(arg("--dir", new URL("../out", import.meta.url).pathname)); +const OUT = join(DIR, arg("--out", "index.html")); + +// Roughly what a mark costs a reader, in seconds. Taken from the sittings already +// sat rather than guessed: the hand-written page it replaces put 30 marks at about +// a quarter of an hour and 116 at about an hour, which is the same rate twice. +// It is an estimate and the page says so; it exists to stop somebody opening a +// sitting expecting five minutes. +const SECONDS_PER_MARK = 31; + +// The editorial half. Keyed by the band each sitting was built with, because that +// is what the file carries. A band with no entry here still gets listed — under +// its own range, which is ugly and visible, rather than dropped, which is not. +const BANDS = [ + ["0.55-0.65", "Barely accepted", "the weakest matches that were still allowed through"], + ["0.65-0.75", "Not very sure", "a poor match, but not the worst"], + ["0.75-0.85", "Fairly sure", "a decent match"], + ["0.85-0.95", "Sure", "where nine marks in ten live"], + ["0.95-1.01", "As close as it gets", "near-perfect matches"], +]; + +// The counting, the wording and the drawing all live in lib/sittings-view.mjs, +// because the page has to do every one of them again in a browser against a +// listing this script never saw. Nothing about how a tile looks or how a total +// is reached is written here; what is written here is the editorial half above +// and the prose below. +const hours = (n) => howLong(n, SECONDS_PER_MARK); + +if (!existsSync(DIR)) { + console.error(` no such directory: ${DIR}`); + process.exit(2); +} + +// ── Read the sittings back ─────────────────────────────────────────────── + +const sittings = []; +for (const name of readdirSync(DIR).sort()) { + if (!name.startsWith("sit.") || !name.endsWith(".html")) continue; + const s = readSitting(join(DIR, name)); + if (!s) { + console.error(` ${name}: no readable header — skipped`); + continue; + } + // A part whose card list is torn still gets listed, because its header carries + // the counts and dropping it would understate the work left. It cannot show + // progress, and the fault is printed so the reason is not a mystery. + for (const fault of s.faults) console.error(` ${name}: ${fault}`); + sittings.push({ name, ids: s.ids, ...s.head, mtime: statSync(join(DIR, name)).mtime }); +} + +const { bands, parts, deal, mixed, shown, answered, population, per, dealId } = census(sittings); + +if (!bands.length && !parts.length) { + console.error(` no sittings found in ${DIR} — nothing to index`); + process.exit(1); +} + +// Parts on disk from two different builds is the one state where an index would +// quietly mislead about how much work is left. The page, meeting the same thing +// live, says so and carries on; a builder can simply decline to write it. +if (mixed.length) { + console.error(` parts on disk are from more than one deal:`); + for (const p of [deal, ...mixed]) console.error(` ${p.name} ${p.slice}`); + console.error(` rebuild all of them before indexing.`); + process.exit(1); +} + +/** + * How far each part has got, as it stands on disk at this moment. + * + * Defaulted, unlike the auditor's identical-looking flag, and the difference is + * the point of each. The auditor delivers a verdict on freshness, so a default + * there would let it pronounce confidently on a list it invented. This delivers a + * progress bar which the page re-fetches and overwrites the moment it is opened + * against a live server — so the cost of reading the wrong file is a number that + * is briefly stale, and the cost of reading nothing is a page that always opens + * claiming no work has been done. What it read is printed either way. + */ +const answeredPaths = String(arg("--answered", "")) + .split(",") + .map((p) => p.trim()) + .filter(Boolean); +if (!answeredPaths.length) { + const running = join(DIR, "mark-answers.jsonl"); + if (existsSync(running)) answeredPaths.push(running); +} +const standing = answeredPaths.length ? readAnswered(answeredPaths) : new Set(); + +// Local time, not UTC. The reader compares this against a file listing on the same +// machine, and a stamp five hours off the listing reads as a stale page when the +// page is fine — which is the exact confusion this line exists to prevent. +const newest = sittings.map((s) => s.mtime).sort((a, b) => b - a)[0]; +const pad = (n) => String(n).padStart(2, "0"); +const stamp = + `${newest.getFullYear()}-${pad(newest.getMonth() + 1)}-${pad(newest.getDate())}` + + ` ${pad(newest.getHours())}:${pad(newest.getMinutes())}`; + +// ── The page, as it is true at this moment ─────────────────────────────── +// +// Everything below is baked through the very functions the page then re-runs in +// the browser, so what a reader sees before the machine answers and what they see +// after differ in their numbers and in nothing else. It is also what they keep +// when nothing answers at all — a page opened off the filesystem, or served by +// something that only hands out files, still shows a true picture of the moment +// it was written rather than an empty frame waiting for a fetch that will not +// come. +// +// A finished sitting is folded away rather than deleted. Removing it would make +// the front door quietly disagree with the directory behind it, and a reader who +// has just finished number nine wants one glance's worth of proof that it landed. + +const sortDone = (list, tile) => { + const open = []; + const shut = []; + for (const [s, n] of list) (isDone(s, n) ? shut : open).push(tile(s, n)); + return { open: open.join("\n"), shut: shut.join("\n"), left: open.length, gone: shut.length }; +}; + +const bandRows = sortDone( + bands.map((s) => [s, doneCount(s, standing)]), + (s, n) => { + const { title, blurb } = bandCopy(s.band, BANDS); + return ` ${bandTile(s, n, title, blurb)}`; + }, +); + +const partRows = sortDone( + parts.map((s) => [s, doneCount(s, standing)]), + (s, n) => ` ${partTile(s, n)}`, +); + +const partsDone = parts.reduce((t, s) => t + doneCount(s, standing), 0); +const carry = carryOn(parts, standing); +const bandCheck = bandsLine(bands, standing); + +/** The line on a fold, which has to say what is behind it or nobody opens it. */ +const foldLine = (gone, one, many) => + `${Word(gone)} finished ${gone === 1 ? one : many}`; + +const carryLine = carry + ? `Carry on with sitting ${esc(String(carry.sitting.part).split("/")[0])}. ` + + (carry.done + ? `You are ${carry.done} of ${carry.of} through it. ` + : "Nothing has been answered in it yet. ") + + `Open it.` + : ""; + +const WHERE = canonicalAddress(); + +const html = ` + + + + +Which sitting? + + + + + + +

      Which sitting?

      + +

      Each one shows you a mark on a page of the mus'haf with a box drawn around it, +one at a time, and asks whether the box is on the right thing. There is no score and no running +tally — a reader who can see the score answers the score.

      + + + +

      ${carryLine}

      + +

      Does the machine know which ones it got wrong?

      + +

      Every mark carries a number saying how well its box matched the ink underneath. The whole +plan below rests on that number meaning something: that the marks it was least sure about really +are the ones most likely to be wrong. ${bandCheck}

      + +

      These ${word(bands.length)} sittings check it. Each draws +${word(bands[0]?.shown ?? 0)} marks from a different level of confidence. +If the barely-accepted ones turn out no worse than the near-perfect ones, the number is not telling +us where to look, and there is no point following it through the sittings below.

      + +

      ${Word(bands.length)} sittings · ${num(bands[0]?.shown ?? 0)} marks each · ${hours(bands[0]?.shown ?? 0)} each

      + +

      Start with the first +one. It is the least convincing band — if anything is wrong anywhere, it is likeliest to +be wrong here.

      + +
        +${bandRows.open} +
      +

      All of them have been sat.

      + +
      + ${foldLine(bandRows.gone, "check", "checks")} +
        +${bandRows.shut} +
      +
      + +

      The marks nothing could place

      + +

      For ${num(population)} marks the machine could not +find its own ink well enough to trust, so it fell back to putting them where the printed line said. +These are the ones most likely to be visibly wrong, and they are few enough to look at all +of them.

      + +

      ${num(answered + partsDone)} ${answered + partsDone === 1 ? "is" : "are"} answered and gone. What is +left is ${num(shown - partsDone)}, cut into ${word(parts.length)} +sittings of about ${per}. Every mark appears in exactly one sitting, so +finishing all ${word(parts.length)} means every one has been seen — not +sampled, seen.

      + +

      The ${word(parts.length)} are re-dealt each time answered marks come +out, so a part is a fresh mix rather than the part of the same number you sat before. Nothing is +asked twice and nothing is dropped.

      + +

      ${Word(parts.length)} sittings · about ${per} marks each · ${hours(per)} each

      + +
        +${partRows.open} +
      +

      Every one has been seen.

      + +
      + ${foldLine(partRows.gone, "sitting", "sittings")} +
        +${partRows.shut} +
      +
      + +

      Which address should I open this at?

      + +

      ${WHERE.onPrivateNetwork + ? `This one: ${esc(WHERE.host)}. The Mac answers to other spellings of the same +name, and every one of them is somewhere else entirely as far as a browser is concerned — a +sitting begun at one opens at card one at the other. Nothing is lost when that happens, because +the Mac hands everything back, but the minute of wondering what went wrong is real. Use the same +spelling every time and it never comes up.` + : `Wherever this page is being served from — the private network is not up on the Mac, so there +is only one address and it only works on the Mac itself. A phone cannot reach these sittings until +that network is running.`}

      + +

      Where do your answers go?

      + +

      Two places, both the moment you give the answer — nothing waits for the end. The browser you +are holding keeps its own copy, and the Mac writes the answer down as it arrives.

      + +

      The Mac's copy is what makes the count come down. Marks you have answered are left out of the +next set of sittings, so the numbers on this page fall as you work rather than staying where they +started.

      + +

      Under every card there is Hand over what I have said so far. It does not end +the sitting and it does not lose your place — it hands over a complete copy of everything you +have said and lets you carry on. Press it whenever you like, as often as you like.

      + +

      What if a sitting opens as though you had never touched it?

      + +

      It will not stay that way. A browser sometimes loses what it was keeping — most often +because the same sitting was opened at a different address, which counts as somewhere else +entirely as far as the browser is concerned. When that happens the page asks the Mac for +everything you have said, puts every corrected box back where you put it, and drops you one +card past the last one you answered. Give it a second; it will tell you how many came back.

      + +

      So nothing you have answered is ever lost by opening the wrong link. What +does not come back is anything you gave while the Mac was asleep and have not handed over +since — that lives only in the browser you gave it in.

      + +

      What if it offers you a download instead?

      + +

      Then the Mac is not hearing you, and that browser holds the only copy of what you have said. +Keep the file — it has everything, including anything that did not reach the Mac.

      + +

      The likeliest reason is a page that was already open before the Mac started listening. Reload +it and carry on; your answers and your place both survive a reload.

      + +

      While it is offering downloads it also cannot give a sitting back to you, so a sitting the +browser has forgotten will stay forgotten until the Mac is answering again.

      + +

      Does it work while the Mac is asleep?

      + +

      No. The pages are served from it, so if it sleeps they stop loading until it wakes, and it +stops hearing answers while it is out. A sitting already open on the screen keeps working and +keeps its own copy of everything you say — so what a sleeping Mac costs you is the next page you +try to load, not the hour you have already spent. Press Hand over what I have said so +far once it is awake again and the two are back in step.

      + +

      Are these numbers current?

      + +

      They are counted out of the sittings themselves rather than typed here, so this page cannot +say one thing while the sittings behind the links say another. The bars on the sittings are asked +of the Mac each time this page is opened, so they are current to the second while it is +answering — and frozen at the moment below when it is not. Timings are estimates.

      + +

      built ${esc(stamp)} · deal ${esc(dealId)} · +${num(shown - partsDone)} of ${num(population)} left

      + + + + + +`; + +writeFileSync(OUT, html); +console.log(` index → ${OUT}`); +console.log(` ${bands.length} band sittings · ${parts.length} parts · ${num(shown)} of ${num(population)} marks left (${num(answered)} answered)`); +if (answeredPaths.length) console.log(` progress read from ${answeredPaths.map((p) => p.replace(`${DIR}/`, "")).join(", ")} (${standing.size} standing)`); +else console.log(` no answers on disk to read progress from — every part opens at nothing done`); +console.log(` address baked in: ${WHERE.onPrivateNetwork ? WHERE.host : "none — the private network is not up"}`); diff --git a/packages/etl/scripts/build-sittings-index.test.mjs b/packages/etl/scripts/build-sittings-index.test.mjs new file mode 100644 index 00000000..0a15aa36 --- /dev/null +++ b/packages/etl/scripts/build-sittings-index.test.mjs @@ -0,0 +1,405 @@ +/** + * The front door, and the two things it is easy to get wrong there. + * + * The first is the mistake it was written to end: a page whose numbers were typed, + * right on the day they were typed and quietly wrong every day after. Those numbers + * are now counted out of the sittings, so what has to be tested is that the counting + * is real — that a part with one answer against it says one, and that an answer for + * a mark in no part on the page is not counted at all. That last one is the trap: the + * running log covers every deal there has ever been, and adding its length to + * anything would take the remaining figure below zero. + * + * The second is subtler and is the reason `standingIds` was written closed over + * nothing. The page has to answer *which answers still stand* in a browser, and the + * tempting way to do that is to write the arithmetic out again in the generated + * script — three lines, hard to get wrong, and wrong within a month of the next time + * somebody changes what a retraction means. Instead the builder ships that + * function's own source text. These tests are what makes that claim checkable: the + * inlined copy is pulled back out of the page, evaluated with nothing in scope, and + * made to agree with the module it came from. + */ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { standingIds } from "./lib/answered.mjs"; +import * as view from "./lib/sittings-view.mjs"; + +const SCRIPT = new URL("./build-sittings-index.mjs", import.meta.url).pathname; + +let dir; + +function sitting(name, head, ids) { + const full = { built: "mark-report", set: "fallback", seed: 23, band: null, ...head }; + writeFileSync( + join(dir, name), + [ + "", + ].join("\n"), + ); +} + +/** Three parts of two marks each, as a fresh rebuild leaves them. */ +function deal({ already = 0 } = {}) { + const pool = [["1:1", "1:2"], ["2:1", "2:2"], ["3:1", "3:2"]]; + for (let n = 1; n <= 3; n += 1) { + sitting(`sit.part-${n}.html`, { + slice: `-p${n}of3-aXX`, + part: `${n}/3`, + pool: 6, + population: 6 + already, + alreadyAnswered: already, + shown: 2, + }, pool[n - 1]); + } +} + +function answers(ids, name = "mark-answers.jsonl") { + writeFileSync(join(dir, name), `${ids.map((id) => JSON.stringify({ kind: "placement", id })).join("\n")}\n`); +} + +// Both streams, always. The builder says what it could not read on the error stream +// and keeps going, so a helper that only kept the output stream would make the +// carries-on-and-says-so case indistinguishable from the says-nothing case. +function build(...extra) { + const r = spawnSync(process.execPath, [SCRIPT, "--dir", dir, ...extra], { encoding: "utf8" }); + return { + code: r.status, + out: `${r.stdout}${r.stderr}`, + html: r.status === 0 ? readFileSync(join(dir, "index.html"), "utf8") : null, + }; +} + +// Digits, not any text: the page also carries the source of the function that +// draws these, and its own unfilled template would otherwise count as a tile. +const tiles = (html) => [...html.matchAll(/(\d+ of \d+)<\/span>/g)].map((m) => m[1]); + +/** The tiles inside one list, by the part number each links to. */ +const listed = (html, id) => { + const m = new RegExp(`id="${id}"[^>]*>([\\s\\S]*?)`).exec(html); + return m ? [...m[1].matchAll(/data-part="(\d+)"/g)].map((x) => x[1]) : null; +}; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "hifth-front-door-")); +}); +afterEach(() => rmSync(dir, { recursive: true, force: true })); + +describe("counting out of the sittings rather than out of somebody's memory", () => { + it("draws one tile per part", () => { + deal(); + const r = build(); + expect(r.code).toBe(0); + expect(listed(r.html, "parts")).toEqual(["1", "2", "3"]); + expect(tiles(r.html)).toEqual(["0 of 2", "0 of 2", "0 of 2"]); + }); + + it("shows how far each part has got, from the answers on disk", () => { + deal(); + answers(["2:1"]); + expect(tiles(build().html)).toEqual(["0 of 2", "1 of 2", "0 of 2"]); + }); + + // The trap. One log covers every deal there has ever been, so most of what is in + // it belongs to marks that were dropped from these parts at build time — and were + // already subtracted from the totals the page prints. + it("ignores answers for marks that are in no part on this page", () => { + deal({ already: 4 }); + answers(["9:9", "8:8", "2:1"]); + const r = build(); + expect(tiles(r.html)).toEqual(["0 of 2", "1 of 2", "0 of 2"]); + expect(/id="left">(\d+)/.exec(r.html)[1]).toBe("5"); + }); + + it("takes a retraction back out again", () => { + deal(); + writeFileSync( + join(dir, "mark-answers.jsonl"), + `${[{ kind: "placement", id: "1:1" }, { kind: "placement", id: "1:2" }, { kind: "retracted", id: "1:2" }].map((e) => JSON.stringify(e)).join("\n")}\n`, + ); + expect(tiles(build().html)[0]).toBe("1 of 2"); + }); + + it("opens at nothing done when nobody has answered anything yet", () => { + const r = (deal(), build()); + expect(r.out).toContain("no answers on disk"); + expect(tiles(r.html)).toEqual(["0 of 2", "0 of 2", "0 of 2"]); + }); + + /** + * The page used to carry every mark id of every part — twelve bytes a mark and + * about fifteen kilobytes of it — because that was the only way it could recount + * itself against the server. It asks the server for the listing now, ids and all, + * so shipping a second copy would be a stale second copy. + */ + it("no longer ships a copy of which marks are in which part", () => { + deal(); + const html = build().html; + expect(html).not.toContain("data-ids"); + expect(html).not.toContain("1:1"); + }); +}); + +/** + * The thing the front door could not do, which is why it was a photograph. + * + * Finishing a sitting is what a reader is there to do, and until this the page had + * no way to show it: the tile stayed in the list looking exactly as unfinished as + * the ones beside it. Folding rather than deleting is deliberate — a front door + * that silently drops rooms cannot be checked against the directory behind it, and + * somebody who has just finished number two wants one glance's worth of proof. + */ +describe("getting finished sittings out of the way", () => { + it("moves a finished part into the fold and leaves the rest in the list", () => { + deal(); + answers(["2:1", "2:2"]); + const html = build().html; + expect(listed(html, "parts")).toEqual(["1", "3"]); + expect(listed(html, "parts-done")).toEqual(["2"]); + expect(/([^<]*)/.exec(html)[1]).toBe("One finished sitting"); + }); + + it("keeps the fold shut away entirely when nothing is finished", () => { + deal(); + const html = build().html; + expect(listed(html, "parts-done")).toEqual([]); + expect(/